@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
|
@@ -8,9 +8,13 @@
|
|
|
8
8
|
* - Unit: 낮은 depth, 불투명
|
|
9
9
|
*/
|
|
10
10
|
import * as THREE from 'three';
|
|
11
|
-
import
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
11
|
+
import * as BufferGeometryUtils from 'three/examples/jsm/utils/BufferGeometryUtils.js';
|
|
12
|
+
import { RealObjectGroup, registerRealObjectFactory, disposeObject3D, MaterialBank } from '@hatiolab/things-scene';
|
|
13
|
+
import { LEGEND_CAPACITY, resolveCarrierLegend } from '../features/mcs-status-default';
|
|
14
|
+
import { getValueOnRanges } from '../utils/get-value-on-ranges';
|
|
15
|
+
import { fitGaugeScale } from '../utils/fit-gauge-scale';
|
|
16
|
+
import { carrierSegmentsFromData, abnormalSegmentsFromData, carrierColorsFromLegend, ABNORMAL_COLORS } from '../utils/stocker-gauge';
|
|
17
|
+
import { Z_FMSIM } from './z-priority';
|
|
14
18
|
/**
|
|
15
19
|
* 포트 기본 높이 (절대값).
|
|
16
20
|
* 모든 이송장치의 상단 높이 = 이 값.
|
|
@@ -33,15 +37,32 @@ class MCSRealObject3D extends RealObjectGroup {
|
|
|
33
37
|
this._updatePending = false;
|
|
34
38
|
this._config = config;
|
|
35
39
|
}
|
|
36
|
-
|
|
40
|
+
get zPriority() {
|
|
41
|
+
var _a;
|
|
42
|
+
return (_a = this._config.zPriority) !== null && _a !== void 0 ? _a : 0;
|
|
43
|
+
}
|
|
44
|
+
// width, height 절대값 (음수 값은 양수로 취급).
|
|
45
|
+
// `resizable:false` 컴포넌트(예: Scanner)는 state.width/height 가 없고
|
|
46
|
+
// bounds 만 있을 수 있으므로 bounds 를 fallback 으로 사용.
|
|
47
|
+
//
|
|
48
|
+
// `??` only catches null/undefined — a property-panel input that produced
|
|
49
|
+
// NaN would slip through and corrupt every downstream Math.* call. Use a
|
|
50
|
+
// finite check so NaN falls through to bounds → 10 like missing values do.
|
|
37
51
|
get absSize() {
|
|
38
|
-
const
|
|
52
|
+
const state = (this.component.state || {});
|
|
53
|
+
const bounds = (this.component.bounds || {});
|
|
54
|
+
const width = pickFinite(state.width, bounds.width, 10);
|
|
55
|
+
const height = pickFinite(state.height, bounds.height, 10);
|
|
39
56
|
return { w: Math.abs(width), h: Math.abs(height) };
|
|
40
57
|
}
|
|
41
58
|
get effectiveDepth() {
|
|
42
59
|
const { depth } = this.component.state;
|
|
43
|
-
if (depth)
|
|
60
|
+
// Truthy `if (depth)` already rejected NaN/0/undefined, but be explicit
|
|
61
|
+
// so the intent (and the resolution chain) reads cleanly: only an
|
|
62
|
+
// explicit positive finite number wins; everything else → defaults.
|
|
63
|
+
if (typeof depth === 'number' && Number.isFinite(depth) && depth > 0) {
|
|
44
64
|
return depth;
|
|
65
|
+
}
|
|
45
66
|
if (this._config.defaultDepth)
|
|
46
67
|
return this._config.defaultDepth;
|
|
47
68
|
const { w, h } = this.absSize;
|
|
@@ -49,20 +70,33 @@ class MCSRealObject3D extends RealObjectGroup {
|
|
|
49
70
|
}
|
|
50
71
|
get syncZPosOffset() {
|
|
51
72
|
var _a, _b;
|
|
73
|
+
// syncZPosOffset = placement + geometric (center origin = effectiveDepth/2)
|
|
74
|
+
// 중첩 보정(parent shift 상쇄)은 base RealObject.cz가 parent.geometricOffsetY를
|
|
75
|
+
// 빼는 방식으로 처리하므로 여기선 placement + 본인 geometric만 반환
|
|
76
|
+
const halfD = this.effectiveDepth / 2;
|
|
77
|
+
// zPosOverride: 고정 높이 (OHTLine 등)
|
|
78
|
+
if (this._config.zPosOverride != null) {
|
|
79
|
+
return this._config.zPosOverride + halfD;
|
|
80
|
+
}
|
|
52
81
|
// Port: 이송 표면 높이에 배치
|
|
53
82
|
if (this._config.defaultZPosFromParent) {
|
|
54
|
-
return TRANSPORT_SURFACE_HEIGHT;
|
|
83
|
+
return TRANSPORT_SURFACE_HEIGHT + halfD;
|
|
55
84
|
}
|
|
56
|
-
// Carrier:
|
|
85
|
+
// Carrier: 부모 유형에 따라 배치 위치 결정
|
|
57
86
|
if (this._config.sizeRatio && this._config.sizeRatio < 1) {
|
|
58
87
|
const parentType = (_b = (_a = this.component.parent) === null || _a === void 0 ? void 0 : _a.state) === null || _b === void 0 ? void 0 : _b.type;
|
|
59
|
-
|
|
60
|
-
|
|
88
|
+
// OHT: 그리퍼 아래에 매달림 — placement = -effectiveDepth(carrier top이 OHT bottom)
|
|
89
|
+
if (parentType === 'OHT') {
|
|
90
|
+
return -this.effectiveDepth + halfD;
|
|
61
91
|
}
|
|
62
|
-
|
|
92
|
+
if (parentType === 'Port' || parentType === 'Shelf' || parentType === 'Shuttle') {
|
|
93
|
+
return halfD;
|
|
94
|
+
}
|
|
95
|
+
return TRANSPORT_SURFACE_HEIGHT + halfD;
|
|
63
96
|
}
|
|
64
|
-
return
|
|
97
|
+
return halfD;
|
|
65
98
|
}
|
|
99
|
+
// geometric 부분(center origin: depth/2)은 base 기본값 사용 — override 불필요
|
|
66
100
|
// 컴포넌트의 상태 색상(statusColor) 또는 fillStyle → Three.js 색상 변환
|
|
67
101
|
resolveColor() {
|
|
68
102
|
const comp = this.component;
|
|
@@ -70,7 +104,7 @@ class MCSRealObject3D extends RealObjectGroup {
|
|
|
70
104
|
// '#F0F0F0'은 상태 미설정 시 기본 fallback — 무시하고 defaultColor 사용
|
|
71
105
|
try {
|
|
72
106
|
const sc = comp.statusColor;
|
|
73
|
-
if (typeof sc === 'string' && sc && sc.toUpperCase() !== '#F0F0F0')
|
|
107
|
+
if (typeof sc === 'string' && sc && sc.toUpperCase() !== '#F0F0F0' && sc !== 'transparent')
|
|
74
108
|
return new THREE.Color(sc).getHex();
|
|
75
109
|
}
|
|
76
110
|
catch (_a) {
|
|
@@ -78,7 +112,7 @@ class MCSRealObject3D extends RealObjectGroup {
|
|
|
78
112
|
}
|
|
79
113
|
// 2) state.fillStyle fallback
|
|
80
114
|
const { fillStyle } = this.component.state;
|
|
81
|
-
if (fillStyle && typeof fillStyle === 'string') {
|
|
115
|
+
if (fillStyle && typeof fillStyle === 'string' && fillStyle !== 'transparent') {
|
|
82
116
|
try {
|
|
83
117
|
return new THREE.Color(fillStyle).getHex();
|
|
84
118
|
}
|
|
@@ -118,7 +152,7 @@ class MCSRealObject3D extends RealObjectGroup {
|
|
|
118
152
|
// ── Geometry builders ──
|
|
119
153
|
buildBoxGeometry(w, d, h) {
|
|
120
154
|
const geo = new THREE.BoxGeometry(w, d, h);
|
|
121
|
-
return this.removeBottomFace(geo);
|
|
155
|
+
return this._config.keepBottomFace ? geo : this.removeBottomFace(geo);
|
|
122
156
|
}
|
|
123
157
|
buildHexagonGeometry(w, d, h) {
|
|
124
158
|
// Buffer의 2D 형태: 좌우 끝이 꺾인 육각형
|
|
@@ -143,6 +177,37 @@ class MCSRealObject3D extends RealObjectGroup {
|
|
|
143
177
|
geo.rotateX(Math.PI / 2);
|
|
144
178
|
return this.removeBottomFace(geo);
|
|
145
179
|
}
|
|
180
|
+
buildPlaneGeometry(w, h) {
|
|
181
|
+
const geo = new THREE.PlaneGeometry(w, h);
|
|
182
|
+
geo.rotateX(-Math.PI / 2);
|
|
183
|
+
return geo;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* 삼각형 prism — 2D Triangle 의 꼭짓점(state.x1/y1, x2/y2, x3/y3) 을 그대로 사용.
|
|
187
|
+
* things-scene 의 TriangleExtrude 와 동일한 방식 — 각 Scanner 인스턴스의
|
|
188
|
+
* 실제 정점 배치(방향, 비대칭 등) 가 3D 에 반영된다.
|
|
189
|
+
*
|
|
190
|
+
* `geometry.center()` 로 로컬 원점을 bounding-box 중심으로 맞추므로
|
|
191
|
+
* 후속 placement 와 정합.
|
|
192
|
+
*/
|
|
193
|
+
buildTriangleGeometry(d) {
|
|
194
|
+
const state = (this.component.state || {});
|
|
195
|
+
const { x1 = 0, y1 = 0, x2 = 0, y2 = 0, x3 = 0, y3 = 0 } = state;
|
|
196
|
+
const shape = new THREE.Shape();
|
|
197
|
+
shape.moveTo(x1, y1);
|
|
198
|
+
shape.lineTo(x2, y2);
|
|
199
|
+
shape.lineTo(x3, y3);
|
|
200
|
+
shape.closePath();
|
|
201
|
+
const geo = new THREE.ExtrudeGeometry(shape, {
|
|
202
|
+
steps: 1,
|
|
203
|
+
depth: d,
|
|
204
|
+
bevelEnabled: false,
|
|
205
|
+
});
|
|
206
|
+
// bounding box 중심으로 원점 정렬 후 Y-up 좌표계로 회전
|
|
207
|
+
geo.center();
|
|
208
|
+
geo.rotateX(Math.PI / 2);
|
|
209
|
+
return this.removeBottomFace(geo);
|
|
210
|
+
}
|
|
146
211
|
build() {
|
|
147
212
|
var _a;
|
|
148
213
|
super.build();
|
|
@@ -159,19 +224,30 @@ class MCSRealObject3D extends RealObjectGroup {
|
|
|
159
224
|
const depth = rawDepth * ratio;
|
|
160
225
|
const color = this.resolveColor();
|
|
161
226
|
const { opacity, metalness, roughness, shape } = this._config;
|
|
162
|
-
const geometry = shape === '
|
|
163
|
-
? this.
|
|
164
|
-
:
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
227
|
+
const geometry = shape === 'plane'
|
|
228
|
+
? this.buildPlaneGeometry(w, h)
|
|
229
|
+
: shape === 'hexagon'
|
|
230
|
+
? this.buildHexagonGeometry(w, depth, h)
|
|
231
|
+
: shape === 'triangle'
|
|
232
|
+
? this.buildTriangleGeometry(depth)
|
|
233
|
+
: this.buildBoxGeometry(w, depth, h);
|
|
234
|
+
const isPlane = shape === 'plane';
|
|
235
|
+
// Standard body 는 MaterialBank 공유(색 변형) — 인스턴스 dedup + faint 안전(코어가 공유 base 의
|
|
236
|
+
// 변형만 스왑, 원본 base 불변). plane 은 unlit MeshBasicMaterial 이라 bank(Standard 전용) 비대상 → raw.
|
|
237
|
+
const material = isPlane
|
|
238
|
+
? new THREE.MeshBasicMaterial({
|
|
239
|
+
color,
|
|
240
|
+
opacity,
|
|
241
|
+
transparent: opacity < 1,
|
|
242
|
+
side: THREE.DoubleSide,
|
|
243
|
+
})
|
|
244
|
+
: MaterialBank.material('mcs|body', Object.assign({ metalness,
|
|
245
|
+
roughness,
|
|
246
|
+
opacity, side: opacity < 1 ? THREE.DoubleSide : THREE.FrontSide }, (this._config.emissiveIntensity
|
|
247
|
+
? { emissive: color, emissiveIntensity: this._config.emissiveIntensity }
|
|
248
|
+
: {})), color);
|
|
249
|
+
// z-fighting 방지: 바닥면(floor grid)보다 앞. bank body 는 공유 base 라 polygonOffset 은 모든 mcs
|
|
250
|
+
// body 공통 상수 — 반환 material 에 세팅(같은 값이라 멱등, 인스턴스별 발산 없음).
|
|
175
251
|
material.polygonOffset = true;
|
|
176
252
|
material.polygonOffsetFactor = -1;
|
|
177
253
|
material.polygonOffsetUnit = -1;
|
|
@@ -195,11 +271,18 @@ class MCSRealObject3D extends RealObjectGroup {
|
|
|
195
271
|
}
|
|
196
272
|
}
|
|
197
273
|
this._ownMesh = new THREE.Mesh(geometry, meshMaterial);
|
|
198
|
-
//
|
|
199
|
-
|
|
200
|
-
|
|
274
|
+
// ownGroup 기준(bottom-up): mesh 바닥을 y=0에 맞추고 ownGroup이 -d/2로 shift해
|
|
275
|
+
// center-origin object3d에 정렬됨.
|
|
276
|
+
// box/hexagon (center-local geometry): y=rawDepth/2 (바닥을 0에 맞춤)
|
|
277
|
+
// plane: y=1 (바닥 살짝 띄움)
|
|
278
|
+
this._ownMesh.position.y = isPlane ? 1 : rawDepth / 2;
|
|
279
|
+
this._ownMesh.castShadow = !isPlane;
|
|
201
280
|
this._ownMesh.receiveShadow = true;
|
|
202
|
-
this.
|
|
281
|
+
this.components3D.add(this._ownMesh);
|
|
282
|
+
// mesh 는 super.build() *이후* 생성되므로, 코어의 build-시점 faint 적용이 이 mesh 에 못 닿는다.
|
|
283
|
+
// mesh 생성 직후 명시 적용 — 코어 applyMaterialOpacity 가 content-alpha × faint 를 일괄 처리.
|
|
284
|
+
// view 모드에선 factor=1(=state.alpha 없음) → canonical(config.opacity) 그대로, 고객 화면 불변.
|
|
285
|
+
this.updateAlpha();
|
|
203
286
|
}
|
|
204
287
|
onchange(after, before) {
|
|
205
288
|
if ('width' in after ||
|
|
@@ -283,7 +366,7 @@ class MCSRealObject3D extends RealObjectGroup {
|
|
|
283
366
|
update() {
|
|
284
367
|
// 자신의 mesh만 제거 (자식 컴포넌트 3D 오브젝트 보존)
|
|
285
368
|
if (this._ownMesh) {
|
|
286
|
-
this.
|
|
369
|
+
this.components3D.remove(this._ownMesh);
|
|
287
370
|
disposeObject3D(this._ownMesh);
|
|
288
371
|
this._ownMesh = undefined;
|
|
289
372
|
}
|
|
@@ -294,24 +377,19 @@ class MCSRealObject3D extends RealObjectGroup {
|
|
|
294
377
|
}
|
|
295
378
|
// RealObjectGroup 기본 동작 비활성화
|
|
296
379
|
updateDimension() { }
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
if (m.map)
|
|
309
|
-
continue; // 텍스처 material은 자체 transparent 유지
|
|
310
|
-
m.opacity = effective;
|
|
311
|
-
m.transparent = effective < 1;
|
|
312
|
-
}
|
|
313
|
-
});
|
|
380
|
+
}
|
|
381
|
+
// ── 헬퍼 ────────────────────────────────────────────────
|
|
382
|
+
/**
|
|
383
|
+
* Returns the first argument that is a finite number. NaN slips past `??`
|
|
384
|
+
* (which only catches null/undefined), so without this a property-panel
|
|
385
|
+
* input producing NaN would corrupt absSize and cascade through Math.*.
|
|
386
|
+
*/
|
|
387
|
+
function pickFinite(...candidates) {
|
|
388
|
+
for (const c of candidates) {
|
|
389
|
+
if (typeof c === 'number' && Number.isFinite(c))
|
|
390
|
+
return c;
|
|
314
391
|
}
|
|
392
|
+
return 0;
|
|
315
393
|
}
|
|
316
394
|
// ── 팩토리 등록 헬퍼 ─────────────────────────────────────
|
|
317
395
|
function register(type, overrides = {}) {
|
|
@@ -327,6 +405,7 @@ register('Equipment', {
|
|
|
327
405
|
metalness: 0.4,
|
|
328
406
|
roughness: 0.6,
|
|
329
407
|
defaultColor: 0x8899aa,
|
|
408
|
+
zPriority: Z_FMSIM.EQUIPMENT,
|
|
330
409
|
});
|
|
331
410
|
register('Buffer', {
|
|
332
411
|
defaultDepth: TRANSPORT_SURFACE_HEIGHT,
|
|
@@ -335,21 +414,96 @@ register('Buffer', {
|
|
|
335
414
|
roughness: 0.7,
|
|
336
415
|
defaultColor: 0x99aa88,
|
|
337
416
|
shape: 'hexagon',
|
|
417
|
+
zPriority: Z_FMSIM.BUFFER,
|
|
338
418
|
});
|
|
339
419
|
register('AGVLine', {
|
|
340
|
-
depthRatio: 0
|
|
341
|
-
|
|
342
|
-
roughness: 0.3,
|
|
343
|
-
defaultColor: 0x888899,
|
|
344
|
-
});
|
|
345
|
-
register('OHTLine', {
|
|
346
|
-
depthRatio: 0.12,
|
|
420
|
+
depthRatio: 0,
|
|
421
|
+
defaultDepth: 0.1,
|
|
347
422
|
metalness: 0.6,
|
|
348
423
|
roughness: 0.3,
|
|
349
424
|
defaultColor: 0x888899,
|
|
425
|
+
zPriority: Z_FMSIM.AGV_LINE,
|
|
350
426
|
});
|
|
427
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
428
|
+
// OHTLine — 천장 I-beam 레일
|
|
429
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
430
|
+
const OHT_LINE_RAIL_COLOR = 0xcccccc;
|
|
431
|
+
const OHT_LINE_HEIGHT = 7 * TRANSPORT_SURFACE_HEIGHT; // OHT 상단과 일치
|
|
432
|
+
class OHTLineRealObject3D extends RealObjectGroup {
|
|
433
|
+
get zPriority() { return Z_FMSIM.OHT_LINE; }
|
|
434
|
+
get effectiveDepth() { return 0; }
|
|
435
|
+
get syncZPosOffset() { return OHT_LINE_HEIGHT; }
|
|
436
|
+
build() {
|
|
437
|
+
super.build();
|
|
438
|
+
const { width = 10, height = 10 } = this.component.state;
|
|
439
|
+
const w = Math.abs(width);
|
|
440
|
+
const h = Math.abs(height);
|
|
441
|
+
if (!w || !h)
|
|
442
|
+
return;
|
|
443
|
+
// I-beam 치수: 이송 방향 = 긴 변, 폭은 2D의 1/2
|
|
444
|
+
const isVertical = w < h;
|
|
445
|
+
const shortSide = isVertical ? w : h; // 2D 원본 짧은 쪽 폭
|
|
446
|
+
const beltW = shortSide * 0.25; // I-beam 폭: 원본의 1/4
|
|
447
|
+
const beamH = Math.max(beltW * 0.5, 3); // I-beam 전체 높이
|
|
448
|
+
const inset = shortSide * 0.5; // 양끝 여백: 원본 폭의 1/2
|
|
449
|
+
const railLen = (isVertical ? h : w) - inset * 2;
|
|
450
|
+
const flangeH = beamH * 0.2; // 플랜지 두께
|
|
451
|
+
const flangeW = beltW * 0.8; // 플랜지 폭
|
|
452
|
+
const webH = beamH - flangeH * 2; // 웹 높이
|
|
453
|
+
const webW = Math.max(flangeW * 0.25, 1); // 웹 두께
|
|
454
|
+
// 공유 base(MaterialBank) — OHTLine rail 은 정적 색이라 전 인스턴스 dedup. faint 는 코어가
|
|
455
|
+
// 공유 base 의 변형을 스왑(원본 불변). dispose 는 bank-safe(update 에서 material.dispose 안 함).
|
|
456
|
+
const mat = MaterialBank.material('oht-line|rail', { metalness: 0.5, roughness: 0.4 }, OHT_LINE_RAIL_COLOR);
|
|
457
|
+
const geoms = [];
|
|
458
|
+
// 상부 플랜지
|
|
459
|
+
const topFlange = isVertical
|
|
460
|
+
? new THREE.BoxGeometry(flangeW, flangeH, railLen)
|
|
461
|
+
: new THREE.BoxGeometry(railLen, flangeH, flangeW);
|
|
462
|
+
topFlange.translate(0, -flangeH / 2, 0);
|
|
463
|
+
geoms.push(topFlange);
|
|
464
|
+
// 웹 (수직판)
|
|
465
|
+
const web = isVertical
|
|
466
|
+
? new THREE.BoxGeometry(webW, webH, railLen)
|
|
467
|
+
: new THREE.BoxGeometry(railLen, webH, webW);
|
|
468
|
+
web.translate(0, -flangeH - webH / 2, 0);
|
|
469
|
+
geoms.push(web);
|
|
470
|
+
// 하부 플랜지
|
|
471
|
+
const botFlange = isVertical
|
|
472
|
+
? new THREE.BoxGeometry(flangeW, flangeH, railLen)
|
|
473
|
+
: new THREE.BoxGeometry(railLen, flangeH, flangeW);
|
|
474
|
+
botFlange.translate(0, -flangeH - webH - flangeH / 2, 0);
|
|
475
|
+
geoms.push(botFlange);
|
|
476
|
+
this._mesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(geoms), mat);
|
|
477
|
+
this._mesh.castShadow = true;
|
|
478
|
+
this.object3d.add(this._mesh);
|
|
479
|
+
// mesh 가 super.build() 이후 생성 → 코어 faint 를 명시 적용(편집 흐림이 mesh 에 닿게).
|
|
480
|
+
this.updateAlpha();
|
|
481
|
+
}
|
|
482
|
+
update() {
|
|
483
|
+
if (this._mesh) {
|
|
484
|
+
this.object3d.remove(this._mesh);
|
|
485
|
+
this._mesh.geometry.dispose();
|
|
486
|
+
// material 은 MaterialBank 공유(앱 수명) — 여기서 dispose 하면 다른 인스턴스가 깨진다.
|
|
487
|
+
this._mesh = undefined;
|
|
488
|
+
}
|
|
489
|
+
this.build();
|
|
490
|
+
this.updateTransform();
|
|
491
|
+
this.updateHidden();
|
|
492
|
+
}
|
|
493
|
+
onchange(after, before) {
|
|
494
|
+
if ('width' in after || 'height' in after || 'depth' in after ||
|
|
495
|
+
'fillStyle' in after || 'strokeStyle' in after) {
|
|
496
|
+
this.update();
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
super.onchange(after, before);
|
|
500
|
+
}
|
|
501
|
+
// updateAlpha override 제거 — 코어 RealObject.updateAlpha(=applyMaterialOpacity)로 수렴(faint).
|
|
502
|
+
updateDimension() { }
|
|
503
|
+
}
|
|
504
|
+
registerRealObjectFactory('OHTLine', (component) => new OHTLineRealObject3D(component));
|
|
351
505
|
// STOCKER: 벽면에 용량 게이지 바를 포함하는 커스텀 3D 오브젝트
|
|
352
|
-
const STOCKER_CONFIG = Object.assign(Object.assign({}, DEFAULTS), { defaultDepth: 5 * TRANSPORT_SURFACE_HEIGHT, opacity: 0.75, metalness: 0.3, roughness: 0.6, defaultColor: 0x8899bb });
|
|
506
|
+
const STOCKER_CONFIG = Object.assign(Object.assign({}, DEFAULTS), { defaultDepth: 5 * TRANSPORT_SURFACE_HEIGHT, opacity: 0.75, metalness: 0.3, roughness: 0.6, defaultColor: 0x8899bb, zPriority: Z_FMSIM.STOCKER });
|
|
353
507
|
class StockerRealObject3D extends MCSRealObject3D {
|
|
354
508
|
constructor(component) {
|
|
355
509
|
super(component, STOCKER_CONFIG);
|
|
@@ -362,7 +516,7 @@ class StockerRealObject3D extends MCSRealObject3D {
|
|
|
362
516
|
var _a, _b;
|
|
363
517
|
const comp = this.component;
|
|
364
518
|
const data = comp.data || {};
|
|
365
|
-
const gaugeMode = ((
|
|
519
|
+
const gaugeMode = ((_a = comp.root) === null || _a === void 0 ? void 0 : _a.stockerGaugeMode) || 'capacity';
|
|
366
520
|
const gauge = this.resolveGauge(data, gaugeMode);
|
|
367
521
|
// 테두리는 스토커의 상태 색상 (machine status)
|
|
368
522
|
const statusColor = comp.statusColor;
|
|
@@ -372,12 +526,18 @@ class StockerRealObject3D extends MCSRealObject3D {
|
|
|
372
526
|
const { w, h } = this.absSize;
|
|
373
527
|
const depth = this.effectiveDepth;
|
|
374
528
|
const isWidthLonger = w >= h;
|
|
375
|
-
// 게이지
|
|
376
|
-
const
|
|
377
|
-
const
|
|
529
|
+
// 게이지 최대 크기 (스토커가 충분히 크면 이 크기 그대로 사용).
|
|
530
|
+
const MAX_BAR_W = 130;
|
|
531
|
+
const MAX_BAR_H = 26;
|
|
532
|
+
// 스토커 지붕 footprint 에 맞춰 비례 축소 (확대는 안 함 — 현재 크기가 상한).
|
|
533
|
+
const fit = fitGaugeScale(w, h, MAX_BAR_W, MAX_BAR_H);
|
|
534
|
+
// PlaneGeometry 의 월드 크기는 clamp, 텍스처 캔버스 해상도는 최대 크기 기준 고정
|
|
535
|
+
// (축소되어도 선명도 유지).
|
|
536
|
+
const barW = MAX_BAR_W * fit;
|
|
537
|
+
const barH = MAX_BAR_H * fit;
|
|
378
538
|
const scale = 4;
|
|
379
|
-
const canvasW =
|
|
380
|
-
const canvasH =
|
|
539
|
+
const canvasW = MAX_BAR_W * scale;
|
|
540
|
+
const canvasH = MAX_BAR_H * scale;
|
|
381
541
|
const canvas = document.createElement('canvas');
|
|
382
542
|
canvas.width = canvasW;
|
|
383
543
|
canvas.height = canvasH;
|
|
@@ -389,9 +549,12 @@ class StockerRealObject3D extends MCSRealObject3D {
|
|
|
389
549
|
ctx.strokeStyle = gauge.borderColor;
|
|
390
550
|
ctx.lineWidth = 12;
|
|
391
551
|
ctx.strokeRect(6, 6, canvasW - 12, canvasH - 12);
|
|
392
|
-
// 세그먼트 채움 (왼→오)
|
|
552
|
+
// 세그먼트 채움 (왼→오) + carrier/abnormal 은 세그먼트별 숫자(%) 표기 (2D 와 동일)
|
|
393
553
|
const innerW = canvasW - pad * 2;
|
|
394
554
|
const innerH = canvasH - pad * 2;
|
|
555
|
+
// 컴포넌트 폰트 설정(fontFamily/bold/italic) 을 그대로, 크기만 텍스처 scale 배 (2D 와 상대 크기 일치).
|
|
556
|
+
const fontSize = (Number((_b = this.component.state) === null || _b === void 0 ? void 0 : _b.fontSize) || 15) * scale;
|
|
557
|
+
const font = this.gaugeFont(fontSize);
|
|
395
558
|
let x = pad;
|
|
396
559
|
for (const seg of gauge.segments) {
|
|
397
560
|
const segW = Math.max(innerW * seg.ratio, seg.ratio > 0 ? 1 : 0);
|
|
@@ -399,14 +562,19 @@ class StockerRealObject3D extends MCSRealObject3D {
|
|
|
399
562
|
continue;
|
|
400
563
|
ctx.fillStyle = seg.color;
|
|
401
564
|
ctx.fillRect(x, pad, segW, innerH);
|
|
565
|
+
if (gauge.perSegment) {
|
|
566
|
+
this.drawGaugeSegmentNumber(ctx, seg.ratio, x, pad, segW, innerH, font, fontSize);
|
|
567
|
+
}
|
|
402
568
|
x += segW;
|
|
403
569
|
}
|
|
404
|
-
//
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
570
|
+
// capacity: 전체 중앙에 usage% 한 개 (2D 와 동일). carrier/abnormal 은 위 세그먼트별 숫자 사용.
|
|
571
|
+
if (gauge.label) {
|
|
572
|
+
ctx.font = font;
|
|
573
|
+
ctx.textAlign = 'center';
|
|
574
|
+
ctx.textBaseline = 'middle';
|
|
575
|
+
ctx.fillStyle = '#333333';
|
|
576
|
+
ctx.fillText(gauge.label, canvasW / 2, canvasH / 2);
|
|
577
|
+
}
|
|
410
578
|
// 텍스처 → Plane
|
|
411
579
|
const texture = new THREE.CanvasTexture(canvas);
|
|
412
580
|
texture.colorSpace = THREE.SRGBColorSpace;
|
|
@@ -423,61 +591,94 @@ class StockerRealObject3D extends MCSRealObject3D {
|
|
|
423
591
|
mat.polygonOffsetFactor = -2;
|
|
424
592
|
mat.polygonOffsetUnit = -2;
|
|
425
593
|
this.gaugeMesh = new THREE.Mesh(geo, mat);
|
|
594
|
+
// ownGroup 기준(bottom-up): 볼륨 상단 = +depth. 그 위로 0.5 띄움.
|
|
426
595
|
this.gaugeMesh.position.set(0, depth + 0.5, 0);
|
|
427
|
-
this.
|
|
596
|
+
this.components3D.add(this.gaugeMesh);
|
|
428
597
|
}
|
|
429
598
|
resolveGauge(data, mode) {
|
|
430
|
-
var _a
|
|
599
|
+
var _a;
|
|
431
600
|
switch (mode) {
|
|
432
601
|
case 'carrier': {
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
const
|
|
436
|
-
const
|
|
437
|
-
const
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
{ ratio:
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
label: total > 0 ? `F${full} E${empty} EE${emptyEmpty}` : '',
|
|
448
|
-
};
|
|
602
|
+
// 세그먼트 비율/색상은 2D 와 공유되는 단일 소스에서 계산 (필드/색상 drift 방지).
|
|
603
|
+
// 색상은 carrier 컴포넌트와 동일한 legend(테마)에서 취득.
|
|
604
|
+
const colors = carrierColorsFromLegend(resolveCarrierLegend(this.component));
|
|
605
|
+
const [f, e, ee] = carrierSegmentsFromData(data, colors);
|
|
606
|
+
const total = f.count + e.count + ee.count;
|
|
607
|
+
// 접두사 없는 세그먼트별 숫자(%)로 표기 (capacity·2D 와 동일). label(중앙 한 개) 미사용.
|
|
608
|
+
return total > 0
|
|
609
|
+
? {
|
|
610
|
+
segments: [f, e, ee].map(s => ({ ratio: s.ratio, color: s.color })),
|
|
611
|
+
borderColor: colors.full,
|
|
612
|
+
label: null,
|
|
613
|
+
perSegment: true,
|
|
614
|
+
}
|
|
615
|
+
: { segments: [{ ratio: 1, color: '#dddddd' }], borderColor: colors.full, label: null, perSegment: false };
|
|
449
616
|
}
|
|
450
617
|
case 'abnormal': {
|
|
451
|
-
const
|
|
452
|
-
const
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
618
|
+
const [n, a] = abnormalSegmentsFromData(data);
|
|
619
|
+
const total = n.count + a.count;
|
|
620
|
+
return total > 0
|
|
621
|
+
? {
|
|
622
|
+
segments: [n, a].map(s => ({ ratio: s.ratio, color: s.color })),
|
|
623
|
+
borderColor: a.count > 0 ? ABNORMAL_COLORS.abnormal : ABNORMAL_COLORS.normal,
|
|
624
|
+
label: null,
|
|
625
|
+
perSegment: true,
|
|
626
|
+
}
|
|
627
|
+
: {
|
|
628
|
+
segments: [{ ratio: 1, color: '#dddddd' }],
|
|
629
|
+
borderColor: ABNORMAL_COLORS.normal,
|
|
630
|
+
label: null,
|
|
631
|
+
perSegment: false,
|
|
632
|
+
};
|
|
465
633
|
}
|
|
466
|
-
default: { // capacity
|
|
634
|
+
default: { // capacity — 전체 중앙에 usage% 한 개
|
|
467
635
|
const cur = Number(data.CURRENTCAPACITY) || 0;
|
|
468
636
|
const max = Number(data.MAXCAPACITY) || 100;
|
|
469
637
|
const fullup = data.FULLUP === 'T';
|
|
470
638
|
const usage = Math.round((cur / max) * 100);
|
|
471
|
-
const legend = ((
|
|
472
|
-
const color = fullup ? 'red' : (
|
|
639
|
+
const legend = ((_a = this.component.root) === null || _a === void 0 ? void 0 : _a.stockerCapacityLegendTheme) || LEGEND_CAPACITY;
|
|
640
|
+
const color = fullup ? 'red' : (getValueOnRanges(usage, legend) || '#44aa44');
|
|
473
641
|
return {
|
|
474
642
|
segments: [{ ratio: usage / 100, color }],
|
|
475
643
|
borderColor: color,
|
|
476
644
|
label: `${usage}%`,
|
|
645
|
+
perSegment: false,
|
|
477
646
|
};
|
|
478
647
|
}
|
|
479
648
|
}
|
|
480
649
|
}
|
|
650
|
+
/** 컴포넌트 폰트 설정(fontFamily/bold/italic)으로 CSS font 문자열 조합. size 는 텍스처 스케일 반영값. */
|
|
651
|
+
gaugeFont(sizePx) {
|
|
652
|
+
const st = this.component.state || {};
|
|
653
|
+
const parts = [];
|
|
654
|
+
if (st.bold)
|
|
655
|
+
parts.push('bold');
|
|
656
|
+
if (st.italic)
|
|
657
|
+
parts.push('italic');
|
|
658
|
+
parts.push(`${sizePx}px`);
|
|
659
|
+
parts.push(st.fontFamily || 'sans-serif');
|
|
660
|
+
return parts.join(' ');
|
|
661
|
+
}
|
|
662
|
+
/** 세그먼트 영역 중심에 숫자(%)만 표기. 좁으면 값% → 값 → 생략 (2D _drawSegmentLabel 과 동일 정책). */
|
|
663
|
+
drawGaugeSegmentNumber(ctx, ratio, x, y, w, h, font, fontSize) {
|
|
664
|
+
ctx.font = font;
|
|
665
|
+
ctx.textAlign = 'center';
|
|
666
|
+
ctx.textBaseline = 'middle';
|
|
667
|
+
ctx.fillStyle = '#333333';
|
|
668
|
+
const value = `${Math.round(ratio * 100)}`;
|
|
669
|
+
const cx = x + w / 2;
|
|
670
|
+
const cy = y + h / 2;
|
|
671
|
+
if (fontSize <= h && ctx.measureText(`${value}%`).width <= w) {
|
|
672
|
+
ctx.fillText(`${value}%`, cx, cy);
|
|
673
|
+
}
|
|
674
|
+
else if (fontSize <= h && ctx.measureText(value).width <= w) {
|
|
675
|
+
ctx.fillText(value, cx, cy);
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
update() {
|
|
679
|
+
this.clearGauge();
|
|
680
|
+
super.update();
|
|
681
|
+
}
|
|
481
682
|
onchange(after, before) {
|
|
482
683
|
if ('data' in after) {
|
|
483
684
|
this.clearGauge();
|
|
@@ -492,7 +693,7 @@ class StockerRealObject3D extends MCSRealObject3D {
|
|
|
492
693
|
clearGauge() {
|
|
493
694
|
var _a, _b;
|
|
494
695
|
if (this.gaugeMesh) {
|
|
495
|
-
this.
|
|
696
|
+
this.components3D.remove(this.gaugeMesh);
|
|
496
697
|
(_a = this.gaugeMesh.geometry) === null || _a === void 0 ? void 0 : _a.dispose();
|
|
497
698
|
const mat = this.gaugeMesh.material;
|
|
498
699
|
(_b = mat === null || mat === void 0 ? void 0 : mat.map) === null || _b === void 0 ? void 0 : _b.dispose();
|
|
@@ -505,123 +706,258 @@ registerRealObjectFactory('STOCKER', (component) => new StockerRealObject3D(comp
|
|
|
505
706
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
506
707
|
// Unit (이동체 · 홀더) — 낮은 depth, 불투명
|
|
507
708
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
508
|
-
// Port:
|
|
509
|
-
const PORT_CONFIG = Object.assign(Object.assign({}, DEFAULTS), { depthRatio: 0.
|
|
709
|
+
// Port: 바닥판 (PlaneGeometry)
|
|
710
|
+
const PORT_CONFIG = Object.assign(Object.assign({}, DEFAULTS), { depthRatio: 0.01, defaultZPosFromParent: true, opacity: 0.1, metalness: 0.3, roughness: 0.8, defaultColor: 0xb0a090, shape: 'plane', zPriority: Z_FMSIM.PORT });
|
|
510
711
|
registerRealObjectFactory('Port', (component) => new MCSRealObject3D(component, PORT_CONFIG));
|
|
511
712
|
register('Shelf', {
|
|
512
713
|
depthRatio: 0.25,
|
|
513
714
|
metalness: 0.3,
|
|
514
715
|
roughness: 0.7,
|
|
515
716
|
defaultColor: 0xaabbcc,
|
|
717
|
+
zPriority: Z_FMSIM.SHELF,
|
|
516
718
|
});
|
|
517
719
|
// Carrier: CARRIERSTATUS에 따른 3D 애니메이션 지원
|
|
518
|
-
const CARRIER_CONFIG = Object.assign(Object.assign({}, DEFAULTS), { depthRatio: 0.5, metalness: 0
|
|
720
|
+
const CARRIER_CONFIG = Object.assign(Object.assign({}, DEFAULTS), { depthRatio: 0.5, metalness: 0, roughness: 1, defaultColor: 0xf0f0f0, isVisible: (c) => !!c.state.id, sizeRatio: 0.8, texturedTop: true });
|
|
721
|
+
/**
|
|
722
|
+
* 적층 시 다음 carrier 의 z-position 이 *_이전 carrier 의 effectiveDepth × N*
|
|
723
|
+
* 만큼 위에 놓인다. N=1 이면 carrier 들이 맞붙어 적층 (기존), N<1 이면 부분
|
|
724
|
+
* overlap (3D box 들이 겹쳐서 적층). 사용자 의도 — carrier 두께는 두꺼워도
|
|
725
|
+
* 적층 시 *_compact 하게 보이도록* — N 을 작게.
|
|
726
|
+
*/
|
|
727
|
+
const CARRIER_STACK_STEP_RATIO = 0.9;
|
|
519
728
|
class CarrierRealObject3D extends MCSRealObject3D {
|
|
520
729
|
constructor(component) {
|
|
521
730
|
super(component, CARRIER_CONFIG);
|
|
522
|
-
this._animRaf = null;
|
|
523
|
-
this._animStartTime = 0;
|
|
524
|
-
}
|
|
525
|
-
build() {
|
|
526
|
-
super.build();
|
|
527
|
-
this.startCarrierAnimation();
|
|
528
731
|
}
|
|
529
732
|
/**
|
|
530
|
-
*
|
|
531
|
-
*
|
|
733
|
+
* cz override — *carrier 자기 결정* 의 적층 위치.
|
|
734
|
+
* parent.components 안 자기 index 까지의 형제 carrier 들의 effectiveDepth
|
|
735
|
+
* 누적합을 base cz 위에 더한다. holder 측에 별도 layout 책임을 두지 않고,
|
|
736
|
+
* carrier 가 자기 stack 위치를 알아서 결정.
|
|
532
737
|
*/
|
|
533
|
-
|
|
534
|
-
var _a, _b
|
|
535
|
-
|
|
536
|
-
const
|
|
537
|
-
if (!(
|
|
538
|
-
return;
|
|
539
|
-
const
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
738
|
+
get cz() {
|
|
739
|
+
var _a, _b;
|
|
740
|
+
const base = super.cz;
|
|
741
|
+
const parent = this.component.parent;
|
|
742
|
+
if (!(parent === null || parent === void 0 ? void 0 : parent.components))
|
|
743
|
+
return base;
|
|
744
|
+
const siblings = parent.components;
|
|
745
|
+
const idx = siblings.indexOf(this.component);
|
|
746
|
+
if (idx <= 0)
|
|
747
|
+
return base;
|
|
748
|
+
let offset = 0;
|
|
749
|
+
for (let i = 0; i < idx; i++) {
|
|
750
|
+
const ro = (_a = siblings[i]) === null || _a === void 0 ? void 0 : _a._realObject;
|
|
751
|
+
const eff = Number(ro === null || ro === void 0 ? void 0 : ro.effectiveDepth);
|
|
752
|
+
const d = Number.isFinite(eff) && eff > 0 ? eff : 1;
|
|
753
|
+
offset += d * CARRIER_STACK_STEP_RATIO;
|
|
754
|
+
}
|
|
755
|
+
// 적층 방향 — placement (= 부모 holder 타입) 에 따라 결정.
|
|
756
|
+
// OHT 처럼 그리퍼 아래에 매달리는 holder 는 carrier 가 *_아래로_* 누적,
|
|
757
|
+
// 그 외 (Port / Shelf / Shuttle / Vehicle) 는 위로 누적.
|
|
758
|
+
// MCSRealObject3D.syncZPosOffset 의 OHT 분기 (-effectiveDepth + halfD) 와
|
|
759
|
+
// 같은 방향성 일관 적용.
|
|
760
|
+
const parentType = (_b = parent === null || parent === void 0 ? void 0 : parent.state) === null || _b === void 0 ? void 0 : _b.type;
|
|
761
|
+
const dir = parentType === 'OHT' ? -1 : 1;
|
|
762
|
+
return base + dir * offset;
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
registerRealObjectFactory('Carrier', (component) => new CarrierRealObject3D(component));
|
|
766
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
767
|
+
// Gauge (용량 게이지) — 얇은 바
|
|
768
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
769
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
770
|
+
// Gauge (용량 게이지) — 2D 렌더링을 Plane 텍스처로 3D 표시
|
|
771
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
772
|
+
// Gauge 컴포넌트는 3D에서 표시하지 않음 (부모 STOCKER의 3D 모델에서 직접 게이지 렌더링)
|
|
773
|
+
// null 반환으로 3D 오브젝트 생성을 억제하고, default factory fallback도 방지
|
|
774
|
+
registerRealObjectFactory('StockerCapacityBar', () => undefined);
|
|
775
|
+
registerRealObjectFactory('MCSGaugeCapacityBar', () => undefined);
|
|
776
|
+
registerRealObjectFactory('ZoneCapacityBar', () => undefined);
|
|
777
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
778
|
+
// PortFlow — 2D render를 Plane 텍스처로 3D 표시
|
|
779
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
780
|
+
class PortFlowRealObject3D extends RealObjectGroup {
|
|
781
|
+
get zPriority() { return Z_FMSIM.PORT_FLOW; }
|
|
782
|
+
get effectiveDepth() { return 0; }
|
|
783
|
+
get syncZPosOffset() {
|
|
784
|
+
var _a;
|
|
785
|
+
// 부모 Port/Conveyor의 zPos와 동일하게
|
|
786
|
+
const parent = this.component.parentObject;
|
|
787
|
+
if (((_a = parent === null || parent === void 0 ? void 0 : parent.state) === null || _a === void 0 ? void 0 : _a.zPos) != null)
|
|
788
|
+
return parent.state.zPos;
|
|
789
|
+
return TRANSPORT_SURFACE_HEIGHT;
|
|
790
|
+
}
|
|
791
|
+
build() {
|
|
792
|
+
super.build();
|
|
793
|
+
const { width = 10, height = 10 } = this.component.state;
|
|
794
|
+
const w = Math.abs(width);
|
|
795
|
+
const h = Math.abs(height);
|
|
796
|
+
if (!w || !h)
|
|
548
797
|
return;
|
|
549
|
-
|
|
550
|
-
//
|
|
551
|
-
|
|
552
|
-
const
|
|
553
|
-
const
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
case 'heartbeat': {
|
|
561
|
-
const t = progress < 0.5 ? progress * 2 : (1 - progress) * 2;
|
|
562
|
-
const s = 1 + (animScale - 1) * t;
|
|
563
|
-
obj.scale.set(s, 1, s);
|
|
798
|
+
// 2D render를 캔버스에 캡처
|
|
799
|
+
// 화살표 날개 + 텍스트 영역이 bounds 밖으로 나가므로 전체 영역 계산
|
|
800
|
+
const { left, top } = this.component.bounds;
|
|
801
|
+
const { fontSize = 24, labelPosition = 'top' } = this.component.state;
|
|
802
|
+
const arrowPad = Math.min(w, h) * 0.4;
|
|
803
|
+
// 텍스트 영역 확장
|
|
804
|
+
let padLeft = arrowPad, padRight = arrowPad, padTop = arrowPad, padBottom = arrowPad;
|
|
805
|
+
if (this.component.hasTextProperty) {
|
|
806
|
+
switch (labelPosition) {
|
|
807
|
+
case 'top':
|
|
808
|
+
padTop = Math.max(padTop, fontSize + 4);
|
|
564
809
|
break;
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
obj.rotation.y = progress * Math.PI * 2;
|
|
810
|
+
case 'bottom':
|
|
811
|
+
padBottom = Math.max(padBottom, fontSize + 4);
|
|
568
812
|
break;
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
const theta = (_a = config.animation.theta) !== null && _a !== void 0 ? _a : 0.26;
|
|
572
|
-
const current = Math.sin(progress * Math.PI * 2) * theta;
|
|
573
|
-
obj.rotation.y = current;
|
|
813
|
+
case 'left':
|
|
814
|
+
padLeft = Math.max(padLeft, fontSize * 4 + 4);
|
|
574
815
|
break;
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
const alpha = 0.3 + 0.7 * (progress < 0.5 ? progress * 2 : (1 - progress) * 2);
|
|
578
|
-
obj.traverse((o) => {
|
|
579
|
-
if (o.material && !o.material.map) {
|
|
580
|
-
o.material.opacity = alpha;
|
|
581
|
-
o.material.transparent = true;
|
|
582
|
-
}
|
|
583
|
-
});
|
|
816
|
+
case 'right':
|
|
817
|
+
padRight = Math.max(padRight, fontSize * 4 + 4);
|
|
584
818
|
break;
|
|
585
|
-
}
|
|
586
819
|
}
|
|
587
|
-
this._animRaf = requestAnimationFrame(animate);
|
|
588
|
-
};
|
|
589
|
-
this._animRaf = requestAnimationFrame(animate);
|
|
590
|
-
}
|
|
591
|
-
// 자체 애니메이션이 실행 중이면 things-scene의 syncAnimationDelta 비활성화
|
|
592
|
-
syncAnimationDelta() {
|
|
593
|
-
if (!this._animRaf) {
|
|
594
|
-
super.syncAnimationDelta();
|
|
595
820
|
}
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
821
|
+
const totalW = w + padLeft + padRight;
|
|
822
|
+
const totalH = h + padTop + padBottom;
|
|
823
|
+
const scale = 2;
|
|
824
|
+
const canvas = document.createElement('canvas');
|
|
825
|
+
canvas.width = totalW * scale;
|
|
826
|
+
canvas.height = totalH * scale;
|
|
827
|
+
const ctx = canvas.getContext('2d');
|
|
828
|
+
ctx.save();
|
|
829
|
+
ctx.scale(scale, scale);
|
|
830
|
+
ctx.translate(-left + padLeft, -top + padTop);
|
|
831
|
+
try {
|
|
832
|
+
// 화살표 렌더
|
|
833
|
+
this.component.render(ctx);
|
|
834
|
+
ctx.fillStyle = this.component.state.strokeStyle || '#666666';
|
|
835
|
+
ctx.fill();
|
|
836
|
+
// 텍스트 렌더
|
|
837
|
+
if (this.component.hasTextProperty && this.component.drawText) {
|
|
838
|
+
;
|
|
839
|
+
this.component.drawText(ctx);
|
|
840
|
+
}
|
|
601
841
|
}
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
this.
|
|
842
|
+
catch ( /* render 실패 시 빈 텍스처 */_a) { /* render 실패 시 빈 텍스처 */ }
|
|
843
|
+
ctx.restore();
|
|
844
|
+
this._texture = new THREE.CanvasTexture(canvas);
|
|
845
|
+
this._texture.colorSpace = THREE.SRGBColorSpace;
|
|
846
|
+
const geo = new THREE.PlaneGeometry(totalW, totalH);
|
|
847
|
+
geo.rotateX(-Math.PI / 2);
|
|
848
|
+
const mat = new THREE.MeshBasicMaterial({
|
|
849
|
+
map: this._texture,
|
|
850
|
+
alphaTest: 0.1,
|
|
851
|
+
side: THREE.DoubleSide,
|
|
852
|
+
});
|
|
853
|
+
this._mesh = new THREE.Mesh(geo, mat);
|
|
854
|
+
// 비대칭 패딩 보정: 플레인 센터를 컴포넌트 bounds 센터에 맞춤
|
|
855
|
+
this._mesh.position.x = (padRight - padLeft) / 2;
|
|
856
|
+
this._mesh.position.y = 1;
|
|
857
|
+
this._mesh.position.z = (padBottom - padTop) / 2;
|
|
858
|
+
this.object3d.add(this._mesh);
|
|
605
859
|
}
|
|
606
860
|
update() {
|
|
607
|
-
|
|
608
|
-
|
|
861
|
+
var _a;
|
|
862
|
+
if (this._mesh) {
|
|
863
|
+
this.object3d.remove(this._mesh);
|
|
864
|
+
this._mesh.geometry.dispose();
|
|
865
|
+
this._mesh.material.dispose();
|
|
866
|
+
(_a = this._texture) === null || _a === void 0 ? void 0 : _a.dispose();
|
|
867
|
+
this._mesh = undefined;
|
|
868
|
+
this._texture = undefined;
|
|
869
|
+
}
|
|
870
|
+
this.build();
|
|
871
|
+
this.updateTransform();
|
|
872
|
+
this.updateHidden();
|
|
609
873
|
}
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
874
|
+
onchange(after, before) {
|
|
875
|
+
if ('width' in after || 'height' in after || 'orientation' in after ||
|
|
876
|
+
'direction' in after || 'strokeStyle' in after || 'INOUTTYPE' in after) {
|
|
877
|
+
this.update();
|
|
878
|
+
return;
|
|
879
|
+
}
|
|
880
|
+
super.onchange(after, before);
|
|
613
881
|
}
|
|
882
|
+
updateDimension() { }
|
|
883
|
+
updateAlpha() { }
|
|
614
884
|
}
|
|
615
|
-
registerRealObjectFactory('
|
|
885
|
+
registerRealObjectFactory('PortFlow', (component) => new PortFlowRealObject3D(component));
|
|
616
886
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
617
|
-
//
|
|
887
|
+
// Node — 2D render를 바닥 Plane 텍스처로 3D 표시
|
|
618
888
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
889
|
+
class NodeRealObject3D extends RealObjectGroup {
|
|
890
|
+
get zPriority() { return Z_FMSIM.NODE; }
|
|
891
|
+
get effectiveDepth() { return 0; }
|
|
892
|
+
get syncZPosOffset() { return 0; }
|
|
893
|
+
build() {
|
|
894
|
+
super.build();
|
|
895
|
+
const { cx, cy } = this.component.state;
|
|
896
|
+
const radius = 8;
|
|
897
|
+
const size = radius * 2;
|
|
898
|
+
const scale = 2;
|
|
899
|
+
const pad = 2;
|
|
900
|
+
const canvas = document.createElement('canvas');
|
|
901
|
+
canvas.width = (size + pad * 2) * scale;
|
|
902
|
+
canvas.height = (size + pad * 2) * scale;
|
|
903
|
+
const ctx = canvas.getContext('2d');
|
|
904
|
+
ctx.save();
|
|
905
|
+
ctx.scale(scale, scale);
|
|
906
|
+
ctx.translate(-cx + radius + pad, -cy + radius + pad);
|
|
907
|
+
try {
|
|
908
|
+
this.component.render(ctx);
|
|
909
|
+
const { fillStyle = '#888888' } = this.component.state;
|
|
910
|
+
ctx.fillStyle = typeof fillStyle === 'string' ? fillStyle : '#888888';
|
|
911
|
+
ctx.fill();
|
|
912
|
+
const { strokeStyle } = this.component.state;
|
|
913
|
+
if (strokeStyle && typeof strokeStyle === 'string') {
|
|
914
|
+
ctx.strokeStyle = strokeStyle;
|
|
915
|
+
ctx.lineWidth = 1;
|
|
916
|
+
ctx.stroke();
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
catch ( /* render 실패 시 빈 텍스처 */_a) { /* render 실패 시 빈 텍스처 */ }
|
|
920
|
+
ctx.restore();
|
|
921
|
+
this._texture = new THREE.CanvasTexture(canvas);
|
|
922
|
+
this._texture.colorSpace = THREE.SRGBColorSpace;
|
|
923
|
+
const geo = new THREE.PlaneGeometry(size + pad * 2, size + pad * 2);
|
|
924
|
+
geo.rotateX(-Math.PI / 2);
|
|
925
|
+
const mat = new THREE.MeshBasicMaterial({
|
|
926
|
+
map: this._texture,
|
|
927
|
+
alphaTest: 0.1,
|
|
928
|
+
side: THREE.DoubleSide,
|
|
929
|
+
});
|
|
930
|
+
this._mesh = new THREE.Mesh(geo, mat);
|
|
931
|
+
this._mesh.position.y = 1;
|
|
932
|
+
this.object3d.add(this._mesh);
|
|
933
|
+
}
|
|
934
|
+
update() {
|
|
935
|
+
var _a;
|
|
936
|
+
if (this._mesh) {
|
|
937
|
+
this.object3d.remove(this._mesh);
|
|
938
|
+
this._mesh.geometry.dispose();
|
|
939
|
+
this._mesh.material.dispose();
|
|
940
|
+
(_a = this._texture) === null || _a === void 0 ? void 0 : _a.dispose();
|
|
941
|
+
this._mesh = undefined;
|
|
942
|
+
this._texture = undefined;
|
|
943
|
+
}
|
|
944
|
+
this.build();
|
|
945
|
+
this.updateTransform();
|
|
946
|
+
this.updateHidden();
|
|
947
|
+
}
|
|
948
|
+
onchange(after, before) {
|
|
949
|
+
if ('fillStyle' in after || 'strokeStyle' in after) {
|
|
950
|
+
this.update();
|
|
951
|
+
return;
|
|
952
|
+
}
|
|
953
|
+
super.onchange(after, before);
|
|
954
|
+
}
|
|
955
|
+
updateDimension() { }
|
|
956
|
+
updateAlpha() { }
|
|
957
|
+
}
|
|
958
|
+
registerRealObjectFactory('Node', (component) => new NodeRealObject3D(component));
|
|
959
|
+
// Scanner — 2D Triangle 실루엣과 정합되는 삼각형 prism.
|
|
960
|
+
// 이송 표면 높이의 0.2배(=6) 얇은 기둥 — 원래 값 유지, shape 만 'box' → 'triangle'.
|
|
961
|
+
const SCANNER_CONFIG = Object.assign(Object.assign({}, DEFAULTS), { defaultDepth: TRANSPORT_SURFACE_HEIGHT * 0.2, defaultZPosFromParent: true, opacity: 1, metalness: 0.3, roughness: 0.7, defaultColor: 0x888888, shape: 'triangle', zPriority: Z_FMSIM.SCANNER });
|
|
962
|
+
registerRealObjectFactory('Scanner', (component) => new MCSRealObject3D(component, SCANNER_CONFIG));
|
|
627
963
|
//# sourceMappingURL=machine-3d.js.map
|