@fmsim/machine 2.0.0-beta.10 → 2.0.0-beta.101
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 +17 -36
- package/dist/factories/agv-3d.js.map +1 -1
- package/dist/factories/conveyor-3d.js +179 -100
- package/dist/factories/conveyor-3d.js.map +1 -1
- package/dist/factories/crane-3d.js +56 -29
- package/dist/factories/crane-3d.js.map +1 -1
- package/dist/factories/crane-rail-3d.js +14 -18
- package/dist/factories/crane-rail-3d.js.map +1 -1
- package/dist/factories/machine-3d.js +533 -197
- package/dist/factories/machine-3d.js.map +1 -1
- package/dist/factories/oht-3d.js +29 -47
- package/dist/factories/oht-3d.js.map +1 -1
- package/dist/factories/shuttle-3d.js +13 -32
- 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 +20 -0
- package/dist/mcs-machine.js.map +1 -1
- package/dist/mcs-unit.js +17 -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 +19 -36
- package/src/factories/conveyor-3d.ts +183 -103
- package/src/factories/crane-3d.ts +62 -29
- package/src/factories/crane-rail-3d.ts +17 -20
- package/src/factories/machine-3d.ts +631 -205
- package/src/factories/oht-3d.ts +30 -46
- package/src/factories/shuttle-3d.ts +14 -32
- 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 +23 -0
- package/src/mcs-unit.ts +21 -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
|
|
12
|
+
import * as BufferGeometryUtils from 'three/examples/jsm/utils/BufferGeometryUtils.js'
|
|
13
|
+
import { RealObjectGroup, registerRealObjectFactory, disposeObject3D, MaterialBank } 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,20 +303,39 @@ 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
|
-
|
|
233
|
-
|
|
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
|
+
// Standard body 는 MaterialBank 공유(색 변형) — 인스턴스 dedup + faint 안전(코어가 공유 base 의
|
|
316
|
+
// 변형만 스왑, 원본 base 불변). plane 은 unlit MeshBasicMaterial 이라 bank(Standard 전용) 비대상 → raw.
|
|
317
|
+
const material: THREE.Material = isPlane
|
|
318
|
+
? new THREE.MeshBasicMaterial({
|
|
319
|
+
color,
|
|
320
|
+
opacity,
|
|
321
|
+
transparent: opacity < 1,
|
|
322
|
+
side: THREE.DoubleSide,
|
|
323
|
+
})
|
|
324
|
+
: MaterialBank.material(
|
|
325
|
+
'mcs|body',
|
|
326
|
+
{
|
|
327
|
+
metalness,
|
|
328
|
+
roughness,
|
|
329
|
+
opacity,
|
|
330
|
+
side: opacity < 1 ? THREE.DoubleSide : THREE.FrontSide,
|
|
331
|
+
...(this._config.emissiveIntensity
|
|
332
|
+
? { emissive: color, emissiveIntensity: this._config.emissiveIntensity }
|
|
333
|
+
: {}),
|
|
334
|
+
},
|
|
335
|
+
color,
|
|
336
|
+
)
|
|
337
|
+
// z-fighting 방지: 바닥면(floor grid)보다 앞. bank body 는 공유 base 라 polygonOffset 은 모든 mcs
|
|
338
|
+
// body 공통 상수 — 반환 material 에 세팅(같은 값이라 멱등, 인스턴스별 발산 없음).
|
|
234
339
|
material.polygonOffset = true
|
|
235
340
|
material.polygonOffsetFactor = -1
|
|
236
341
|
material.polygonOffsetUnit = -1
|
|
@@ -254,11 +359,19 @@ class MCSRealObject3D extends RealObjectGroup {
|
|
|
254
359
|
}
|
|
255
360
|
|
|
256
361
|
this._ownMesh = new THREE.Mesh(geometry, meshMaterial)
|
|
257
|
-
//
|
|
258
|
-
|
|
259
|
-
|
|
362
|
+
// ownGroup 기준(bottom-up): mesh 바닥을 y=0에 맞추고 ownGroup이 -d/2로 shift해
|
|
363
|
+
// center-origin object3d에 정렬됨.
|
|
364
|
+
// box/hexagon (center-local geometry): y=rawDepth/2 (바닥을 0에 맞춤)
|
|
365
|
+
// plane: y=1 (바닥 살짝 띄움)
|
|
366
|
+
this._ownMesh.position.y = isPlane ? 1 : rawDepth / 2
|
|
367
|
+
this._ownMesh.castShadow = !isPlane
|
|
260
368
|
this._ownMesh.receiveShadow = true
|
|
261
|
-
this.
|
|
369
|
+
this.components3D.add(this._ownMesh)
|
|
370
|
+
|
|
371
|
+
// mesh 는 super.build() *이후* 생성되므로, 코어의 build-시점 faint 적용이 이 mesh 에 못 닿는다.
|
|
372
|
+
// mesh 생성 직후 명시 적용 — 코어 applyMaterialOpacity 가 content-alpha × faint 를 일괄 처리.
|
|
373
|
+
// view 모드에선 factor=1(=state.alpha 없음) → canonical(config.opacity) 그대로, 고객 화면 불변.
|
|
374
|
+
this.updateAlpha()
|
|
262
375
|
}
|
|
263
376
|
|
|
264
377
|
// ── Change handling ──
|
|
@@ -353,7 +466,7 @@ class MCSRealObject3D extends RealObjectGroup {
|
|
|
353
466
|
update() {
|
|
354
467
|
// 자신의 mesh만 제거 (자식 컴포넌트 3D 오브젝트 보존)
|
|
355
468
|
if (this._ownMesh) {
|
|
356
|
-
this.
|
|
469
|
+
this.components3D.remove(this._ownMesh)
|
|
357
470
|
disposeObject3D(this._ownMesh)
|
|
358
471
|
this._ownMesh = undefined
|
|
359
472
|
}
|
|
@@ -367,22 +480,24 @@ class MCSRealObject3D extends RealObjectGroup {
|
|
|
367
480
|
// RealObjectGroup 기본 동작 비활성화
|
|
368
481
|
updateDimension() {}
|
|
369
482
|
|
|
370
|
-
updateAlpha()
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
483
|
+
// updateAlpha override 제거 — 코어 RealObject.updateAlpha(=applyMaterialOpacity)로 수렴.
|
|
484
|
+
// 코어가 canonical(=build 시 config.opacity 로 만든 material) 위에 factor = state.alpha × faint 를
|
|
485
|
+
// 공유 변형 스왑으로 일괄 적용한다(원본 base 불변). 기존 in-place 변이는 (1) faint 미적용,
|
|
486
|
+
// (2) 코어 applyFocusDim 과 이중 적용 소지가 있어 방언이었다. config.opacity 는 canonical 로 보존.
|
|
487
|
+
}
|
|
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
|
|
385
499
|
}
|
|
500
|
+
return 0
|
|
386
501
|
}
|
|
387
502
|
|
|
388
503
|
// ── 팩토리 등록 헬퍼 ─────────────────────────────────────
|
|
@@ -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,113 @@ 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
|
+
// 공유 base(MaterialBank) — OHTLine rail 은 정적 색이라 전 인스턴스 dedup. faint 는 코어가
|
|
577
|
+
// 공유 base 의 변형을 스왑(원본 불변). dispose 는 bank-safe(update 에서 material.dispose 안 함).
|
|
578
|
+
const mat = MaterialBank.material('oht-line|rail', { metalness: 0.5, roughness: 0.4 }, OHT_LINE_RAIL_COLOR)
|
|
579
|
+
|
|
580
|
+
const geoms: THREE.BufferGeometry[] = []
|
|
581
|
+
|
|
582
|
+
// 상부 플랜지
|
|
583
|
+
const topFlange = isVertical
|
|
584
|
+
? new THREE.BoxGeometry(flangeW, flangeH, railLen)
|
|
585
|
+
: new THREE.BoxGeometry(railLen, flangeH, flangeW)
|
|
586
|
+
topFlange.translate(0, -flangeH / 2, 0)
|
|
587
|
+
geoms.push(topFlange)
|
|
588
|
+
|
|
589
|
+
// 웹 (수직판)
|
|
590
|
+
const web = isVertical
|
|
591
|
+
? new THREE.BoxGeometry(webW, webH, railLen)
|
|
592
|
+
: new THREE.BoxGeometry(railLen, webH, webW)
|
|
593
|
+
web.translate(0, -flangeH - webH / 2, 0)
|
|
594
|
+
geoms.push(web)
|
|
595
|
+
|
|
596
|
+
// 하부 플랜지
|
|
597
|
+
const botFlange = isVertical
|
|
598
|
+
? new THREE.BoxGeometry(flangeW, flangeH, railLen)
|
|
599
|
+
: new THREE.BoxGeometry(railLen, flangeH, flangeW)
|
|
600
|
+
botFlange.translate(0, -flangeH - webH - flangeH / 2, 0)
|
|
601
|
+
geoms.push(botFlange)
|
|
602
|
+
|
|
603
|
+
this._mesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(geoms), mat)
|
|
604
|
+
this._mesh.castShadow = true
|
|
605
|
+
this.object3d.add(this._mesh)
|
|
606
|
+
|
|
607
|
+
// mesh 가 super.build() 이후 생성 → 코어 faint 를 명시 적용(편집 흐림이 mesh 에 닿게).
|
|
608
|
+
this.updateAlpha()
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
update() {
|
|
612
|
+
if (this._mesh) {
|
|
613
|
+
this.object3d.remove(this._mesh)
|
|
614
|
+
this._mesh.geometry.dispose()
|
|
615
|
+
// material 은 MaterialBank 공유(앱 수명) — 여기서 dispose 하면 다른 인스턴스가 깨진다.
|
|
616
|
+
this._mesh = undefined
|
|
617
|
+
}
|
|
618
|
+
this.build()
|
|
619
|
+
this.updateTransform()
|
|
620
|
+
this.updateHidden()
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
onchange(after: Record<string, unknown>, before: Record<string, unknown>) {
|
|
624
|
+
if ('width' in after || 'height' in after || 'depth' in after ||
|
|
625
|
+
'fillStyle' in after || 'strokeStyle' in after) {
|
|
626
|
+
this.update()
|
|
627
|
+
return
|
|
628
|
+
}
|
|
629
|
+
super.onchange(after, before)
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// updateAlpha override 제거 — 코어 RealObject.updateAlpha(=applyMaterialOpacity)로 수렴(faint).
|
|
633
|
+
updateDimension() {}
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
registerRealObjectFactory('OHTLine', (component: Component) => new OHTLineRealObject3D(component))
|
|
429
637
|
|
|
430
638
|
// STOCKER: 벽면에 용량 게이지 바를 포함하는 커스텀 3D 오브젝트
|
|
431
639
|
const STOCKER_CONFIG: MCS3DConfig = {
|
|
@@ -435,6 +643,7 @@ const STOCKER_CONFIG: MCS3DConfig = {
|
|
|
435
643
|
metalness: 0.3,
|
|
436
644
|
roughness: 0.6,
|
|
437
645
|
defaultColor: 0x8899bb,
|
|
646
|
+
zPriority: Z_FMSIM.STOCKER,
|
|
438
647
|
}
|
|
439
648
|
|
|
440
649
|
class StockerRealObject3D extends MCSRealObject3D {
|
|
@@ -452,7 +661,7 @@ class StockerRealObject3D extends MCSRealObject3D {
|
|
|
452
661
|
private buildGauge() {
|
|
453
662
|
const comp = this.component as any
|
|
454
663
|
const data = comp.data || {}
|
|
455
|
-
const gaugeMode = comp.root?.
|
|
664
|
+
const gaugeMode = comp.root?.stockerGaugeMode || 'capacity'
|
|
456
665
|
|
|
457
666
|
const gauge = this.resolveGauge(data, gaugeMode)
|
|
458
667
|
|
|
@@ -466,12 +675,20 @@ class StockerRealObject3D extends MCSRealObject3D {
|
|
|
466
675
|
const depth = this.effectiveDepth
|
|
467
676
|
const isWidthLonger = w >= h
|
|
468
677
|
|
|
469
|
-
// 게이지
|
|
470
|
-
const
|
|
471
|
-
const
|
|
678
|
+
// 게이지 최대 크기 (스토커가 충분히 크면 이 크기 그대로 사용).
|
|
679
|
+
const MAX_BAR_W = 130
|
|
680
|
+
const MAX_BAR_H = 26
|
|
681
|
+
|
|
682
|
+
// 스토커 지붕 footprint 에 맞춰 비례 축소 (확대는 안 함 — 현재 크기가 상한).
|
|
683
|
+
const fit = fitGaugeScale(w, h, MAX_BAR_W, MAX_BAR_H)
|
|
684
|
+
|
|
685
|
+
// PlaneGeometry 의 월드 크기는 clamp, 텍스처 캔버스 해상도는 최대 크기 기준 고정
|
|
686
|
+
// (축소되어도 선명도 유지).
|
|
687
|
+
const barW = MAX_BAR_W * fit
|
|
688
|
+
const barH = MAX_BAR_H * fit
|
|
472
689
|
const scale = 4
|
|
473
|
-
const canvasW =
|
|
474
|
-
const canvasH =
|
|
690
|
+
const canvasW = MAX_BAR_W * scale
|
|
691
|
+
const canvasH = MAX_BAR_H * scale
|
|
475
692
|
const canvas = document.createElement('canvas')
|
|
476
693
|
canvas.width = canvasW
|
|
477
694
|
canvas.height = canvasH
|
|
@@ -486,24 +703,32 @@ class StockerRealObject3D extends MCSRealObject3D {
|
|
|
486
703
|
ctx.lineWidth = 12
|
|
487
704
|
ctx.strokeRect(6, 6, canvasW - 12, canvasH - 12)
|
|
488
705
|
|
|
489
|
-
// 세그먼트 채움 (왼→오)
|
|
706
|
+
// 세그먼트 채움 (왼→오) + carrier/abnormal 은 세그먼트별 숫자(%) 표기 (2D 와 동일)
|
|
490
707
|
const innerW = canvasW - pad * 2
|
|
491
708
|
const innerH = canvasH - pad * 2
|
|
709
|
+
// 컴포넌트 폰트 설정(fontFamily/bold/italic) 을 그대로, 크기만 텍스처 scale 배 (2D 와 상대 크기 일치).
|
|
710
|
+
const fontSize = (Number((this.component as any).state?.fontSize) || 15) * scale
|
|
711
|
+
const font = this.gaugeFont(fontSize)
|
|
492
712
|
let x = pad
|
|
493
713
|
for (const seg of gauge.segments) {
|
|
494
714
|
const segW = Math.max(innerW * seg.ratio, seg.ratio > 0 ? 1 : 0)
|
|
495
715
|
if (segW <= 0) continue
|
|
496
716
|
ctx.fillStyle = seg.color
|
|
497
717
|
ctx.fillRect(x, pad, segW, innerH)
|
|
718
|
+
if (gauge.perSegment) {
|
|
719
|
+
this.drawGaugeSegmentNumber(ctx, seg.ratio, x, pad, segW, innerH, font, fontSize)
|
|
720
|
+
}
|
|
498
721
|
x += segW
|
|
499
722
|
}
|
|
500
723
|
|
|
501
|
-
//
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
724
|
+
// capacity: 전체 중앙에 usage% 한 개 (2D 와 동일). carrier/abnormal 은 위 세그먼트별 숫자 사용.
|
|
725
|
+
if (gauge.label) {
|
|
726
|
+
ctx.font = font
|
|
727
|
+
ctx.textAlign = 'center'
|
|
728
|
+
ctx.textBaseline = 'middle'
|
|
729
|
+
ctx.fillStyle = '#333333'
|
|
730
|
+
ctx.fillText(gauge.label, canvasW / 2, canvasH / 2)
|
|
731
|
+
}
|
|
507
732
|
|
|
508
733
|
// 텍스처 → Plane
|
|
509
734
|
const texture = new THREE.CanvasTexture(canvas)
|
|
@@ -523,62 +748,107 @@ class StockerRealObject3D extends MCSRealObject3D {
|
|
|
523
748
|
mat.polygonOffsetUnit = -2
|
|
524
749
|
|
|
525
750
|
this.gaugeMesh = new THREE.Mesh(geo, mat)
|
|
751
|
+
// ownGroup 기준(bottom-up): 볼륨 상단 = +depth. 그 위로 0.5 띄움.
|
|
526
752
|
this.gaugeMesh.position.set(0, depth + 0.5, 0)
|
|
527
|
-
this.
|
|
753
|
+
this.components3D.add(this.gaugeMesh)
|
|
528
754
|
}
|
|
529
755
|
|
|
530
|
-
private resolveGauge(
|
|
756
|
+
private resolveGauge(
|
|
757
|
+
data: any,
|
|
758
|
+
mode: string
|
|
759
|
+
): { segments: { ratio: number; color: string }[]; borderColor: string; label: string | null; perSegment: boolean } {
|
|
531
760
|
switch (mode) {
|
|
532
761
|
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
|
-
}
|
|
762
|
+
// 세그먼트 비율/색상은 2D 와 공유되는 단일 소스에서 계산 (필드/색상 drift 방지).
|
|
763
|
+
// 색상은 carrier 컴포넌트와 동일한 legend(테마)에서 취득.
|
|
764
|
+
const colors = carrierColorsFromLegend(resolveCarrierLegend(this.component))
|
|
765
|
+
const [f, e, ee] = carrierSegmentsFromData(data, colors)
|
|
766
|
+
const total = f.count + e.count + ee.count
|
|
767
|
+
// 접두사 없는 세그먼트별 숫자(%)로 표기 (capacity·2D 와 동일). label(중앙 한 개) 미사용.
|
|
768
|
+
return total > 0
|
|
769
|
+
? {
|
|
770
|
+
segments: [f, e, ee].map(s => ({ ratio: s.ratio, color: s.color })),
|
|
771
|
+
borderColor: colors.full,
|
|
772
|
+
label: null,
|
|
773
|
+
perSegment: true,
|
|
774
|
+
}
|
|
775
|
+
: { segments: [{ ratio: 1, color: '#dddddd' }], borderColor: colors.full, label: null, perSegment: false }
|
|
549
776
|
}
|
|
550
777
|
case 'abnormal': {
|
|
551
|
-
const
|
|
552
|
-
const
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
778
|
+
const [n, a] = abnormalSegmentsFromData(data)
|
|
779
|
+
const total = n.count + a.count
|
|
780
|
+
return total > 0
|
|
781
|
+
? {
|
|
782
|
+
segments: [n, a].map(s => ({ ratio: s.ratio, color: s.color })),
|
|
783
|
+
borderColor: a.count > 0 ? ABNORMAL_COLORS.abnormal : ABNORMAL_COLORS.normal,
|
|
784
|
+
label: null,
|
|
785
|
+
perSegment: true,
|
|
786
|
+
}
|
|
787
|
+
: {
|
|
788
|
+
segments: [{ ratio: 1, color: '#dddddd' }],
|
|
789
|
+
borderColor: ABNORMAL_COLORS.normal,
|
|
790
|
+
label: null,
|
|
791
|
+
perSegment: false,
|
|
792
|
+
}
|
|
565
793
|
}
|
|
566
|
-
default: { // capacity
|
|
794
|
+
default: { // capacity — 전체 중앙에 usage% 한 개
|
|
567
795
|
const cur = Number(data.CURRENTCAPACITY) || 0
|
|
568
796
|
const max = Number(data.MAXCAPACITY) || 100
|
|
569
797
|
const fullup = data.FULLUP === 'T'
|
|
570
798
|
const usage = Math.round((cur / max) * 100)
|
|
571
|
-
const legend = (this.component as any).root?.
|
|
572
|
-
const color = fullup ? 'red' : (
|
|
799
|
+
const legend = (this.component as any).root?.stockerCapacityLegendTheme || LEGEND_CAPACITY
|
|
800
|
+
const color = fullup ? 'red' : (getValueOnRanges(usage, legend) || '#44aa44')
|
|
573
801
|
return {
|
|
574
802
|
segments: [{ ratio: usage / 100, color }],
|
|
575
803
|
borderColor: color,
|
|
576
804
|
label: `${usage}%`,
|
|
805
|
+
perSegment: false,
|
|
577
806
|
}
|
|
578
807
|
}
|
|
579
808
|
}
|
|
580
809
|
}
|
|
581
810
|
|
|
811
|
+
/** 컴포넌트 폰트 설정(fontFamily/bold/italic)으로 CSS font 문자열 조합. size 는 텍스처 스케일 반영값. */
|
|
812
|
+
private gaugeFont(sizePx: number): string {
|
|
813
|
+
const st = (this.component as any).state || {}
|
|
814
|
+
const parts: string[] = []
|
|
815
|
+
if (st.bold) parts.push('bold')
|
|
816
|
+
if (st.italic) parts.push('italic')
|
|
817
|
+
parts.push(`${sizePx}px`)
|
|
818
|
+
parts.push(st.fontFamily || 'sans-serif')
|
|
819
|
+
return parts.join(' ')
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
/** 세그먼트 영역 중심에 숫자(%)만 표기. 좁으면 값% → 값 → 생략 (2D _drawSegmentLabel 과 동일 정책). */
|
|
823
|
+
private drawGaugeSegmentNumber(
|
|
824
|
+
ctx: CanvasRenderingContext2D,
|
|
825
|
+
ratio: number,
|
|
826
|
+
x: number,
|
|
827
|
+
y: number,
|
|
828
|
+
w: number,
|
|
829
|
+
h: number,
|
|
830
|
+
font: string,
|
|
831
|
+
fontSize: number
|
|
832
|
+
) {
|
|
833
|
+
ctx.font = font
|
|
834
|
+
ctx.textAlign = 'center'
|
|
835
|
+
ctx.textBaseline = 'middle'
|
|
836
|
+
ctx.fillStyle = '#333333'
|
|
837
|
+
const value = `${Math.round(ratio * 100)}`
|
|
838
|
+
const cx = x + w / 2
|
|
839
|
+
const cy = y + h / 2
|
|
840
|
+
if (fontSize <= h && ctx.measureText(`${value}%`).width <= w) {
|
|
841
|
+
ctx.fillText(`${value}%`, cx, cy)
|
|
842
|
+
} else if (fontSize <= h && ctx.measureText(value).width <= w) {
|
|
843
|
+
ctx.fillText(value, cx, cy)
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
update() {
|
|
848
|
+
this.clearGauge()
|
|
849
|
+
super.update()
|
|
850
|
+
}
|
|
851
|
+
|
|
582
852
|
onchange(after: Record<string, unknown>, before: Record<string, unknown>) {
|
|
583
853
|
if ('data' in after) {
|
|
584
854
|
this.clearGauge()
|
|
@@ -594,7 +864,7 @@ class StockerRealObject3D extends MCSRealObject3D {
|
|
|
594
864
|
|
|
595
865
|
private clearGauge() {
|
|
596
866
|
if (this.gaugeMesh) {
|
|
597
|
-
this.
|
|
867
|
+
this.components3D.remove(this.gaugeMesh)
|
|
598
868
|
this.gaugeMesh.geometry?.dispose()
|
|
599
869
|
const mat = this.gaugeMesh.material as THREE.MeshBasicMaterial
|
|
600
870
|
mat?.map?.dispose()
|
|
@@ -610,16 +880,17 @@ registerRealObjectFactory('STOCKER', (component: Component) => new StockerRealOb
|
|
|
610
880
|
// Unit (이동체 · 홀더) — 낮은 depth, 불투명
|
|
611
881
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
612
882
|
|
|
613
|
-
// Port:
|
|
883
|
+
// Port: 바닥판 (PlaneGeometry)
|
|
614
884
|
const PORT_CONFIG: MCS3DConfig = {
|
|
615
885
|
...DEFAULTS,
|
|
616
|
-
depthRatio: 0.
|
|
886
|
+
depthRatio: 0.01,
|
|
617
887
|
defaultZPosFromParent: true,
|
|
618
|
-
opacity: 0.
|
|
619
|
-
metalness: 0.
|
|
620
|
-
roughness: 0.
|
|
888
|
+
opacity: 0.1,
|
|
889
|
+
metalness: 0.3,
|
|
890
|
+
roughness: 0.8,
|
|
621
891
|
defaultColor: 0xb0a090,
|
|
622
|
-
|
|
892
|
+
shape: 'plane',
|
|
893
|
+
zPriority: Z_FMSIM.PORT,
|
|
623
894
|
}
|
|
624
895
|
registerRealObjectFactory('Port', (component: Component) => new MCSRealObject3D(component, PORT_CONFIG))
|
|
625
896
|
|
|
@@ -628,140 +899,295 @@ register('Shelf', {
|
|
|
628
899
|
metalness: 0.3,
|
|
629
900
|
roughness: 0.7,
|
|
630
901
|
defaultColor: 0xaabbcc,
|
|
902
|
+
zPriority: Z_FMSIM.SHELF,
|
|
631
903
|
})
|
|
632
904
|
|
|
633
905
|
// Carrier: CARRIERSTATUS에 따른 3D 애니메이션 지원
|
|
634
906
|
const CARRIER_CONFIG: MCS3DConfig = {
|
|
635
907
|
...DEFAULTS,
|
|
636
908
|
depthRatio: 0.5,
|
|
637
|
-
metalness: 0
|
|
638
|
-
roughness:
|
|
639
|
-
defaultColor:
|
|
909
|
+
metalness: 0,
|
|
910
|
+
roughness: 1,
|
|
911
|
+
defaultColor: 0xf0f0f0,
|
|
640
912
|
isVisible: (c: Component) => !!(c.state as any).id,
|
|
641
913
|
sizeRatio: 0.8,
|
|
642
914
|
texturedTop: true,
|
|
915
|
+
// Carrier 는 이동 entity — default OBJECTS(0). zPriority 미설정.
|
|
643
916
|
}
|
|
644
917
|
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
918
|
+
/**
|
|
919
|
+
* 적층 시 다음 carrier 의 z-position 이 *_이전 carrier 의 effectiveDepth × N*
|
|
920
|
+
* 만큼 위에 놓인다. N=1 이면 carrier 들이 맞붙어 적층 (기존), N<1 이면 부분
|
|
921
|
+
* overlap (3D box 들이 겹쳐서 적층). 사용자 의도 — carrier 두께는 두꺼워도
|
|
922
|
+
* 적층 시 *_compact 하게 보이도록* — N 을 작게.
|
|
923
|
+
*/
|
|
924
|
+
const CARRIER_STACK_STEP_RATIO = 0.9
|
|
648
925
|
|
|
926
|
+
class CarrierRealObject3D extends MCSRealObject3D {
|
|
649
927
|
constructor(component: Component) {
|
|
650
928
|
super(component, CARRIER_CONFIG)
|
|
651
929
|
}
|
|
652
930
|
|
|
653
|
-
build() {
|
|
654
|
-
super.build()
|
|
655
|
-
this.startCarrierAnimation()
|
|
656
|
-
}
|
|
657
|
-
|
|
658
931
|
/**
|
|
659
|
-
*
|
|
660
|
-
*
|
|
932
|
+
* cz override — *carrier 자기 결정* 의 적층 위치.
|
|
933
|
+
* parent.components 안 자기 index 까지의 형제 carrier 들의 effectiveDepth
|
|
934
|
+
* 누적합을 base cz 위에 더한다. holder 측에 별도 layout 책임을 두지 않고,
|
|
935
|
+
* carrier 가 자기 stack 위치를 알아서 결정.
|
|
661
936
|
*/
|
|
662
|
-
|
|
663
|
-
|
|
937
|
+
override get cz(): number {
|
|
938
|
+
const base = super.cz
|
|
939
|
+
const parent = this.component.parent as any
|
|
940
|
+
if (!parent?.components) return base
|
|
941
|
+
const siblings = parent.components as any[]
|
|
942
|
+
const idx = siblings.indexOf(this.component)
|
|
943
|
+
if (idx <= 0) return base
|
|
944
|
+
let offset = 0
|
|
945
|
+
for (let i = 0; i < idx; i++) {
|
|
946
|
+
const ro = siblings[i]?._realObject
|
|
947
|
+
const eff = Number(ro?.effectiveDepth)
|
|
948
|
+
const d = Number.isFinite(eff) && eff > 0 ? eff : 1
|
|
949
|
+
offset += d * CARRIER_STACK_STEP_RATIO
|
|
950
|
+
}
|
|
951
|
+
// 적층 방향 — placement (= 부모 holder 타입) 에 따라 결정.
|
|
952
|
+
// OHT 처럼 그리퍼 아래에 매달리는 holder 는 carrier 가 *_아래로_* 누적,
|
|
953
|
+
// 그 외 (Port / Shelf / Shuttle / Vehicle) 는 위로 누적.
|
|
954
|
+
// MCSRealObject3D.syncZPosOffset 의 OHT 분기 (-effectiveDepth + halfD) 와
|
|
955
|
+
// 같은 방향성 일관 적용.
|
|
956
|
+
const parentType = parent?.state?.type
|
|
957
|
+
const dir = parentType === 'OHT' ? -1 : 1
|
|
958
|
+
return base + dir * offset
|
|
959
|
+
}
|
|
960
|
+
}
|
|
664
961
|
|
|
665
|
-
|
|
666
|
-
if (!comp.app?.isViewMode) return
|
|
962
|
+
registerRealObjectFactory('Carrier', (component: Component) => new CarrierRealObject3D(component))
|
|
667
963
|
|
|
668
|
-
|
|
669
|
-
|
|
964
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
965
|
+
// Gauge (용량 게이지) — 얇은 바
|
|
966
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
670
967
|
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
968
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
969
|
+
// Gauge (용량 게이지) — 2D 렌더링을 Plane 텍스처로 3D 표시
|
|
970
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
674
971
|
|
|
675
|
-
|
|
676
|
-
|
|
972
|
+
// Gauge 컴포넌트는 3D에서 표시하지 않음 (부모 STOCKER의 3D 모델에서 직접 게이지 렌더링)
|
|
973
|
+
// null 반환으로 3D 오브젝트 생성을 억제하고, default factory fallback도 방지
|
|
974
|
+
registerRealObjectFactory('StockerCapacityBar', () => undefined as any)
|
|
975
|
+
registerRealObjectFactory('MCSGaugeCapacityBar', () => undefined as any)
|
|
976
|
+
registerRealObjectFactory('ZoneCapacityBar', () => undefined as any)
|
|
677
977
|
|
|
678
|
-
|
|
978
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
979
|
+
// PortFlow — 2D render를 Plane 텍스처로 3D 표시
|
|
980
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
679
981
|
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
982
|
+
class PortFlowRealObject3D extends RealObjectGroup {
|
|
983
|
+
private _mesh?: THREE.Mesh
|
|
984
|
+
private _texture?: THREE.CanvasTexture
|
|
683
985
|
|
|
684
|
-
|
|
685
|
-
|
|
986
|
+
get zPriority(): number { return Z_FMSIM.PORT_FLOW }
|
|
987
|
+
get effectiveDepth(): number { return 0 }
|
|
988
|
+
protected get syncZPosOffset(): number {
|
|
989
|
+
// 부모 Port/Conveyor의 zPos와 동일하게
|
|
990
|
+
const parent = (this.component as any).parentObject
|
|
991
|
+
if (parent?.state?.zPos != null) return parent.state.zPos
|
|
992
|
+
return TRANSPORT_SURFACE_HEIGHT
|
|
993
|
+
}
|
|
686
994
|
|
|
687
|
-
|
|
688
|
-
|
|
995
|
+
build() {
|
|
996
|
+
super.build()
|
|
689
997
|
|
|
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
|
-
}
|
|
998
|
+
const { width = 10, height = 10 } = this.component.state
|
|
999
|
+
const w = Math.abs(width)
|
|
1000
|
+
const h = Math.abs(height)
|
|
1001
|
+
if (!w || !h) return
|
|
1002
|
+
|
|
1003
|
+
// 2D render를 캔버스에 캡처
|
|
1004
|
+
// 화살표 날개 + 텍스트 영역이 bounds 밖으로 나가므로 전체 영역 계산
|
|
1005
|
+
const { left, top } = this.component.bounds
|
|
1006
|
+
const { fontSize = 24, labelPosition = 'top' } = this.component.state
|
|
1007
|
+
const arrowPad = Math.min(w, h) * 0.4
|
|
1008
|
+
|
|
1009
|
+
// 텍스트 영역 확장
|
|
1010
|
+
let padLeft = arrowPad, padRight = arrowPad, padTop = arrowPad, padBottom = arrowPad
|
|
1011
|
+
if ((this.component as any).hasTextProperty) {
|
|
1012
|
+
switch (labelPosition) {
|
|
1013
|
+
case 'top': padTop = Math.max(padTop, fontSize + 4); break
|
|
1014
|
+
case 'bottom': padBottom = Math.max(padBottom, fontSize + 4); break
|
|
1015
|
+
case 'left': padLeft = Math.max(padLeft, fontSize * 4 + 4); break
|
|
1016
|
+
case 'right': padRight = Math.max(padRight, fontSize * 4 + 4); break
|
|
717
1017
|
}
|
|
718
|
-
|
|
719
|
-
this._animRaf = requestAnimationFrame(animate)
|
|
720
1018
|
}
|
|
721
1019
|
|
|
722
|
-
|
|
723
|
-
|
|
1020
|
+
const totalW = w + padLeft + padRight
|
|
1021
|
+
const totalH = h + padTop + padBottom
|
|
1022
|
+
const scale = 2
|
|
1023
|
+
const canvas = document.createElement('canvas')
|
|
1024
|
+
canvas.width = totalW * scale
|
|
1025
|
+
canvas.height = totalH * scale
|
|
1026
|
+
const ctx = canvas.getContext('2d')!
|
|
724
1027
|
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
1028
|
+
ctx.save()
|
|
1029
|
+
ctx.scale(scale, scale)
|
|
1030
|
+
ctx.translate(-left + padLeft, -top + padTop)
|
|
1031
|
+
try {
|
|
1032
|
+
// 화살표 렌더
|
|
1033
|
+
this.component.render(ctx)
|
|
1034
|
+
ctx.fillStyle = this.component.state.strokeStyle || '#666666'
|
|
1035
|
+
ctx.fill()
|
|
1036
|
+
// 텍스트 렌더
|
|
1037
|
+
if ((this.component as any).hasTextProperty && (this.component as any).drawText) {
|
|
1038
|
+
;(this.component as any).drawText(ctx)
|
|
1039
|
+
}
|
|
1040
|
+
} catch { /* render 실패 시 빈 텍스처 */ }
|
|
1041
|
+
ctx.restore()
|
|
731
1042
|
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
1043
|
+
this._texture = new THREE.CanvasTexture(canvas)
|
|
1044
|
+
this._texture.colorSpace = THREE.SRGBColorSpace
|
|
1045
|
+
|
|
1046
|
+
const geo = new THREE.PlaneGeometry(totalW, totalH)
|
|
1047
|
+
geo.rotateX(-Math.PI / 2)
|
|
1048
|
+
|
|
1049
|
+
const mat = new THREE.MeshBasicMaterial({
|
|
1050
|
+
map: this._texture,
|
|
1051
|
+
alphaTest: 0.1,
|
|
1052
|
+
side: THREE.DoubleSide,
|
|
1053
|
+
})
|
|
1054
|
+
|
|
1055
|
+
this._mesh = new THREE.Mesh(geo, mat)
|
|
1056
|
+
// 비대칭 패딩 보정: 플레인 센터를 컴포넌트 bounds 센터에 맞춤
|
|
1057
|
+
this._mesh.position.x = (padRight - padLeft) / 2
|
|
1058
|
+
this._mesh.position.y = 1
|
|
1059
|
+
this._mesh.position.z = (padBottom - padTop) / 2
|
|
1060
|
+
this.object3d.add(this._mesh)
|
|
740
1061
|
}
|
|
741
1062
|
|
|
742
1063
|
update() {
|
|
743
|
-
this.
|
|
744
|
-
|
|
1064
|
+
if (this._mesh) {
|
|
1065
|
+
this.object3d.remove(this._mesh)
|
|
1066
|
+
this._mesh.geometry.dispose()
|
|
1067
|
+
;(this._mesh.material as THREE.Material).dispose()
|
|
1068
|
+
this._texture?.dispose()
|
|
1069
|
+
this._mesh = undefined
|
|
1070
|
+
this._texture = undefined
|
|
1071
|
+
}
|
|
1072
|
+
this.build()
|
|
1073
|
+
this.updateTransform()
|
|
1074
|
+
this.updateHidden()
|
|
745
1075
|
}
|
|
746
1076
|
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
1077
|
+
onchange(after: Record<string, unknown>, before: Record<string, unknown>) {
|
|
1078
|
+
if ('width' in after || 'height' in after || 'orientation' in after ||
|
|
1079
|
+
'direction' in after || 'strokeStyle' in after || 'INOUTTYPE' in after) {
|
|
1080
|
+
this.update()
|
|
1081
|
+
return
|
|
1082
|
+
}
|
|
1083
|
+
super.onchange(after, before)
|
|
750
1084
|
}
|
|
1085
|
+
|
|
1086
|
+
updateDimension() {}
|
|
1087
|
+
updateAlpha() {}
|
|
751
1088
|
}
|
|
752
1089
|
|
|
753
|
-
registerRealObjectFactory('
|
|
1090
|
+
registerRealObjectFactory('PortFlow', (component: Component) => new PortFlowRealObject3D(component))
|
|
754
1091
|
|
|
755
1092
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
756
|
-
//
|
|
1093
|
+
// Node — 2D render를 바닥 Plane 텍스처로 3D 표시
|
|
757
1094
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
758
1095
|
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
1096
|
+
class NodeRealObject3D extends RealObjectGroup {
|
|
1097
|
+
private _mesh?: THREE.Mesh
|
|
1098
|
+
private _texture?: THREE.CanvasTexture
|
|
1099
|
+
|
|
1100
|
+
get zPriority(): number { return Z_FMSIM.NODE }
|
|
1101
|
+
get effectiveDepth(): number { return 0 }
|
|
1102
|
+
protected get syncZPosOffset(): number { return 0 }
|
|
1103
|
+
|
|
1104
|
+
build() {
|
|
1105
|
+
super.build()
|
|
1106
|
+
|
|
1107
|
+
const { cx, cy } = this.component.state
|
|
1108
|
+
const radius = 8
|
|
1109
|
+
const size = radius * 2
|
|
1110
|
+
const scale = 2
|
|
1111
|
+
const pad = 2
|
|
1112
|
+
|
|
1113
|
+
const canvas = document.createElement('canvas')
|
|
1114
|
+
canvas.width = (size + pad * 2) * scale
|
|
1115
|
+
canvas.height = (size + pad * 2) * scale
|
|
1116
|
+
const ctx = canvas.getContext('2d')!
|
|
1117
|
+
|
|
1118
|
+
ctx.save()
|
|
1119
|
+
ctx.scale(scale, scale)
|
|
1120
|
+
ctx.translate(-cx + radius + pad, -cy + radius + pad)
|
|
1121
|
+
try {
|
|
1122
|
+
this.component.render(ctx)
|
|
1123
|
+
const { fillStyle = '#888888' } = this.component.state
|
|
1124
|
+
ctx.fillStyle = typeof fillStyle === 'string' ? fillStyle : '#888888'
|
|
1125
|
+
ctx.fill()
|
|
1126
|
+
const { strokeStyle } = this.component.state
|
|
1127
|
+
if (strokeStyle && typeof strokeStyle === 'string') {
|
|
1128
|
+
ctx.strokeStyle = strokeStyle
|
|
1129
|
+
ctx.lineWidth = 1
|
|
1130
|
+
ctx.stroke()
|
|
1131
|
+
}
|
|
1132
|
+
} catch { /* render 실패 시 빈 텍스처 */ }
|
|
1133
|
+
ctx.restore()
|
|
1134
|
+
|
|
1135
|
+
this._texture = new THREE.CanvasTexture(canvas)
|
|
1136
|
+
this._texture.colorSpace = THREE.SRGBColorSpace
|
|
1137
|
+
|
|
1138
|
+
const geo = new THREE.PlaneGeometry(size + pad * 2, size + pad * 2)
|
|
1139
|
+
geo.rotateX(-Math.PI / 2)
|
|
1140
|
+
|
|
1141
|
+
const mat = new THREE.MeshBasicMaterial({
|
|
1142
|
+
map: this._texture,
|
|
1143
|
+
alphaTest: 0.1,
|
|
1144
|
+
side: THREE.DoubleSide,
|
|
1145
|
+
})
|
|
1146
|
+
|
|
1147
|
+
this._mesh = new THREE.Mesh(geo, mat)
|
|
1148
|
+
this._mesh.position.y = 1
|
|
1149
|
+
this.object3d.add(this._mesh)
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
update() {
|
|
1153
|
+
if (this._mesh) {
|
|
1154
|
+
this.object3d.remove(this._mesh)
|
|
1155
|
+
this._mesh.geometry.dispose()
|
|
1156
|
+
;(this._mesh.material as THREE.Material).dispose()
|
|
1157
|
+
this._texture?.dispose()
|
|
1158
|
+
this._mesh = undefined
|
|
1159
|
+
this._texture = undefined
|
|
1160
|
+
}
|
|
1161
|
+
this.build()
|
|
1162
|
+
this.updateTransform()
|
|
1163
|
+
this.updateHidden()
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
onchange(after: Record<string, unknown>, before: Record<string, unknown>) {
|
|
1167
|
+
if ('fillStyle' in after || 'strokeStyle' in after) {
|
|
1168
|
+
this.update()
|
|
1169
|
+
return
|
|
1170
|
+
}
|
|
1171
|
+
super.onchange(after, before)
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
updateDimension() {}
|
|
1175
|
+
updateAlpha() {}
|
|
1176
|
+
}
|
|
762
1177
|
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
1178
|
+
registerRealObjectFactory('Node', (component: Component) => new NodeRealObject3D(component))
|
|
1179
|
+
|
|
1180
|
+
// Scanner — 2D Triangle 실루엣과 정합되는 삼각형 prism.
|
|
1181
|
+
// 이송 표면 높이의 0.2배(=6) 얇은 기둥 — 원래 값 유지, shape 만 'box' → 'triangle'.
|
|
1182
|
+
const SCANNER_CONFIG: MCS3DConfig = {
|
|
1183
|
+
...DEFAULTS,
|
|
1184
|
+
defaultDepth: TRANSPORT_SURFACE_HEIGHT * 0.2,
|
|
1185
|
+
defaultZPosFromParent: true,
|
|
1186
|
+
opacity: 1,
|
|
1187
|
+
metalness: 0.3,
|
|
1188
|
+
roughness: 0.7,
|
|
1189
|
+
defaultColor: 0x888888,
|
|
1190
|
+
shape: 'triangle',
|
|
1191
|
+
zPriority: Z_FMSIM.SCANNER,
|
|
1192
|
+
}
|
|
1193
|
+
registerRealObjectFactory('Scanner', (component: Component) => new MCSRealObject3D(component, SCANNER_CONFIG))
|