@fmsim/machine 2.0.0-beta.10 → 2.0.0-beta.100
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 +6 -2
- package/dist/agv.js.map +1 -1
- package/dist/carrier-manager.js +183 -0
- package/dist/carrier-manager.js.map +1 -0
- package/dist/carrier.js +99 -9
- 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 +144 -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 +168 -81
- package/dist/factories/conveyor-3d.js.map +1 -1
- package/dist/factories/crane-3d.js +48 -12
- package/dist/factories/crane-3d.js.map +1 -1
- package/dist/factories/crane-rail-3d.js +6 -6
- package/dist/factories/crane-rail-3d.js.map +1 -1
- package/dist/factories/machine-3d.js +527 -181
- 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/factories/z-priority-defaults.js +33 -0
- package/dist/factories/z-priority-defaults.js.map +1 -0
- package/dist/factories/z-priority.js +54 -0
- package/dist/factories/z-priority.js.map +1 -0
- package/dist/features/conveyor-mixin.js.map +1 -1
- package/dist/features/mcs-status-default.js +15 -1
- package/dist/features/mcs-status-default.js.map +1 -1
- package/dist/features/mcs-status-mixin.js +22 -0
- package/dist/features/mcs-status-mixin.js.map +1 -1
- package/dist/features/mcs-zone-mixin.js.map +1 -1
- package/dist/features/parent-object-mixin.js +15 -7
- package/dist/features/parent-object-mixin.js.map +1 -1
- package/dist/floating-container.js +5 -0
- package/dist/floating-container.js.map +1 -1
- package/dist/index.js +52 -34
- package/dist/index.js.map +1 -1
- package/dist/mcs-carrier-holder.js +56 -22
- package/dist/mcs-carrier-holder.js.map +1 -1
- package/dist/mcs-machine.js +39 -0
- package/dist/mcs-machine.js.map +1 -1
- package/dist/mcs-unit.js +20 -0
- package/dist/mcs-unit.js.map +1 -1
- package/dist/mcs-vehicle.js +8 -3
- package/dist/mcs-vehicle.js.map +1 -1
- package/dist/node.js +10 -3
- package/dist/node.js.map +1 -1
- package/dist/oht.js +4 -0
- package/dist/oht.js.map +1 -1
- package/dist/port-flow.js +4 -0
- package/dist/port-flow.js.map +1 -1
- package/dist/port.js +4 -0
- package/dist/port.js.map +1 -1
- package/dist/scanner.js +30 -0
- package/dist/scanner.js.map +1 -0
- package/dist/scene/root-container-override.js +160 -96
- package/dist/scene/root-container-override.js.map +1 -1
- package/dist/shelf.js +4 -0
- package/dist/shelf.js.map +1 -1
- package/dist/static-container.js +5 -0
- package/dist/static-container.js.map +1 -1
- package/dist/stocker-abnormal-bar.js +5 -1
- package/dist/stocker-abnormal-bar.js.map +1 -1
- package/dist/stocker-capacity-bar.js +253 -6
- package/dist/stocker-capacity-bar.js.map +1 -1
- package/dist/stocker-carrier-bar.js +5 -1
- package/dist/stocker-carrier-bar.js.map +1 -1
- package/dist/stocker.js +6 -0
- package/dist/stocker.js.map +1 -1
- package/dist/templates/backdrop.js +30 -0
- package/dist/templates/backdrop.js.map +1 -0
- package/dist/templates/decal.js +21 -0
- package/dist/templates/decal.js.map +1 -0
- package/dist/templates/index.js +7 -1
- 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/crane-position.js +83 -0
- package/dist/utils/crane-position.js.map +1 -0
- package/dist/utils/data-value.js +21 -0
- package/dist/utils/data-value.js.map +1 -0
- package/dist/utils/fit-gauge-scale.js +28 -0
- package/dist/utils/fit-gauge-scale.js.map +1 -0
- package/dist/utils/get-value-on-ranges.js +2 -4
- package/dist/utils/get-value-on-ranges.js.map +1 -1
- package/dist/utils/stocker-gauge.js +78 -0
- package/dist/utils/stocker-gauge.js.map +1 -0
- package/dist/zone-capacity-bar.js +3 -3
- package/dist/zone-capacity-bar.js.map +1 -1
- package/icons/backdrop.png +0 -0
- package/icons/decal.png +0 -0
- package/jest.config.js +10 -0
- package/package.json +4 -3
- package/src/agv.ts +7 -2
- package/src/carrier-manager.ts +223 -0
- package/src/carrier.ts +103 -10
- package/src/conveyor-join.ts +5 -2
- package/src/conveyor.ts +8 -0
- package/src/crane.ts +181 -1
- package/src/factories/agv-3d.ts +9 -10
- package/src/factories/conveyor-3d.ts +171 -84
- package/src/factories/crane-3d.ts +53 -12
- package/src/factories/crane-rail-3d.ts +8 -7
- package/src/factories/machine-3d.ts +616 -191
- package/src/factories/oht-3d.ts +20 -20
- package/src/factories/shuttle-3d.ts +2 -6
- package/src/factories/z-priority-defaults.ts +38 -0
- package/src/factories/z-priority.ts +60 -0
- package/src/features/conveyor-mixin.ts +1 -1
- package/src/features/mcs-status-default.ts +15 -1
- package/src/features/mcs-status-mixin.ts +26 -4
- package/src/features/mcs-zone-mixin.ts +1 -1
- package/src/features/parent-object-mixin.ts +17 -8
- package/src/floating-container.ts +5 -0
- package/src/index.ts +58 -34
- package/src/mcs-carrier-holder.ts +57 -34
- package/src/mcs-machine.ts +42 -0
- package/src/mcs-unit.ts +24 -1
- package/src/mcs-vehicle.ts +9 -6
- package/src/node.ts +9 -3
- package/src/oht.ts +6 -1
- package/src/port-flow.ts +5 -0
- package/src/port.ts +5 -0
- package/src/scanner.ts +29 -0
- package/src/scene/root-container-override.ts +154 -103
- package/src/shelf.ts +5 -0
- package/src/static-container.ts +5 -0
- package/src/stocker-abnormal-bar.ts +6 -1
- package/src/stocker-capacity-bar.ts +298 -7
- package/src/stocker-carrier-bar.ts +6 -1
- package/src/stocker.ts +7 -0
- package/src/templates/backdrop.ts +30 -0
- package/src/templates/decal.ts +21 -0
- package/src/templates/index.ts +7 -1
- package/src/templates/scanner.ts +20 -0
- package/src/utils/crane-position.ts +105 -0
- package/src/utils/data-value.ts +19 -0
- package/src/utils/fit-gauge-scale.ts +35 -0
- package/src/utils/get-value-on-ranges.ts +2 -4
- package/src/utils/stocker-gauge.ts +111 -0
- package/src/zone-capacity-bar.ts +3 -3
- package/translations/en.json +26 -1
- package/translations/ja.json +26 -1
- package/translations/ko.json +26 -1
- package/translations/ms.json +26 -1
- package/translations/zh.json +26 -1
|
@@ -9,10 +9,19 @@
|
|
|
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
|
-
import { LEGEND_CAPACITY } from '../features/mcs-status-default'
|
|
15
|
-
import {
|
|
15
|
+
import { LEGEND_CAPACITY, resolveCarrierLegend } from '../features/mcs-status-default'
|
|
16
|
+
import { getValueOnRanges } from '../utils/get-value-on-ranges'
|
|
17
|
+
import { fitGaugeScale } from '../utils/fit-gauge-scale'
|
|
18
|
+
import {
|
|
19
|
+
carrierSegmentsFromData,
|
|
20
|
+
abnormalSegmentsFromData,
|
|
21
|
+
carrierColorsFromLegend,
|
|
22
|
+
ABNORMAL_COLORS
|
|
23
|
+
} from '../utils/stocker-gauge'
|
|
24
|
+
import { Z_FMSIM } from './z-priority'
|
|
16
25
|
|
|
17
26
|
// ── 설정 타입 ────────────────────────────────────────────
|
|
18
27
|
|
|
@@ -29,8 +38,8 @@ interface MCS3DConfig {
|
|
|
29
38
|
roughness: number
|
|
30
39
|
/** statusColor 미사용 시 기본 색상 */
|
|
31
40
|
defaultColor: number
|
|
32
|
-
/** 'box' | 'hexagon' */
|
|
33
|
-
shape: 'box' | 'hexagon'
|
|
41
|
+
/** 'box' | 'hexagon' | 'plane' | 'triangle' */
|
|
42
|
+
shape: 'box' | 'hexagon' | 'plane' | 'triangle'
|
|
34
43
|
/** mesh 생성 전 가시성 판정 — false 반환 시 mesh 미생성 */
|
|
35
44
|
isVisible?: (component: Component) => boolean
|
|
36
45
|
/** 부모 대비 크기 비율 (기본 1.0). < 1이면 부모 중심에 배치. */
|
|
@@ -41,6 +50,14 @@ interface MCS3DConfig {
|
|
|
41
50
|
matteTop?: boolean
|
|
42
51
|
/** 상판에 2D 렌더링 텍스처 적용 */
|
|
43
52
|
texturedTop?: boolean
|
|
53
|
+
/** 자체 발광 강도 (0~1). 조명 약한 면에서도 색상 유지 */
|
|
54
|
+
emissiveIntensity?: number
|
|
55
|
+
/** 바닥면 유지 (기본 false — 바닥면 제거) */
|
|
56
|
+
keepBottomFace?: boolean
|
|
57
|
+
/** syncZPosOffset 고정값 (설정 시 다른 로직 무시) */
|
|
58
|
+
zPosOverride?: number
|
|
59
|
+
/** z-priority — polygonOffset 으로 z-fight 완화. 기본 0 (OBJECTS). */
|
|
60
|
+
zPriority?: number
|
|
44
61
|
}
|
|
45
62
|
|
|
46
63
|
/**
|
|
@@ -69,36 +86,68 @@ class MCSRealObject3D extends RealObjectGroup {
|
|
|
69
86
|
this._config = config
|
|
70
87
|
}
|
|
71
88
|
|
|
72
|
-
|
|
89
|
+
get zPriority(): number {
|
|
90
|
+
return this._config.zPriority ?? 0
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// width, height 절대값 (음수 값은 양수로 취급).
|
|
94
|
+
// `resizable:false` 컴포넌트(예: Scanner)는 state.width/height 가 없고
|
|
95
|
+
// bounds 만 있을 수 있으므로 bounds 를 fallback 으로 사용.
|
|
96
|
+
//
|
|
97
|
+
// `??` only catches null/undefined — a property-panel input that produced
|
|
98
|
+
// NaN would slip through and corrupt every downstream Math.* call. Use a
|
|
99
|
+
// finite check so NaN falls through to bounds → 10 like missing values do.
|
|
73
100
|
protected get absSize(): { w: number; h: number } {
|
|
74
|
-
const
|
|
101
|
+
const state = (this.component.state || {}) as any
|
|
102
|
+
const bounds = (this.component.bounds || {}) as any
|
|
103
|
+
const width = pickFinite(state.width, bounds.width, 10)
|
|
104
|
+
const height = pickFinite(state.height, bounds.height, 10)
|
|
75
105
|
return { w: Math.abs(width), h: Math.abs(height) }
|
|
76
106
|
}
|
|
77
107
|
|
|
78
108
|
get effectiveDepth(): number {
|
|
79
109
|
const { depth } = this.component.state
|
|
80
|
-
if (depth)
|
|
110
|
+
// Truthy `if (depth)` already rejected NaN/0/undefined, but be explicit
|
|
111
|
+
// so the intent (and the resolution chain) reads cleanly: only an
|
|
112
|
+
// explicit positive finite number wins; everything else → defaults.
|
|
113
|
+
if (typeof depth === 'number' && Number.isFinite(depth) && depth > 0) {
|
|
114
|
+
return depth
|
|
115
|
+
}
|
|
81
116
|
if (this._config.defaultDepth) return this._config.defaultDepth
|
|
82
117
|
const { w, h } = this.absSize
|
|
83
118
|
return Math.min(w, h) * this._config.depthRatio
|
|
84
119
|
}
|
|
85
120
|
|
|
86
121
|
protected get syncZPosOffset(): number {
|
|
122
|
+
// syncZPosOffset = placement + geometric (center origin = effectiveDepth/2)
|
|
123
|
+
// 중첩 보정(parent shift 상쇄)은 base RealObject.cz가 parent.geometricOffsetY를
|
|
124
|
+
// 빼는 방식으로 처리하므로 여기선 placement + 본인 geometric만 반환
|
|
125
|
+
const halfD = this.effectiveDepth / 2
|
|
126
|
+
// zPosOverride: 고정 높이 (OHTLine 등)
|
|
127
|
+
if (this._config.zPosOverride != null) {
|
|
128
|
+
return this._config.zPosOverride + halfD
|
|
129
|
+
}
|
|
87
130
|
// Port: 이송 표면 높이에 배치
|
|
88
131
|
if (this._config.defaultZPosFromParent) {
|
|
89
|
-
return TRANSPORT_SURFACE_HEIGHT
|
|
132
|
+
return TRANSPORT_SURFACE_HEIGHT + halfD
|
|
90
133
|
}
|
|
91
|
-
// Carrier:
|
|
134
|
+
// Carrier: 부모 유형에 따라 배치 위치 결정
|
|
92
135
|
if (this._config.sizeRatio && this._config.sizeRatio < 1) {
|
|
93
136
|
const parentType = this.component.parent?.state?.type
|
|
94
|
-
|
|
95
|
-
|
|
137
|
+
// OHT: 그리퍼 아래에 매달림 — placement = -effectiveDepth(carrier top이 OHT bottom)
|
|
138
|
+
if (parentType === 'OHT') {
|
|
139
|
+
return -this.effectiveDepth + halfD
|
|
140
|
+
}
|
|
141
|
+
if (parentType === 'Port' || parentType === 'Shelf' || parentType === 'Shuttle') {
|
|
142
|
+
return halfD
|
|
96
143
|
}
|
|
97
|
-
return TRANSPORT_SURFACE_HEIGHT
|
|
144
|
+
return TRANSPORT_SURFACE_HEIGHT + halfD
|
|
98
145
|
}
|
|
99
|
-
return
|
|
146
|
+
return halfD
|
|
100
147
|
}
|
|
101
148
|
|
|
149
|
+
// geometric 부분(center origin: depth/2)은 base 기본값 사용 — override 불필요
|
|
150
|
+
|
|
102
151
|
// 컴포넌트의 상태 색상(statusColor) 또는 fillStyle → Three.js 색상 변환
|
|
103
152
|
protected resolveColor(): number {
|
|
104
153
|
const comp = this.component as any
|
|
@@ -107,14 +156,14 @@ class MCSRealObject3D extends RealObjectGroup {
|
|
|
107
156
|
// '#F0F0F0'은 상태 미설정 시 기본 fallback — 무시하고 defaultColor 사용
|
|
108
157
|
try {
|
|
109
158
|
const sc = comp.statusColor
|
|
110
|
-
if (typeof sc === 'string' && sc && sc.toUpperCase() !== '#F0F0F0') return new THREE.Color(sc).getHex()
|
|
159
|
+
if (typeof sc === 'string' && sc && sc.toUpperCase() !== '#F0F0F0' && sc !== 'transparent') return new THREE.Color(sc).getHex()
|
|
111
160
|
} catch {
|
|
112
161
|
/* statusColor 미구현 시 무시 */
|
|
113
162
|
}
|
|
114
163
|
|
|
115
164
|
// 2) state.fillStyle fallback
|
|
116
165
|
const { fillStyle } = this.component.state
|
|
117
|
-
if (fillStyle && typeof fillStyle === 'string') {
|
|
166
|
+
if (fillStyle && typeof fillStyle === 'string' && fillStyle !== 'transparent') {
|
|
118
167
|
try {
|
|
119
168
|
return new THREE.Color(fillStyle).getHex()
|
|
120
169
|
} catch {
|
|
@@ -163,7 +212,7 @@ class MCSRealObject3D extends RealObjectGroup {
|
|
|
163
212
|
|
|
164
213
|
private buildBoxGeometry(w: number, d: number, h: number): THREE.BufferGeometry {
|
|
165
214
|
const geo = new THREE.BoxGeometry(w, d, h)
|
|
166
|
-
return this.removeBottomFace(geo)
|
|
215
|
+
return this._config.keepBottomFace ? geo : this.removeBottomFace(geo)
|
|
167
216
|
}
|
|
168
217
|
|
|
169
218
|
private buildHexagonGeometry(w: number, d: number, h: number): THREE.BufferGeometry {
|
|
@@ -194,6 +243,43 @@ class MCSRealObject3D extends RealObjectGroup {
|
|
|
194
243
|
return this.removeBottomFace(geo)
|
|
195
244
|
}
|
|
196
245
|
|
|
246
|
+
private buildPlaneGeometry(w: number, h: number): THREE.BufferGeometry {
|
|
247
|
+
const geo = new THREE.PlaneGeometry(w, h)
|
|
248
|
+
geo.rotateX(-Math.PI / 2)
|
|
249
|
+
return geo
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* 삼각형 prism — 2D Triangle 의 꼭짓점(state.x1/y1, x2/y2, x3/y3) 을 그대로 사용.
|
|
254
|
+
* things-scene 의 TriangleExtrude 와 동일한 방식 — 각 Scanner 인스턴스의
|
|
255
|
+
* 실제 정점 배치(방향, 비대칭 등) 가 3D 에 반영된다.
|
|
256
|
+
*
|
|
257
|
+
* `geometry.center()` 로 로컬 원점을 bounding-box 중심으로 맞추므로
|
|
258
|
+
* 후속 placement 와 정합.
|
|
259
|
+
*/
|
|
260
|
+
private buildTriangleGeometry(d: number): THREE.BufferGeometry {
|
|
261
|
+
const state = (this.component.state || {}) as any
|
|
262
|
+
const { x1 = 0, y1 = 0, x2 = 0, y2 = 0, x3 = 0, y3 = 0 } = state
|
|
263
|
+
|
|
264
|
+
const shape = new THREE.Shape()
|
|
265
|
+
shape.moveTo(x1, y1)
|
|
266
|
+
shape.lineTo(x2, y2)
|
|
267
|
+
shape.lineTo(x3, y3)
|
|
268
|
+
shape.closePath()
|
|
269
|
+
|
|
270
|
+
const geo = new THREE.ExtrudeGeometry(shape, {
|
|
271
|
+
steps: 1,
|
|
272
|
+
depth: d,
|
|
273
|
+
bevelEnabled: false,
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
// bounding box 중심으로 원점 정렬 후 Y-up 좌표계로 회전
|
|
277
|
+
geo.center()
|
|
278
|
+
geo.rotateX(Math.PI / 2)
|
|
279
|
+
|
|
280
|
+
return this.removeBottomFace(geo)
|
|
281
|
+
}
|
|
282
|
+
|
|
197
283
|
// ── Build ──
|
|
198
284
|
|
|
199
285
|
protected _ownMesh?: THREE.Mesh
|
|
@@ -217,19 +303,31 @@ class MCSRealObject3D extends RealObjectGroup {
|
|
|
217
303
|
const { opacity, metalness, roughness, shape } = this._config
|
|
218
304
|
|
|
219
305
|
const geometry =
|
|
220
|
-
shape === '
|
|
221
|
-
? this.
|
|
222
|
-
:
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
306
|
+
shape === 'plane'
|
|
307
|
+
? this.buildPlaneGeometry(w, h)
|
|
308
|
+
: shape === 'hexagon'
|
|
309
|
+
? this.buildHexagonGeometry(w, depth, h)
|
|
310
|
+
: shape === 'triangle'
|
|
311
|
+
? this.buildTriangleGeometry(depth)
|
|
312
|
+
: this.buildBoxGeometry(w, depth, h)
|
|
313
|
+
|
|
314
|
+
const isPlane = shape === 'plane'
|
|
315
|
+
const material = isPlane
|
|
316
|
+
? new THREE.MeshBasicMaterial({
|
|
317
|
+
color,
|
|
318
|
+
opacity,
|
|
319
|
+
transparent: opacity < 1,
|
|
320
|
+
side: THREE.DoubleSide,
|
|
321
|
+
})
|
|
322
|
+
: new THREE.MeshStandardMaterial({
|
|
323
|
+
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
|
+
})
|
|
233
331
|
// z-fighting 방지: 바닥면(floor grid)보다 항상 앞에 렌더링
|
|
234
332
|
material.polygonOffset = true
|
|
235
333
|
material.polygonOffsetFactor = -1
|
|
@@ -254,11 +352,14 @@ class MCSRealObject3D extends RealObjectGroup {
|
|
|
254
352
|
}
|
|
255
353
|
|
|
256
354
|
this._ownMesh = new THREE.Mesh(geometry, meshMaterial)
|
|
257
|
-
//
|
|
258
|
-
|
|
259
|
-
|
|
355
|
+
// ownGroup 기준(bottom-up): mesh 바닥을 y=0에 맞추고 ownGroup이 -d/2로 shift해
|
|
356
|
+
// center-origin object3d에 정렬됨.
|
|
357
|
+
// box/hexagon (center-local geometry): y=rawDepth/2 (바닥을 0에 맞춤)
|
|
358
|
+
// plane: y=1 (바닥 살짝 띄움)
|
|
359
|
+
this._ownMesh.position.y = isPlane ? 1 : rawDepth / 2
|
|
360
|
+
this._ownMesh.castShadow = !isPlane
|
|
260
361
|
this._ownMesh.receiveShadow = true
|
|
261
|
-
this.
|
|
362
|
+
this.components3D.add(this._ownMesh)
|
|
262
363
|
}
|
|
263
364
|
|
|
264
365
|
// ── Change handling ──
|
|
@@ -353,7 +454,7 @@ class MCSRealObject3D extends RealObjectGroup {
|
|
|
353
454
|
update() {
|
|
354
455
|
// 자신의 mesh만 제거 (자식 컴포넌트 3D 오브젝트 보존)
|
|
355
456
|
if (this._ownMesh) {
|
|
356
|
-
this.
|
|
457
|
+
this.components3D.remove(this._ownMesh)
|
|
357
458
|
disposeObject3D(this._ownMesh)
|
|
358
459
|
this._ownMesh = undefined
|
|
359
460
|
}
|
|
@@ -368,9 +469,9 @@ class MCSRealObject3D extends RealObjectGroup {
|
|
|
368
469
|
updateDimension() {}
|
|
369
470
|
|
|
370
471
|
updateAlpha() {
|
|
371
|
-
const
|
|
372
|
-
|
|
373
|
-
const effective =
|
|
472
|
+
const state = this.component.state
|
|
473
|
+
// alpha가 명시적으로 설정되어 있으면 그 값을 사용, 없으면 config 기본값 적용
|
|
474
|
+
const effective = ('alpha' in state) ? (state.alpha as number) : this._config.opacity
|
|
374
475
|
|
|
375
476
|
this.object3d.traverse((o: any) => {
|
|
376
477
|
if (!o.material) return
|
|
@@ -385,6 +486,20 @@ class MCSRealObject3D extends RealObjectGroup {
|
|
|
385
486
|
}
|
|
386
487
|
}
|
|
387
488
|
|
|
489
|
+
// ── 헬퍼 ────────────────────────────────────────────────
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* Returns the first argument that is a finite number. NaN slips past `??`
|
|
493
|
+
* (which only catches null/undefined), so without this a property-panel
|
|
494
|
+
* input producing NaN would corrupt absSize and cascade through Math.*.
|
|
495
|
+
*/
|
|
496
|
+
function pickFinite(...candidates: unknown[]): number {
|
|
497
|
+
for (const c of candidates) {
|
|
498
|
+
if (typeof c === 'number' && Number.isFinite(c)) return c
|
|
499
|
+
}
|
|
500
|
+
return 0
|
|
501
|
+
}
|
|
502
|
+
|
|
388
503
|
// ── 팩토리 등록 헬퍼 ─────────────────────────────────────
|
|
389
504
|
|
|
390
505
|
function register(type: string, overrides: Partial<MCS3DConfig> = {}) {
|
|
@@ -402,6 +517,7 @@ register('Equipment', {
|
|
|
402
517
|
metalness: 0.4,
|
|
403
518
|
roughness: 0.6,
|
|
404
519
|
defaultColor: 0x8899aa,
|
|
520
|
+
zPriority: Z_FMSIM.EQUIPMENT,
|
|
405
521
|
})
|
|
406
522
|
|
|
407
523
|
register('Buffer', {
|
|
@@ -411,21 +527,112 @@ register('Buffer', {
|
|
|
411
527
|
roughness: 0.7,
|
|
412
528
|
defaultColor: 0x99aa88,
|
|
413
529
|
shape: 'hexagon',
|
|
530
|
+
zPriority: Z_FMSIM.BUFFER,
|
|
414
531
|
})
|
|
415
532
|
|
|
416
533
|
register('AGVLine', {
|
|
417
|
-
depthRatio: 0
|
|
534
|
+
depthRatio: 0,
|
|
535
|
+
defaultDepth: 0.1,
|
|
418
536
|
metalness: 0.6,
|
|
419
537
|
roughness: 0.3,
|
|
420
538
|
defaultColor: 0x888899,
|
|
539
|
+
zPriority: Z_FMSIM.AGV_LINE,
|
|
421
540
|
})
|
|
422
541
|
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
542
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
543
|
+
// OHTLine — 천장 I-beam 레일
|
|
544
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
545
|
+
|
|
546
|
+
const OHT_LINE_RAIL_COLOR = 0xcccccc
|
|
547
|
+
const OHT_LINE_HEIGHT = 7 * TRANSPORT_SURFACE_HEIGHT // OHT 상단과 일치
|
|
548
|
+
|
|
549
|
+
class OHTLineRealObject3D extends RealObjectGroup {
|
|
550
|
+
private _mesh?: THREE.Mesh
|
|
551
|
+
|
|
552
|
+
get zPriority(): number { return Z_FMSIM.OHT_LINE }
|
|
553
|
+
get effectiveDepth(): number { return 0 }
|
|
554
|
+
protected get syncZPosOffset(): number { return OHT_LINE_HEIGHT }
|
|
555
|
+
|
|
556
|
+
build() {
|
|
557
|
+
super.build()
|
|
558
|
+
|
|
559
|
+
const { width = 10, height = 10 } = this.component.state
|
|
560
|
+
const w = Math.abs(width)
|
|
561
|
+
const h = Math.abs(height)
|
|
562
|
+
if (!w || !h) return
|
|
563
|
+
|
|
564
|
+
// I-beam 치수: 이송 방향 = 긴 변, 폭은 2D의 1/2
|
|
565
|
+
const isVertical = w < h
|
|
566
|
+
const shortSide = isVertical ? w : h // 2D 원본 짧은 쪽 폭
|
|
567
|
+
const beltW = shortSide * 0.25 // I-beam 폭: 원본의 1/4
|
|
568
|
+
const beamH = Math.max(beltW * 0.5, 3) // I-beam 전체 높이
|
|
569
|
+
const inset = shortSide * 0.5 // 양끝 여백: 원본 폭의 1/2
|
|
570
|
+
const railLen = (isVertical ? h : w) - inset * 2
|
|
571
|
+
const flangeH = beamH * 0.2 // 플랜지 두께
|
|
572
|
+
const flangeW = beltW * 0.8 // 플랜지 폭
|
|
573
|
+
const webH = beamH - flangeH * 2 // 웹 높이
|
|
574
|
+
const webW = Math.max(flangeW * 0.25, 1) // 웹 두께
|
|
575
|
+
|
|
576
|
+
const mat = new THREE.MeshStandardMaterial({
|
|
577
|
+
color: OHT_LINE_RAIL_COLOR,
|
|
578
|
+
metalness: 0.5,
|
|
579
|
+
roughness: 0.4,
|
|
580
|
+
})
|
|
581
|
+
|
|
582
|
+
const geoms: THREE.BufferGeometry[] = []
|
|
583
|
+
|
|
584
|
+
// 상부 플랜지
|
|
585
|
+
const topFlange = isVertical
|
|
586
|
+
? new THREE.BoxGeometry(flangeW, flangeH, railLen)
|
|
587
|
+
: new THREE.BoxGeometry(railLen, flangeH, flangeW)
|
|
588
|
+
topFlange.translate(0, -flangeH / 2, 0)
|
|
589
|
+
geoms.push(topFlange)
|
|
590
|
+
|
|
591
|
+
// 웹 (수직판)
|
|
592
|
+
const web = isVertical
|
|
593
|
+
? new THREE.BoxGeometry(webW, webH, railLen)
|
|
594
|
+
: new THREE.BoxGeometry(railLen, webH, webW)
|
|
595
|
+
web.translate(0, -flangeH - webH / 2, 0)
|
|
596
|
+
geoms.push(web)
|
|
597
|
+
|
|
598
|
+
// 하부 플랜지
|
|
599
|
+
const botFlange = isVertical
|
|
600
|
+
? new THREE.BoxGeometry(flangeW, flangeH, railLen)
|
|
601
|
+
: new THREE.BoxGeometry(railLen, flangeH, flangeW)
|
|
602
|
+
botFlange.translate(0, -flangeH - webH - flangeH / 2, 0)
|
|
603
|
+
geoms.push(botFlange)
|
|
604
|
+
|
|
605
|
+
this._mesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(geoms), mat)
|
|
606
|
+
this._mesh.castShadow = true
|
|
607
|
+
this.object3d.add(this._mesh)
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
update() {
|
|
611
|
+
if (this._mesh) {
|
|
612
|
+
this.object3d.remove(this._mesh)
|
|
613
|
+
this._mesh.geometry.dispose()
|
|
614
|
+
;(this._mesh.material as THREE.Material).dispose()
|
|
615
|
+
this._mesh = undefined
|
|
616
|
+
}
|
|
617
|
+
this.build()
|
|
618
|
+
this.updateTransform()
|
|
619
|
+
this.updateHidden()
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
onchange(after: Record<string, unknown>, before: Record<string, unknown>) {
|
|
623
|
+
if ('width' in after || 'height' in after || 'depth' in after ||
|
|
624
|
+
'fillStyle' in after || 'strokeStyle' in after) {
|
|
625
|
+
this.update()
|
|
626
|
+
return
|
|
627
|
+
}
|
|
628
|
+
super.onchange(after, before)
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
updateDimension() {}
|
|
632
|
+
updateAlpha() {}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
registerRealObjectFactory('OHTLine', (component: Component) => new OHTLineRealObject3D(component))
|
|
429
636
|
|
|
430
637
|
// STOCKER: 벽면에 용량 게이지 바를 포함하는 커스텀 3D 오브젝트
|
|
431
638
|
const STOCKER_CONFIG: MCS3DConfig = {
|
|
@@ -435,6 +642,7 @@ const STOCKER_CONFIG: MCS3DConfig = {
|
|
|
435
642
|
metalness: 0.3,
|
|
436
643
|
roughness: 0.6,
|
|
437
644
|
defaultColor: 0x8899bb,
|
|
645
|
+
zPriority: Z_FMSIM.STOCKER,
|
|
438
646
|
}
|
|
439
647
|
|
|
440
648
|
class StockerRealObject3D extends MCSRealObject3D {
|
|
@@ -452,7 +660,7 @@ class StockerRealObject3D extends MCSRealObject3D {
|
|
|
452
660
|
private buildGauge() {
|
|
453
661
|
const comp = this.component as any
|
|
454
662
|
const data = comp.data || {}
|
|
455
|
-
const gaugeMode = comp.root?.
|
|
663
|
+
const gaugeMode = comp.root?.stockerGaugeMode || 'capacity'
|
|
456
664
|
|
|
457
665
|
const gauge = this.resolveGauge(data, gaugeMode)
|
|
458
666
|
|
|
@@ -466,12 +674,20 @@ class StockerRealObject3D extends MCSRealObject3D {
|
|
|
466
674
|
const depth = this.effectiveDepth
|
|
467
675
|
const isWidthLonger = w >= h
|
|
468
676
|
|
|
469
|
-
// 게이지
|
|
470
|
-
const
|
|
471
|
-
const
|
|
677
|
+
// 게이지 최대 크기 (스토커가 충분히 크면 이 크기 그대로 사용).
|
|
678
|
+
const MAX_BAR_W = 130
|
|
679
|
+
const MAX_BAR_H = 26
|
|
680
|
+
|
|
681
|
+
// 스토커 지붕 footprint 에 맞춰 비례 축소 (확대는 안 함 — 현재 크기가 상한).
|
|
682
|
+
const fit = fitGaugeScale(w, h, MAX_BAR_W, MAX_BAR_H)
|
|
683
|
+
|
|
684
|
+
// PlaneGeometry 의 월드 크기는 clamp, 텍스처 캔버스 해상도는 최대 크기 기준 고정
|
|
685
|
+
// (축소되어도 선명도 유지).
|
|
686
|
+
const barW = MAX_BAR_W * fit
|
|
687
|
+
const barH = MAX_BAR_H * fit
|
|
472
688
|
const scale = 4
|
|
473
|
-
const canvasW =
|
|
474
|
-
const canvasH =
|
|
689
|
+
const canvasW = MAX_BAR_W * scale
|
|
690
|
+
const canvasH = MAX_BAR_H * scale
|
|
475
691
|
const canvas = document.createElement('canvas')
|
|
476
692
|
canvas.width = canvasW
|
|
477
693
|
canvas.height = canvasH
|
|
@@ -486,24 +702,32 @@ class StockerRealObject3D extends MCSRealObject3D {
|
|
|
486
702
|
ctx.lineWidth = 12
|
|
487
703
|
ctx.strokeRect(6, 6, canvasW - 12, canvasH - 12)
|
|
488
704
|
|
|
489
|
-
// 세그먼트 채움 (왼→오)
|
|
705
|
+
// 세그먼트 채움 (왼→오) + carrier/abnormal 은 세그먼트별 숫자(%) 표기 (2D 와 동일)
|
|
490
706
|
const innerW = canvasW - pad * 2
|
|
491
707
|
const innerH = canvasH - pad * 2
|
|
708
|
+
// 컴포넌트 폰트 설정(fontFamily/bold/italic) 을 그대로, 크기만 텍스처 scale 배 (2D 와 상대 크기 일치).
|
|
709
|
+
const fontSize = (Number((this.component as any).state?.fontSize) || 15) * scale
|
|
710
|
+
const font = this.gaugeFont(fontSize)
|
|
492
711
|
let x = pad
|
|
493
712
|
for (const seg of gauge.segments) {
|
|
494
713
|
const segW = Math.max(innerW * seg.ratio, seg.ratio > 0 ? 1 : 0)
|
|
495
714
|
if (segW <= 0) continue
|
|
496
715
|
ctx.fillStyle = seg.color
|
|
497
716
|
ctx.fillRect(x, pad, segW, innerH)
|
|
717
|
+
if (gauge.perSegment) {
|
|
718
|
+
this.drawGaugeSegmentNumber(ctx, seg.ratio, x, pad, segW, innerH, font, fontSize)
|
|
719
|
+
}
|
|
498
720
|
x += segW
|
|
499
721
|
}
|
|
500
722
|
|
|
501
|
-
//
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
723
|
+
// capacity: 전체 중앙에 usage% 한 개 (2D 와 동일). carrier/abnormal 은 위 세그먼트별 숫자 사용.
|
|
724
|
+
if (gauge.label) {
|
|
725
|
+
ctx.font = font
|
|
726
|
+
ctx.textAlign = 'center'
|
|
727
|
+
ctx.textBaseline = 'middle'
|
|
728
|
+
ctx.fillStyle = '#333333'
|
|
729
|
+
ctx.fillText(gauge.label, canvasW / 2, canvasH / 2)
|
|
730
|
+
}
|
|
507
731
|
|
|
508
732
|
// 텍스처 → Plane
|
|
509
733
|
const texture = new THREE.CanvasTexture(canvas)
|
|
@@ -523,62 +747,107 @@ class StockerRealObject3D extends MCSRealObject3D {
|
|
|
523
747
|
mat.polygonOffsetUnit = -2
|
|
524
748
|
|
|
525
749
|
this.gaugeMesh = new THREE.Mesh(geo, mat)
|
|
750
|
+
// ownGroup 기준(bottom-up): 볼륨 상단 = +depth. 그 위로 0.5 띄움.
|
|
526
751
|
this.gaugeMesh.position.set(0, depth + 0.5, 0)
|
|
527
|
-
this.
|
|
752
|
+
this.components3D.add(this.gaugeMesh)
|
|
528
753
|
}
|
|
529
754
|
|
|
530
|
-
private resolveGauge(
|
|
755
|
+
private resolveGauge(
|
|
756
|
+
data: any,
|
|
757
|
+
mode: string
|
|
758
|
+
): { segments: { ratio: number; color: string }[]; borderColor: string; label: string | null; perSegment: boolean } {
|
|
531
759
|
switch (mode) {
|
|
532
760
|
case 'carrier': {
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
const
|
|
536
|
-
const
|
|
537
|
-
const
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
{ ratio:
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
label: total > 0 ? `F${full} E${empty} EE${emptyEmpty}` : '',
|
|
548
|
-
}
|
|
761
|
+
// 세그먼트 비율/색상은 2D 와 공유되는 단일 소스에서 계산 (필드/색상 drift 방지).
|
|
762
|
+
// 색상은 carrier 컴포넌트와 동일한 legend(테마)에서 취득.
|
|
763
|
+
const colors = carrierColorsFromLegend(resolveCarrierLegend(this.component))
|
|
764
|
+
const [f, e, ee] = carrierSegmentsFromData(data, colors)
|
|
765
|
+
const total = f.count + e.count + ee.count
|
|
766
|
+
// 접두사 없는 세그먼트별 숫자(%)로 표기 (capacity·2D 와 동일). label(중앙 한 개) 미사용.
|
|
767
|
+
return total > 0
|
|
768
|
+
? {
|
|
769
|
+
segments: [f, e, ee].map(s => ({ ratio: s.ratio, color: s.color })),
|
|
770
|
+
borderColor: colors.full,
|
|
771
|
+
label: null,
|
|
772
|
+
perSegment: true,
|
|
773
|
+
}
|
|
774
|
+
: { segments: [{ ratio: 1, color: '#dddddd' }], borderColor: colors.full, label: null, perSegment: false }
|
|
549
775
|
}
|
|
550
776
|
case 'abnormal': {
|
|
551
|
-
const
|
|
552
|
-
const
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
777
|
+
const [n, a] = abnormalSegmentsFromData(data)
|
|
778
|
+
const total = n.count + a.count
|
|
779
|
+
return total > 0
|
|
780
|
+
? {
|
|
781
|
+
segments: [n, a].map(s => ({ ratio: s.ratio, color: s.color })),
|
|
782
|
+
borderColor: a.count > 0 ? ABNORMAL_COLORS.abnormal : ABNORMAL_COLORS.normal,
|
|
783
|
+
label: null,
|
|
784
|
+
perSegment: true,
|
|
785
|
+
}
|
|
786
|
+
: {
|
|
787
|
+
segments: [{ ratio: 1, color: '#dddddd' }],
|
|
788
|
+
borderColor: ABNORMAL_COLORS.normal,
|
|
789
|
+
label: null,
|
|
790
|
+
perSegment: false,
|
|
791
|
+
}
|
|
565
792
|
}
|
|
566
|
-
default: { // capacity
|
|
793
|
+
default: { // capacity — 전체 중앙에 usage% 한 개
|
|
567
794
|
const cur = Number(data.CURRENTCAPACITY) || 0
|
|
568
795
|
const max = Number(data.MAXCAPACITY) || 100
|
|
569
796
|
const fullup = data.FULLUP === 'T'
|
|
570
797
|
const usage = Math.round((cur / max) * 100)
|
|
571
|
-
const legend = (this.component as any).root?.
|
|
572
|
-
const color = fullup ? 'red' : (
|
|
798
|
+
const legend = (this.component as any).root?.stockerCapacityLegendTheme || LEGEND_CAPACITY
|
|
799
|
+
const color = fullup ? 'red' : (getValueOnRanges(usage, legend) || '#44aa44')
|
|
573
800
|
return {
|
|
574
801
|
segments: [{ ratio: usage / 100, color }],
|
|
575
802
|
borderColor: color,
|
|
576
803
|
label: `${usage}%`,
|
|
804
|
+
perSegment: false,
|
|
577
805
|
}
|
|
578
806
|
}
|
|
579
807
|
}
|
|
580
808
|
}
|
|
581
809
|
|
|
810
|
+
/** 컴포넌트 폰트 설정(fontFamily/bold/italic)으로 CSS font 문자열 조합. size 는 텍스처 스케일 반영값. */
|
|
811
|
+
private gaugeFont(sizePx: number): string {
|
|
812
|
+
const st = (this.component as any).state || {}
|
|
813
|
+
const parts: string[] = []
|
|
814
|
+
if (st.bold) parts.push('bold')
|
|
815
|
+
if (st.italic) parts.push('italic')
|
|
816
|
+
parts.push(`${sizePx}px`)
|
|
817
|
+
parts.push(st.fontFamily || 'sans-serif')
|
|
818
|
+
return parts.join(' ')
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
/** 세그먼트 영역 중심에 숫자(%)만 표기. 좁으면 값% → 값 → 생략 (2D _drawSegmentLabel 과 동일 정책). */
|
|
822
|
+
private drawGaugeSegmentNumber(
|
|
823
|
+
ctx: CanvasRenderingContext2D,
|
|
824
|
+
ratio: number,
|
|
825
|
+
x: number,
|
|
826
|
+
y: number,
|
|
827
|
+
w: number,
|
|
828
|
+
h: number,
|
|
829
|
+
font: string,
|
|
830
|
+
fontSize: number
|
|
831
|
+
) {
|
|
832
|
+
ctx.font = font
|
|
833
|
+
ctx.textAlign = 'center'
|
|
834
|
+
ctx.textBaseline = 'middle'
|
|
835
|
+
ctx.fillStyle = '#333333'
|
|
836
|
+
const value = `${Math.round(ratio * 100)}`
|
|
837
|
+
const cx = x + w / 2
|
|
838
|
+
const cy = y + h / 2
|
|
839
|
+
if (fontSize <= h && ctx.measureText(`${value}%`).width <= w) {
|
|
840
|
+
ctx.fillText(`${value}%`, cx, cy)
|
|
841
|
+
} else if (fontSize <= h && ctx.measureText(value).width <= w) {
|
|
842
|
+
ctx.fillText(value, cx, cy)
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
update() {
|
|
847
|
+
this.clearGauge()
|
|
848
|
+
super.update()
|
|
849
|
+
}
|
|
850
|
+
|
|
582
851
|
onchange(after: Record<string, unknown>, before: Record<string, unknown>) {
|
|
583
852
|
if ('data' in after) {
|
|
584
853
|
this.clearGauge()
|
|
@@ -594,7 +863,7 @@ class StockerRealObject3D extends MCSRealObject3D {
|
|
|
594
863
|
|
|
595
864
|
private clearGauge() {
|
|
596
865
|
if (this.gaugeMesh) {
|
|
597
|
-
this.
|
|
866
|
+
this.components3D.remove(this.gaugeMesh)
|
|
598
867
|
this.gaugeMesh.geometry?.dispose()
|
|
599
868
|
const mat = this.gaugeMesh.material as THREE.MeshBasicMaterial
|
|
600
869
|
mat?.map?.dispose()
|
|
@@ -610,16 +879,17 @@ registerRealObjectFactory('STOCKER', (component: Component) => new StockerRealOb
|
|
|
610
879
|
// Unit (이동체 · 홀더) — 낮은 depth, 불투명
|
|
611
880
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
612
881
|
|
|
613
|
-
// Port:
|
|
882
|
+
// Port: 바닥판 (PlaneGeometry)
|
|
614
883
|
const PORT_CONFIG: MCS3DConfig = {
|
|
615
884
|
...DEFAULTS,
|
|
616
|
-
depthRatio: 0.
|
|
885
|
+
depthRatio: 0.01,
|
|
617
886
|
defaultZPosFromParent: true,
|
|
618
|
-
opacity: 0.
|
|
619
|
-
metalness: 0.
|
|
620
|
-
roughness: 0.
|
|
887
|
+
opacity: 0.1,
|
|
888
|
+
metalness: 0.3,
|
|
889
|
+
roughness: 0.8,
|
|
621
890
|
defaultColor: 0xb0a090,
|
|
622
|
-
|
|
891
|
+
shape: 'plane',
|
|
892
|
+
zPriority: Z_FMSIM.PORT,
|
|
623
893
|
}
|
|
624
894
|
registerRealObjectFactory('Port', (component: Component) => new MCSRealObject3D(component, PORT_CONFIG))
|
|
625
895
|
|
|
@@ -628,140 +898,295 @@ register('Shelf', {
|
|
|
628
898
|
metalness: 0.3,
|
|
629
899
|
roughness: 0.7,
|
|
630
900
|
defaultColor: 0xaabbcc,
|
|
901
|
+
zPriority: Z_FMSIM.SHELF,
|
|
631
902
|
})
|
|
632
903
|
|
|
633
904
|
// Carrier: CARRIERSTATUS에 따른 3D 애니메이션 지원
|
|
634
905
|
const CARRIER_CONFIG: MCS3DConfig = {
|
|
635
906
|
...DEFAULTS,
|
|
636
907
|
depthRatio: 0.5,
|
|
637
|
-
metalness: 0
|
|
638
|
-
roughness:
|
|
639
|
-
defaultColor:
|
|
908
|
+
metalness: 0,
|
|
909
|
+
roughness: 1,
|
|
910
|
+
defaultColor: 0xf0f0f0,
|
|
640
911
|
isVisible: (c: Component) => !!(c.state as any).id,
|
|
641
912
|
sizeRatio: 0.8,
|
|
642
913
|
texturedTop: true,
|
|
914
|
+
// Carrier 는 이동 entity — default OBJECTS(0). zPriority 미설정.
|
|
643
915
|
}
|
|
644
916
|
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
917
|
+
/**
|
|
918
|
+
* 적층 시 다음 carrier 의 z-position 이 *_이전 carrier 의 effectiveDepth × N*
|
|
919
|
+
* 만큼 위에 놓인다. N=1 이면 carrier 들이 맞붙어 적층 (기존), N<1 이면 부분
|
|
920
|
+
* overlap (3D box 들이 겹쳐서 적층). 사용자 의도 — carrier 두께는 두꺼워도
|
|
921
|
+
* 적층 시 *_compact 하게 보이도록* — N 을 작게.
|
|
922
|
+
*/
|
|
923
|
+
const CARRIER_STACK_STEP_RATIO = 0.9
|
|
648
924
|
|
|
925
|
+
class CarrierRealObject3D extends MCSRealObject3D {
|
|
649
926
|
constructor(component: Component) {
|
|
650
927
|
super(component, CARRIER_CONFIG)
|
|
651
928
|
}
|
|
652
929
|
|
|
653
|
-
build() {
|
|
654
|
-
super.build()
|
|
655
|
-
this.startCarrierAnimation()
|
|
656
|
-
}
|
|
657
|
-
|
|
658
930
|
/**
|
|
659
|
-
*
|
|
660
|
-
*
|
|
931
|
+
* cz override — *carrier 자기 결정* 의 적층 위치.
|
|
932
|
+
* parent.components 안 자기 index 까지의 형제 carrier 들의 effectiveDepth
|
|
933
|
+
* 누적합을 base cz 위에 더한다. holder 측에 별도 layout 책임을 두지 않고,
|
|
934
|
+
* carrier 가 자기 stack 위치를 알아서 결정.
|
|
661
935
|
*/
|
|
662
|
-
|
|
663
|
-
|
|
936
|
+
override get cz(): number {
|
|
937
|
+
const base = super.cz
|
|
938
|
+
const parent = this.component.parent as any
|
|
939
|
+
if (!parent?.components) return base
|
|
940
|
+
const siblings = parent.components as any[]
|
|
941
|
+
const idx = siblings.indexOf(this.component)
|
|
942
|
+
if (idx <= 0) return base
|
|
943
|
+
let offset = 0
|
|
944
|
+
for (let i = 0; i < idx; i++) {
|
|
945
|
+
const ro = siblings[i]?._realObject
|
|
946
|
+
const eff = Number(ro?.effectiveDepth)
|
|
947
|
+
const d = Number.isFinite(eff) && eff > 0 ? eff : 1
|
|
948
|
+
offset += d * CARRIER_STACK_STEP_RATIO
|
|
949
|
+
}
|
|
950
|
+
// 적층 방향 — placement (= 부모 holder 타입) 에 따라 결정.
|
|
951
|
+
// OHT 처럼 그리퍼 아래에 매달리는 holder 는 carrier 가 *_아래로_* 누적,
|
|
952
|
+
// 그 외 (Port / Shelf / Shuttle / Vehicle) 는 위로 누적.
|
|
953
|
+
// MCSRealObject3D.syncZPosOffset 의 OHT 분기 (-effectiveDepth + halfD) 와
|
|
954
|
+
// 같은 방향성 일관 적용.
|
|
955
|
+
const parentType = parent?.state?.type
|
|
956
|
+
const dir = parentType === 'OHT' ? -1 : 1
|
|
957
|
+
return base + dir * offset
|
|
958
|
+
}
|
|
959
|
+
}
|
|
664
960
|
|
|
665
|
-
|
|
666
|
-
if (!comp.app?.isViewMode) return
|
|
961
|
+
registerRealObjectFactory('Carrier', (component: Component) => new CarrierRealObject3D(component))
|
|
667
962
|
|
|
668
|
-
|
|
669
|
-
|
|
963
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
964
|
+
// Gauge (용량 게이지) — 얇은 바
|
|
965
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
670
966
|
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
967
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
968
|
+
// Gauge (용량 게이지) — 2D 렌더링을 Plane 텍스처로 3D 표시
|
|
969
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
674
970
|
|
|
675
|
-
|
|
676
|
-
|
|
971
|
+
// Gauge 컴포넌트는 3D에서 표시하지 않음 (부모 STOCKER의 3D 모델에서 직접 게이지 렌더링)
|
|
972
|
+
// null 반환으로 3D 오브젝트 생성을 억제하고, default factory fallback도 방지
|
|
973
|
+
registerRealObjectFactory('StockerCapacityBar', () => undefined as any)
|
|
974
|
+
registerRealObjectFactory('MCSGaugeCapacityBar', () => undefined as any)
|
|
975
|
+
registerRealObjectFactory('ZoneCapacityBar', () => undefined as any)
|
|
677
976
|
|
|
678
|
-
|
|
977
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
978
|
+
// PortFlow — 2D render를 Plane 텍스처로 3D 표시
|
|
979
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
679
980
|
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
981
|
+
class PortFlowRealObject3D extends RealObjectGroup {
|
|
982
|
+
private _mesh?: THREE.Mesh
|
|
983
|
+
private _texture?: THREE.CanvasTexture
|
|
683
984
|
|
|
684
|
-
|
|
685
|
-
|
|
985
|
+
get zPriority(): number { return Z_FMSIM.PORT_FLOW }
|
|
986
|
+
get effectiveDepth(): number { return 0 }
|
|
987
|
+
protected get syncZPosOffset(): number {
|
|
988
|
+
// 부모 Port/Conveyor의 zPos와 동일하게
|
|
989
|
+
const parent = (this.component as any).parentObject
|
|
990
|
+
if (parent?.state?.zPos != null) return parent.state.zPos
|
|
991
|
+
return TRANSPORT_SURFACE_HEIGHT
|
|
992
|
+
}
|
|
686
993
|
|
|
687
|
-
|
|
688
|
-
|
|
994
|
+
build() {
|
|
995
|
+
super.build()
|
|
689
996
|
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
case '
|
|
708
|
-
|
|
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
|
-
}
|
|
997
|
+
const { width = 10, height = 10 } = this.component.state
|
|
998
|
+
const w = Math.abs(width)
|
|
999
|
+
const h = Math.abs(height)
|
|
1000
|
+
if (!w || !h) return
|
|
1001
|
+
|
|
1002
|
+
// 2D render를 캔버스에 캡처
|
|
1003
|
+
// 화살표 날개 + 텍스트 영역이 bounds 밖으로 나가므로 전체 영역 계산
|
|
1004
|
+
const { left, top } = this.component.bounds
|
|
1005
|
+
const { fontSize = 24, labelPosition = 'top' } = this.component.state
|
|
1006
|
+
const arrowPad = Math.min(w, h) * 0.4
|
|
1007
|
+
|
|
1008
|
+
// 텍스트 영역 확장
|
|
1009
|
+
let padLeft = arrowPad, padRight = arrowPad, padTop = arrowPad, padBottom = arrowPad
|
|
1010
|
+
if ((this.component as any).hasTextProperty) {
|
|
1011
|
+
switch (labelPosition) {
|
|
1012
|
+
case 'top': padTop = Math.max(padTop, fontSize + 4); break
|
|
1013
|
+
case 'bottom': padBottom = Math.max(padBottom, fontSize + 4); break
|
|
1014
|
+
case 'left': padLeft = Math.max(padLeft, fontSize * 4 + 4); break
|
|
1015
|
+
case 'right': padRight = Math.max(padRight, fontSize * 4 + 4); break
|
|
717
1016
|
}
|
|
718
|
-
|
|
719
|
-
this._animRaf = requestAnimationFrame(animate)
|
|
720
1017
|
}
|
|
721
1018
|
|
|
722
|
-
|
|
723
|
-
|
|
1019
|
+
const totalW = w + padLeft + padRight
|
|
1020
|
+
const totalH = h + padTop + padBottom
|
|
1021
|
+
const scale = 2
|
|
1022
|
+
const canvas = document.createElement('canvas')
|
|
1023
|
+
canvas.width = totalW * scale
|
|
1024
|
+
canvas.height = totalH * scale
|
|
1025
|
+
const ctx = canvas.getContext('2d')!
|
|
724
1026
|
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
1027
|
+
ctx.save()
|
|
1028
|
+
ctx.scale(scale, scale)
|
|
1029
|
+
ctx.translate(-left + padLeft, -top + padTop)
|
|
1030
|
+
try {
|
|
1031
|
+
// 화살표 렌더
|
|
1032
|
+
this.component.render(ctx)
|
|
1033
|
+
ctx.fillStyle = this.component.state.strokeStyle || '#666666'
|
|
1034
|
+
ctx.fill()
|
|
1035
|
+
// 텍스트 렌더
|
|
1036
|
+
if ((this.component as any).hasTextProperty && (this.component as any).drawText) {
|
|
1037
|
+
;(this.component as any).drawText(ctx)
|
|
1038
|
+
}
|
|
1039
|
+
} catch { /* render 실패 시 빈 텍스처 */ }
|
|
1040
|
+
ctx.restore()
|
|
731
1041
|
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
1042
|
+
this._texture = new THREE.CanvasTexture(canvas)
|
|
1043
|
+
this._texture.colorSpace = THREE.SRGBColorSpace
|
|
1044
|
+
|
|
1045
|
+
const geo = new THREE.PlaneGeometry(totalW, totalH)
|
|
1046
|
+
geo.rotateX(-Math.PI / 2)
|
|
1047
|
+
|
|
1048
|
+
const mat = new THREE.MeshBasicMaterial({
|
|
1049
|
+
map: this._texture,
|
|
1050
|
+
alphaTest: 0.1,
|
|
1051
|
+
side: THREE.DoubleSide,
|
|
1052
|
+
})
|
|
1053
|
+
|
|
1054
|
+
this._mesh = new THREE.Mesh(geo, mat)
|
|
1055
|
+
// 비대칭 패딩 보정: 플레인 센터를 컴포넌트 bounds 센터에 맞춤
|
|
1056
|
+
this._mesh.position.x = (padRight - padLeft) / 2
|
|
1057
|
+
this._mesh.position.y = 1
|
|
1058
|
+
this._mesh.position.z = (padBottom - padTop) / 2
|
|
1059
|
+
this.object3d.add(this._mesh)
|
|
740
1060
|
}
|
|
741
1061
|
|
|
742
1062
|
update() {
|
|
743
|
-
this.
|
|
744
|
-
|
|
1063
|
+
if (this._mesh) {
|
|
1064
|
+
this.object3d.remove(this._mesh)
|
|
1065
|
+
this._mesh.geometry.dispose()
|
|
1066
|
+
;(this._mesh.material as THREE.Material).dispose()
|
|
1067
|
+
this._texture?.dispose()
|
|
1068
|
+
this._mesh = undefined
|
|
1069
|
+
this._texture = undefined
|
|
1070
|
+
}
|
|
1071
|
+
this.build()
|
|
1072
|
+
this.updateTransform()
|
|
1073
|
+
this.updateHidden()
|
|
745
1074
|
}
|
|
746
1075
|
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
1076
|
+
onchange(after: Record<string, unknown>, before: Record<string, unknown>) {
|
|
1077
|
+
if ('width' in after || 'height' in after || 'orientation' in after ||
|
|
1078
|
+
'direction' in after || 'strokeStyle' in after || 'INOUTTYPE' in after) {
|
|
1079
|
+
this.update()
|
|
1080
|
+
return
|
|
1081
|
+
}
|
|
1082
|
+
super.onchange(after, before)
|
|
750
1083
|
}
|
|
1084
|
+
|
|
1085
|
+
updateDimension() {}
|
|
1086
|
+
updateAlpha() {}
|
|
751
1087
|
}
|
|
752
1088
|
|
|
753
|
-
registerRealObjectFactory('
|
|
1089
|
+
registerRealObjectFactory('PortFlow', (component: Component) => new PortFlowRealObject3D(component))
|
|
754
1090
|
|
|
755
1091
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
756
|
-
//
|
|
1092
|
+
// Node — 2D render를 바닥 Plane 텍스처로 3D 표시
|
|
757
1093
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
758
1094
|
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
1095
|
+
class NodeRealObject3D extends RealObjectGroup {
|
|
1096
|
+
private _mesh?: THREE.Mesh
|
|
1097
|
+
private _texture?: THREE.CanvasTexture
|
|
1098
|
+
|
|
1099
|
+
get zPriority(): number { return Z_FMSIM.NODE }
|
|
1100
|
+
get effectiveDepth(): number { return 0 }
|
|
1101
|
+
protected get syncZPosOffset(): number { return 0 }
|
|
1102
|
+
|
|
1103
|
+
build() {
|
|
1104
|
+
super.build()
|
|
1105
|
+
|
|
1106
|
+
const { cx, cy } = this.component.state
|
|
1107
|
+
const radius = 8
|
|
1108
|
+
const size = radius * 2
|
|
1109
|
+
const scale = 2
|
|
1110
|
+
const pad = 2
|
|
1111
|
+
|
|
1112
|
+
const canvas = document.createElement('canvas')
|
|
1113
|
+
canvas.width = (size + pad * 2) * scale
|
|
1114
|
+
canvas.height = (size + pad * 2) * scale
|
|
1115
|
+
const ctx = canvas.getContext('2d')!
|
|
1116
|
+
|
|
1117
|
+
ctx.save()
|
|
1118
|
+
ctx.scale(scale, scale)
|
|
1119
|
+
ctx.translate(-cx + radius + pad, -cy + radius + pad)
|
|
1120
|
+
try {
|
|
1121
|
+
this.component.render(ctx)
|
|
1122
|
+
const { fillStyle = '#888888' } = this.component.state
|
|
1123
|
+
ctx.fillStyle = typeof fillStyle === 'string' ? fillStyle : '#888888'
|
|
1124
|
+
ctx.fill()
|
|
1125
|
+
const { strokeStyle } = this.component.state
|
|
1126
|
+
if (strokeStyle && typeof strokeStyle === 'string') {
|
|
1127
|
+
ctx.strokeStyle = strokeStyle
|
|
1128
|
+
ctx.lineWidth = 1
|
|
1129
|
+
ctx.stroke()
|
|
1130
|
+
}
|
|
1131
|
+
} catch { /* render 실패 시 빈 텍스처 */ }
|
|
1132
|
+
ctx.restore()
|
|
1133
|
+
|
|
1134
|
+
this._texture = new THREE.CanvasTexture(canvas)
|
|
1135
|
+
this._texture.colorSpace = THREE.SRGBColorSpace
|
|
1136
|
+
|
|
1137
|
+
const geo = new THREE.PlaneGeometry(size + pad * 2, size + pad * 2)
|
|
1138
|
+
geo.rotateX(-Math.PI / 2)
|
|
1139
|
+
|
|
1140
|
+
const mat = new THREE.MeshBasicMaterial({
|
|
1141
|
+
map: this._texture,
|
|
1142
|
+
alphaTest: 0.1,
|
|
1143
|
+
side: THREE.DoubleSide,
|
|
1144
|
+
})
|
|
1145
|
+
|
|
1146
|
+
this._mesh = new THREE.Mesh(geo, mat)
|
|
1147
|
+
this._mesh.position.y = 1
|
|
1148
|
+
this.object3d.add(this._mesh)
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
update() {
|
|
1152
|
+
if (this._mesh) {
|
|
1153
|
+
this.object3d.remove(this._mesh)
|
|
1154
|
+
this._mesh.geometry.dispose()
|
|
1155
|
+
;(this._mesh.material as THREE.Material).dispose()
|
|
1156
|
+
this._texture?.dispose()
|
|
1157
|
+
this._mesh = undefined
|
|
1158
|
+
this._texture = undefined
|
|
1159
|
+
}
|
|
1160
|
+
this.build()
|
|
1161
|
+
this.updateTransform()
|
|
1162
|
+
this.updateHidden()
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
onchange(after: Record<string, unknown>, before: Record<string, unknown>) {
|
|
1166
|
+
if ('fillStyle' in after || 'strokeStyle' in after) {
|
|
1167
|
+
this.update()
|
|
1168
|
+
return
|
|
1169
|
+
}
|
|
1170
|
+
super.onchange(after, before)
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
updateDimension() {}
|
|
1174
|
+
updateAlpha() {}
|
|
1175
|
+
}
|
|
762
1176
|
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
1177
|
+
registerRealObjectFactory('Node', (component: Component) => new NodeRealObject3D(component))
|
|
1178
|
+
|
|
1179
|
+
// Scanner — 2D Triangle 실루엣과 정합되는 삼각형 prism.
|
|
1180
|
+
// 이송 표면 높이의 0.2배(=6) 얇은 기둥 — 원래 값 유지, shape 만 'box' → 'triangle'.
|
|
1181
|
+
const SCANNER_CONFIG: MCS3DConfig = {
|
|
1182
|
+
...DEFAULTS,
|
|
1183
|
+
defaultDepth: TRANSPORT_SURFACE_HEIGHT * 0.2,
|
|
1184
|
+
defaultZPosFromParent: true,
|
|
1185
|
+
opacity: 1,
|
|
1186
|
+
metalness: 0.3,
|
|
1187
|
+
roughness: 0.7,
|
|
1188
|
+
defaultColor: 0x888888,
|
|
1189
|
+
shape: 'triangle',
|
|
1190
|
+
zPriority: Z_FMSIM.SCANNER,
|
|
1191
|
+
}
|
|
1192
|
+
registerRealObjectFactory('Scanner', (component: Component) => new MCSRealObject3D(component, SCANNER_CONFIG))
|