@fmsim/machine 2.0.0-beta.10 → 2.0.0-beta.100

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. package/dist/agv.js +6 -2
  2. package/dist/agv.js.map +1 -1
  3. package/dist/carrier-manager.js +183 -0
  4. package/dist/carrier-manager.js.map +1 -0
  5. package/dist/carrier.js +99 -9
  6. package/dist/carrier.js.map +1 -1
  7. package/dist/conveyor-join.js +8 -2
  8. package/dist/conveyor-join.js.map +1 -1
  9. package/dist/conveyor.js +8 -0
  10. package/dist/conveyor.js.map +1 -1
  11. package/dist/crane.js +144 -1
  12. package/dist/crane.js.map +1 -1
  13. package/dist/factories/agv-3d.js +8 -9
  14. package/dist/factories/agv-3d.js.map +1 -1
  15. package/dist/factories/conveyor-3d.js +168 -81
  16. package/dist/factories/conveyor-3d.js.map +1 -1
  17. package/dist/factories/crane-3d.js +48 -12
  18. package/dist/factories/crane-3d.js.map +1 -1
  19. package/dist/factories/crane-rail-3d.js +6 -6
  20. package/dist/factories/crane-rail-3d.js.map +1 -1
  21. package/dist/factories/machine-3d.js +527 -181
  22. package/dist/factories/machine-3d.js.map +1 -1
  23. package/dist/factories/oht-3d.js +20 -20
  24. package/dist/factories/oht-3d.js.map +1 -1
  25. package/dist/factories/shuttle-3d.js +2 -5
  26. package/dist/factories/shuttle-3d.js.map +1 -1
  27. package/dist/factories/z-priority-defaults.js +33 -0
  28. package/dist/factories/z-priority-defaults.js.map +1 -0
  29. package/dist/factories/z-priority.js +54 -0
  30. package/dist/factories/z-priority.js.map +1 -0
  31. package/dist/features/conveyor-mixin.js.map +1 -1
  32. package/dist/features/mcs-status-default.js +15 -1
  33. package/dist/features/mcs-status-default.js.map +1 -1
  34. package/dist/features/mcs-status-mixin.js +22 -0
  35. package/dist/features/mcs-status-mixin.js.map +1 -1
  36. package/dist/features/mcs-zone-mixin.js.map +1 -1
  37. package/dist/features/parent-object-mixin.js +15 -7
  38. package/dist/features/parent-object-mixin.js.map +1 -1
  39. package/dist/floating-container.js +5 -0
  40. package/dist/floating-container.js.map +1 -1
  41. package/dist/index.js +52 -34
  42. package/dist/index.js.map +1 -1
  43. package/dist/mcs-carrier-holder.js +56 -22
  44. package/dist/mcs-carrier-holder.js.map +1 -1
  45. package/dist/mcs-machine.js +39 -0
  46. package/dist/mcs-machine.js.map +1 -1
  47. package/dist/mcs-unit.js +20 -0
  48. package/dist/mcs-unit.js.map +1 -1
  49. package/dist/mcs-vehicle.js +8 -3
  50. package/dist/mcs-vehicle.js.map +1 -1
  51. package/dist/node.js +10 -3
  52. package/dist/node.js.map +1 -1
  53. package/dist/oht.js +4 -0
  54. package/dist/oht.js.map +1 -1
  55. package/dist/port-flow.js +4 -0
  56. package/dist/port-flow.js.map +1 -1
  57. package/dist/port.js +4 -0
  58. package/dist/port.js.map +1 -1
  59. package/dist/scanner.js +30 -0
  60. package/dist/scanner.js.map +1 -0
  61. package/dist/scene/root-container-override.js +160 -96
  62. package/dist/scene/root-container-override.js.map +1 -1
  63. package/dist/shelf.js +4 -0
  64. package/dist/shelf.js.map +1 -1
  65. package/dist/static-container.js +5 -0
  66. package/dist/static-container.js.map +1 -1
  67. package/dist/stocker-abnormal-bar.js +5 -1
  68. package/dist/stocker-abnormal-bar.js.map +1 -1
  69. package/dist/stocker-capacity-bar.js +253 -6
  70. package/dist/stocker-capacity-bar.js.map +1 -1
  71. package/dist/stocker-carrier-bar.js +5 -1
  72. package/dist/stocker-carrier-bar.js.map +1 -1
  73. package/dist/stocker.js +6 -0
  74. package/dist/stocker.js.map +1 -1
  75. package/dist/templates/backdrop.js +30 -0
  76. package/dist/templates/backdrop.js.map +1 -0
  77. package/dist/templates/decal.js +21 -0
  78. package/dist/templates/decal.js.map +1 -0
  79. package/dist/templates/index.js +7 -1
  80. package/dist/templates/index.js.map +1 -1
  81. package/dist/templates/scanner.js +20 -0
  82. package/dist/templates/scanner.js.map +1 -0
  83. package/dist/tsconfig.tsbuildinfo +1 -1
  84. package/dist/utils/crane-position.js +83 -0
  85. package/dist/utils/crane-position.js.map +1 -0
  86. package/dist/utils/data-value.js +21 -0
  87. package/dist/utils/data-value.js.map +1 -0
  88. package/dist/utils/fit-gauge-scale.js +28 -0
  89. package/dist/utils/fit-gauge-scale.js.map +1 -0
  90. package/dist/utils/get-value-on-ranges.js +2 -4
  91. package/dist/utils/get-value-on-ranges.js.map +1 -1
  92. package/dist/utils/stocker-gauge.js +78 -0
  93. package/dist/utils/stocker-gauge.js.map +1 -0
  94. package/dist/zone-capacity-bar.js +3 -3
  95. package/dist/zone-capacity-bar.js.map +1 -1
  96. package/icons/backdrop.png +0 -0
  97. package/icons/decal.png +0 -0
  98. package/jest.config.js +10 -0
  99. package/package.json +4 -3
  100. package/src/agv.ts +7 -2
  101. package/src/carrier-manager.ts +223 -0
  102. package/src/carrier.ts +103 -10
  103. package/src/conveyor-join.ts +5 -2
  104. package/src/conveyor.ts +8 -0
  105. package/src/crane.ts +181 -1
  106. package/src/factories/agv-3d.ts +9 -10
  107. package/src/factories/conveyor-3d.ts +171 -84
  108. package/src/factories/crane-3d.ts +53 -12
  109. package/src/factories/crane-rail-3d.ts +8 -7
  110. package/src/factories/machine-3d.ts +616 -191
  111. package/src/factories/oht-3d.ts +20 -20
  112. package/src/factories/shuttle-3d.ts +2 -6
  113. package/src/factories/z-priority-defaults.ts +38 -0
  114. package/src/factories/z-priority.ts +60 -0
  115. package/src/features/conveyor-mixin.ts +1 -1
  116. package/src/features/mcs-status-default.ts +15 -1
  117. package/src/features/mcs-status-mixin.ts +26 -4
  118. package/src/features/mcs-zone-mixin.ts +1 -1
  119. package/src/features/parent-object-mixin.ts +17 -8
  120. package/src/floating-container.ts +5 -0
  121. package/src/index.ts +58 -34
  122. package/src/mcs-carrier-holder.ts +57 -34
  123. package/src/mcs-machine.ts +42 -0
  124. package/src/mcs-unit.ts +24 -1
  125. package/src/mcs-vehicle.ts +9 -6
  126. package/src/node.ts +9 -3
  127. package/src/oht.ts +6 -1
  128. package/src/port-flow.ts +5 -0
  129. package/src/port.ts +5 -0
  130. package/src/scanner.ts +29 -0
  131. package/src/scene/root-container-override.ts +154 -103
  132. package/src/shelf.ts +5 -0
  133. package/src/static-container.ts +5 -0
  134. package/src/stocker-abnormal-bar.ts +6 -1
  135. package/src/stocker-capacity-bar.ts +298 -7
  136. package/src/stocker-carrier-bar.ts +6 -1
  137. package/src/stocker.ts +7 -0
  138. package/src/templates/backdrop.ts +30 -0
  139. package/src/templates/decal.ts +21 -0
  140. package/src/templates/index.ts +7 -1
  141. package/src/templates/scanner.ts +20 -0
  142. package/src/utils/crane-position.ts +105 -0
  143. package/src/utils/data-value.ts +19 -0
  144. package/src/utils/fit-gauge-scale.ts +35 -0
  145. package/src/utils/get-value-on-ranges.ts +2 -4
  146. package/src/utils/stocker-gauge.ts +111 -0
  147. package/src/zone-capacity-bar.ts +3 -3
  148. package/translations/en.json +26 -1
  149. package/translations/ja.json +26 -1
  150. package/translations/ko.json +26 -1
  151. package/translations/ms.json +26 -1
  152. 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 * as BufferGeometryUtils from 'three/examples/jsm/utils/BufferGeometryUtils.js';
11
12
  import { RealObjectGroup, registerRealObjectFactory, disposeObject3D } from '@hatiolab/things-scene';
12
- import { LEGEND_CAPACITY } from '../features/mcs-status-default';
13
- import { getVaueOnRanges } from '../utils/get-value-on-ranges';
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
- // width, height 절대값 (음수 값은 양수로 취급)
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 { width = 10, height = 10 } = this.component.state;
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: Port 안이면 중앙, 그 외 컨테이너면 상단 위
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
- if (parentType === 'Port' || parentType === 'Shelf' || parentType === 'OHT' || parentType === 'Shuttle') {
60
- return 0;
88
+ // OHT: 그리퍼 아래에 매달림 — placement = -effectiveDepth(carrier top이 OHT bottom)
89
+ if (parentType === 'OHT') {
90
+ return -this.effectiveDepth + halfD;
61
91
  }
62
- return TRANSPORT_SURFACE_HEIGHT;
92
+ if (parentType === 'Port' || parentType === 'Shelf' || parentType === 'Shuttle') {
93
+ return halfD;
94
+ }
95
+ return TRANSPORT_SURFACE_HEIGHT + halfD;
63
96
  }
64
- return 0;
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,18 +224,25 @@ 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 === 'hexagon'
163
- ? this.buildHexagonGeometry(w, depth, h)
164
- : this.buildBoxGeometry(w, depth, h);
165
- const material = new THREE.MeshStandardMaterial({
166
- color,
167
- metalness,
168
- roughness,
169
- opacity,
170
- transparent: opacity < 1,
171
- side: opacity < 1 ? THREE.DoubleSide : THREE.FrontSide,
172
- depthWrite: opacity >= 0.9,
173
- });
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
+ const material = isPlane
236
+ ? new THREE.MeshBasicMaterial({
237
+ color,
238
+ opacity,
239
+ transparent: opacity < 1,
240
+ side: THREE.DoubleSide,
241
+ })
242
+ : new THREE.MeshStandardMaterial(Object.assign({ color,
243
+ metalness,
244
+ roughness,
245
+ opacity, transparent: opacity < 1, side: opacity < 1 ? THREE.DoubleSide : THREE.FrontSide }, (this._config.emissiveIntensity ? { emissive: color, emissiveIntensity: this._config.emissiveIntensity } : {})));
174
246
  // z-fighting 방지: 바닥면(floor grid)보다 항상 앞에 렌더링
175
247
  material.polygonOffset = true;
176
248
  material.polygonOffsetFactor = -1;
@@ -195,11 +267,14 @@ class MCSRealObject3D extends RealObjectGroup {
195
267
  }
196
268
  }
197
269
  this._ownMesh = new THREE.Mesh(geometry, meshMaterial);
198
- // sizeRatio < 1이면 부모 중심에 배치 (rawDepth/2), 아니면 자체 중심 (depth/2)
199
- this._ownMesh.position.y = ratio < 1 ? rawDepth / 2 : depth / 2;
200
- this._ownMesh.castShadow = true;
270
+ // ownGroup 기준(bottom-up): mesh 바닥을 y=0에 맞추고 ownGroup이 -d/2로 shift해
271
+ // center-origin object3d에 정렬됨.
272
+ // box/hexagon (center-local geometry): y=rawDepth/2 (바닥을 0에 맞춤)
273
+ // plane: y=1 (바닥 살짝 띄움)
274
+ this._ownMesh.position.y = isPlane ? 1 : rawDepth / 2;
275
+ this._ownMesh.castShadow = !isPlane;
201
276
  this._ownMesh.receiveShadow = true;
202
- this.object3d.add(this._ownMesh);
277
+ this.components3D.add(this._ownMesh);
203
278
  }
204
279
  onchange(after, before) {
205
280
  if ('width' in after ||
@@ -283,7 +358,7 @@ class MCSRealObject3D extends RealObjectGroup {
283
358
  update() {
284
359
  // 자신의 mesh만 제거 (자식 컴포넌트 3D 오브젝트 보존)
285
360
  if (this._ownMesh) {
286
- this.object3d.remove(this._ownMesh);
361
+ this.components3D.remove(this._ownMesh);
287
362
  disposeObject3D(this._ownMesh);
288
363
  this._ownMesh = undefined;
289
364
  }
@@ -295,9 +370,9 @@ class MCSRealObject3D extends RealObjectGroup {
295
370
  // RealObjectGroup 기본 동작 비활성화
296
371
  updateDimension() { }
297
372
  updateAlpha() {
298
- const { alpha = 1 } = this.component.state;
299
- const configOpacity = this._config.opacity;
300
- const effective = configOpacity * alpha;
373
+ const state = this.component.state;
374
+ // alpha가 명시적으로 설정되어 있으면 그 값을 사용, 없으면 config 기본값 적용
375
+ const effective = ('alpha' in state) ? state.alpha : this._config.opacity;
301
376
  this.object3d.traverse((o) => {
302
377
  if (!o.material)
303
378
  return;
@@ -313,6 +388,19 @@ class MCSRealObject3D extends RealObjectGroup {
313
388
  });
314
389
  }
315
390
  }
391
+ // ── 헬퍼 ────────────────────────────────────────────────
392
+ /**
393
+ * Returns the first argument that is a finite number. NaN slips past `??`
394
+ * (which only catches null/undefined), so without this a property-panel
395
+ * input producing NaN would corrupt absSize and cascade through Math.*.
396
+ */
397
+ function pickFinite(...candidates) {
398
+ for (const c of candidates) {
399
+ if (typeof c === 'number' && Number.isFinite(c))
400
+ return c;
401
+ }
402
+ return 0;
403
+ }
316
404
  // ── 팩토리 등록 헬퍼 ─────────────────────────────────────
317
405
  function register(type, overrides = {}) {
318
406
  const config = Object.assign(Object.assign({}, DEFAULTS), overrides);
@@ -327,6 +415,7 @@ register('Equipment', {
327
415
  metalness: 0.4,
328
416
  roughness: 0.6,
329
417
  defaultColor: 0x8899aa,
418
+ zPriority: Z_FMSIM.EQUIPMENT,
330
419
  });
331
420
  register('Buffer', {
332
421
  defaultDepth: TRANSPORT_SURFACE_HEIGHT,
@@ -335,21 +424,96 @@ register('Buffer', {
335
424
  roughness: 0.7,
336
425
  defaultColor: 0x99aa88,
337
426
  shape: 'hexagon',
427
+ zPriority: Z_FMSIM.BUFFER,
338
428
  });
339
429
  register('AGVLine', {
340
- depthRatio: 0.12,
341
- metalness: 0.6,
342
- roughness: 0.3,
343
- defaultColor: 0x888899,
344
- });
345
- register('OHTLine', {
346
- depthRatio: 0.12,
430
+ depthRatio: 0,
431
+ defaultDepth: 0.1,
347
432
  metalness: 0.6,
348
433
  roughness: 0.3,
349
434
  defaultColor: 0x888899,
435
+ zPriority: Z_FMSIM.AGV_LINE,
350
436
  });
437
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
438
+ // OHTLine — 천장 I-beam 레일
439
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
440
+ const OHT_LINE_RAIL_COLOR = 0xcccccc;
441
+ const OHT_LINE_HEIGHT = 7 * TRANSPORT_SURFACE_HEIGHT; // OHT 상단과 일치
442
+ class OHTLineRealObject3D extends RealObjectGroup {
443
+ get zPriority() { return Z_FMSIM.OHT_LINE; }
444
+ get effectiveDepth() { return 0; }
445
+ get syncZPosOffset() { return OHT_LINE_HEIGHT; }
446
+ build() {
447
+ super.build();
448
+ const { width = 10, height = 10 } = this.component.state;
449
+ const w = Math.abs(width);
450
+ const h = Math.abs(height);
451
+ if (!w || !h)
452
+ return;
453
+ // I-beam 치수: 이송 방향 = 긴 변, 폭은 2D의 1/2
454
+ const isVertical = w < h;
455
+ const shortSide = isVertical ? w : h; // 2D 원본 짧은 쪽 폭
456
+ const beltW = shortSide * 0.25; // I-beam 폭: 원본의 1/4
457
+ const beamH = Math.max(beltW * 0.5, 3); // I-beam 전체 높이
458
+ const inset = shortSide * 0.5; // 양끝 여백: 원본 폭의 1/2
459
+ const railLen = (isVertical ? h : w) - inset * 2;
460
+ const flangeH = beamH * 0.2; // 플랜지 두께
461
+ const flangeW = beltW * 0.8; // 플랜지 폭
462
+ const webH = beamH - flangeH * 2; // 웹 높이
463
+ const webW = Math.max(flangeW * 0.25, 1); // 웹 두께
464
+ const mat = new THREE.MeshStandardMaterial({
465
+ color: OHT_LINE_RAIL_COLOR,
466
+ metalness: 0.5,
467
+ roughness: 0.4,
468
+ });
469
+ const geoms = [];
470
+ // 상부 플랜지
471
+ const topFlange = isVertical
472
+ ? new THREE.BoxGeometry(flangeW, flangeH, railLen)
473
+ : new THREE.BoxGeometry(railLen, flangeH, flangeW);
474
+ topFlange.translate(0, -flangeH / 2, 0);
475
+ geoms.push(topFlange);
476
+ // 웹 (수직판)
477
+ const web = isVertical
478
+ ? new THREE.BoxGeometry(webW, webH, railLen)
479
+ : new THREE.BoxGeometry(railLen, webH, webW);
480
+ web.translate(0, -flangeH - webH / 2, 0);
481
+ geoms.push(web);
482
+ // 하부 플랜지
483
+ const botFlange = isVertical
484
+ ? new THREE.BoxGeometry(flangeW, flangeH, railLen)
485
+ : new THREE.BoxGeometry(railLen, flangeH, flangeW);
486
+ botFlange.translate(0, -flangeH - webH - flangeH / 2, 0);
487
+ geoms.push(botFlange);
488
+ this._mesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(geoms), mat);
489
+ this._mesh.castShadow = true;
490
+ this.object3d.add(this._mesh);
491
+ }
492
+ update() {
493
+ if (this._mesh) {
494
+ this.object3d.remove(this._mesh);
495
+ this._mesh.geometry.dispose();
496
+ this._mesh.material.dispose();
497
+ this._mesh = undefined;
498
+ }
499
+ this.build();
500
+ this.updateTransform();
501
+ this.updateHidden();
502
+ }
503
+ onchange(after, before) {
504
+ if ('width' in after || 'height' in after || 'depth' in after ||
505
+ 'fillStyle' in after || 'strokeStyle' in after) {
506
+ this.update();
507
+ return;
508
+ }
509
+ super.onchange(after, before);
510
+ }
511
+ updateDimension() { }
512
+ updateAlpha() { }
513
+ }
514
+ registerRealObjectFactory('OHTLine', (component) => new OHTLineRealObject3D(component));
351
515
  // 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 });
516
+ 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
517
  class StockerRealObject3D extends MCSRealObject3D {
354
518
  constructor(component) {
355
519
  super(component, STOCKER_CONFIG);
@@ -362,7 +526,7 @@ class StockerRealObject3D extends MCSRealObject3D {
362
526
  var _a, _b;
363
527
  const comp = this.component;
364
528
  const data = comp.data || {};
365
- const gaugeMode = ((_b = (_a = comp.root) === null || _a === void 0 ? void 0 : _a.style) === null || _b === void 0 ? void 0 : _b.stockerGaugeMode) || 'capacity';
529
+ const gaugeMode = ((_a = comp.root) === null || _a === void 0 ? void 0 : _a.stockerGaugeMode) || 'capacity';
366
530
  const gauge = this.resolveGauge(data, gaugeMode);
367
531
  // 테두리는 스토커의 상태 색상 (machine status)
368
532
  const statusColor = comp.statusColor;
@@ -372,12 +536,18 @@ class StockerRealObject3D extends MCSRealObject3D {
372
536
  const { w, h } = this.absSize;
373
537
  const depth = this.effectiveDepth;
374
538
  const isWidthLonger = w >= h;
375
- // 게이지 캔버스 (고해상도)
376
- const barW = 130;
377
- const barH = 26;
539
+ // 게이지 최대 크기 (스토커가 충분히 크면 이 크기 그대로 사용).
540
+ const MAX_BAR_W = 130;
541
+ const MAX_BAR_H = 26;
542
+ // 스토커 지붕 footprint 에 맞춰 비례 축소 (확대는 안 함 — 현재 크기가 상한).
543
+ const fit = fitGaugeScale(w, h, MAX_BAR_W, MAX_BAR_H);
544
+ // PlaneGeometry 의 월드 크기는 clamp, 텍스처 캔버스 해상도는 최대 크기 기준 고정
545
+ // (축소되어도 선명도 유지).
546
+ const barW = MAX_BAR_W * fit;
547
+ const barH = MAX_BAR_H * fit;
378
548
  const scale = 4;
379
- const canvasW = barW * scale;
380
- const canvasH = barH * scale;
549
+ const canvasW = MAX_BAR_W * scale;
550
+ const canvasH = MAX_BAR_H * scale;
381
551
  const canvas = document.createElement('canvas');
382
552
  canvas.width = canvasW;
383
553
  canvas.height = canvasH;
@@ -389,9 +559,12 @@ class StockerRealObject3D extends MCSRealObject3D {
389
559
  ctx.strokeStyle = gauge.borderColor;
390
560
  ctx.lineWidth = 12;
391
561
  ctx.strokeRect(6, 6, canvasW - 12, canvasH - 12);
392
- // 세그먼트 채움 (왼→오)
562
+ // 세그먼트 채움 (왼→오) + carrier/abnormal 은 세그먼트별 숫자(%) 표기 (2D 와 동일)
393
563
  const innerW = canvasW - pad * 2;
394
564
  const innerH = canvasH - pad * 2;
565
+ // 컴포넌트 폰트 설정(fontFamily/bold/italic) 을 그대로, 크기만 텍스처 scale 배 (2D 와 상대 크기 일치).
566
+ const fontSize = (Number((_b = this.component.state) === null || _b === void 0 ? void 0 : _b.fontSize) || 15) * scale;
567
+ const font = this.gaugeFont(fontSize);
395
568
  let x = pad;
396
569
  for (const seg of gauge.segments) {
397
570
  const segW = Math.max(innerW * seg.ratio, seg.ratio > 0 ? 1 : 0);
@@ -399,14 +572,19 @@ class StockerRealObject3D extends MCSRealObject3D {
399
572
  continue;
400
573
  ctx.fillStyle = seg.color;
401
574
  ctx.fillRect(x, pad, segW, innerH);
575
+ if (gauge.perSegment) {
576
+ this.drawGaugeSegmentNumber(ctx, seg.ratio, x, pad, segW, innerH, font, fontSize);
577
+ }
402
578
  x += segW;
403
579
  }
404
- // 텍스트
405
- ctx.font = `bold ${10 * scale}px sans-serif`;
406
- ctx.textAlign = 'center';
407
- ctx.textBaseline = 'middle';
408
- ctx.fillStyle = '#333333';
409
- ctx.fillText(gauge.label, canvasW / 2, canvasH / 2);
580
+ // capacity: 전체 중앙에 usage% 한 개 (2D 와 동일). carrier/abnormal 은 위 세그먼트별 숫자 사용.
581
+ if (gauge.label) {
582
+ ctx.font = font;
583
+ ctx.textAlign = 'center';
584
+ ctx.textBaseline = 'middle';
585
+ ctx.fillStyle = '#333333';
586
+ ctx.fillText(gauge.label, canvasW / 2, canvasH / 2);
587
+ }
410
588
  // 텍스처 → Plane
411
589
  const texture = new THREE.CanvasTexture(canvas);
412
590
  texture.colorSpace = THREE.SRGBColorSpace;
@@ -423,61 +601,94 @@ class StockerRealObject3D extends MCSRealObject3D {
423
601
  mat.polygonOffsetFactor = -2;
424
602
  mat.polygonOffsetUnit = -2;
425
603
  this.gaugeMesh = new THREE.Mesh(geo, mat);
604
+ // ownGroup 기준(bottom-up): 볼륨 상단 = +depth. 그 위로 0.5 띄움.
426
605
  this.gaugeMesh.position.set(0, depth + 0.5, 0);
427
- this.object3d.add(this.gaugeMesh);
606
+ this.components3D.add(this.gaugeMesh);
428
607
  }
429
608
  resolveGauge(data, mode) {
430
- var _a, _b;
609
+ var _a;
431
610
  switch (mode) {
432
611
  case 'carrier': {
433
- const full = Number(data.FULL_CNT) || 0;
434
- const empty = Number(data.EMPTY_CNT) || 0;
435
- const emptyEmpty = Number(data.EMPTYEMPTY_CNT) || 0;
436
- const total = full + empty + emptyEmpty;
437
- const segments = total > 0
438
- ? [
439
- { ratio: full / total, color: '#4AA7FE' },
440
- { ratio: empty / total, color: '#8BDA5B' },
441
- { ratio: emptyEmpty / total, color: '#019D59' },
442
- ]
443
- : [{ ratio: 1, color: '#dddddd' }];
444
- return {
445
- segments,
446
- borderColor: '#4AA7FE',
447
- label: total > 0 ? `F${full} E${empty} EE${emptyEmpty}` : '',
448
- };
612
+ // 세그먼트 비율/색상은 2D 와 공유되는 단일 소스에서 계산 (필드/색상 drift 방지).
613
+ // 색상은 carrier 컴포넌트와 동일한 legend(테마)에서 취득.
614
+ const colors = carrierColorsFromLegend(resolveCarrierLegend(this.component));
615
+ const [f, e, ee] = carrierSegmentsFromData(data, colors);
616
+ const total = f.count + e.count + ee.count;
617
+ // 접두사 없는 세그먼트별 숫자(%)로 표기 (capacity·2D 와 동일). label(중앙 한 개) 미사용.
618
+ return total > 0
619
+ ? {
620
+ segments: [f, e, ee].map(s => ({ ratio: s.ratio, color: s.color })),
621
+ borderColor: colors.full,
622
+ label: null,
623
+ perSegment: true,
624
+ }
625
+ : { segments: [{ ratio: 1, color: '#dddddd' }], borderColor: colors.full, label: null, perSegment: false };
449
626
  }
450
627
  case 'abnormal': {
451
- const normal = Number(data.CARRIER_NORMAL) || 0;
452
- const abnormal = Number(data.CARRIER_ABNORMAL) || 0;
453
- const total = normal + abnormal;
454
- const segments = total > 0
455
- ? [
456
- { ratio: normal / total, color: '#8BDA5B' },
457
- { ratio: abnormal / total, color: '#F4B4B4' },
458
- ]
459
- : [{ ratio: 1, color: '#dddddd' }];
460
- return {
461
- segments,
462
- borderColor: abnormal > 0 ? '#F4B4B4' : '#8BDA5B',
463
- label: total > 0 ? `N${normal} A${abnormal}` : '',
464
- };
628
+ const [n, a] = abnormalSegmentsFromData(data);
629
+ const total = n.count + a.count;
630
+ return total > 0
631
+ ? {
632
+ segments: [n, a].map(s => ({ ratio: s.ratio, color: s.color })),
633
+ borderColor: a.count > 0 ? ABNORMAL_COLORS.abnormal : ABNORMAL_COLORS.normal,
634
+ label: null,
635
+ perSegment: true,
636
+ }
637
+ : {
638
+ segments: [{ ratio: 1, color: '#dddddd' }],
639
+ borderColor: ABNORMAL_COLORS.normal,
640
+ label: null,
641
+ perSegment: false,
642
+ };
465
643
  }
466
- default: { // capacity
644
+ default: { // capacity — 전체 중앙에 usage% 한 개
467
645
  const cur = Number(data.CURRENTCAPACITY) || 0;
468
646
  const max = Number(data.MAXCAPACITY) || 100;
469
647
  const fullup = data.FULLUP === 'T';
470
648
  const usage = Math.round((cur / max) * 100);
471
- const legend = ((_b = (_a = this.component.root) === null || _a === void 0 ? void 0 : _a.style) === null || _b === void 0 ? void 0 : _b.LEGEND_CAPACITY) || LEGEND_CAPACITY;
472
- const color = fullup ? 'red' : (getVaueOnRanges(usage, legend) || '#44aa44');
649
+ const legend = ((_a = this.component.root) === null || _a === void 0 ? void 0 : _a.stockerCapacityLegendTheme) || LEGEND_CAPACITY;
650
+ const color = fullup ? 'red' : (getValueOnRanges(usage, legend) || '#44aa44');
473
651
  return {
474
652
  segments: [{ ratio: usage / 100, color }],
475
653
  borderColor: color,
476
654
  label: `${usage}%`,
655
+ perSegment: false,
477
656
  };
478
657
  }
479
658
  }
480
659
  }
660
+ /** 컴포넌트 폰트 설정(fontFamily/bold/italic)으로 CSS font 문자열 조합. size 는 텍스처 스케일 반영값. */
661
+ gaugeFont(sizePx) {
662
+ const st = this.component.state || {};
663
+ const parts = [];
664
+ if (st.bold)
665
+ parts.push('bold');
666
+ if (st.italic)
667
+ parts.push('italic');
668
+ parts.push(`${sizePx}px`);
669
+ parts.push(st.fontFamily || 'sans-serif');
670
+ return parts.join(' ');
671
+ }
672
+ /** 세그먼트 영역 중심에 숫자(%)만 표기. 좁으면 값% → 값 → 생략 (2D _drawSegmentLabel 과 동일 정책). */
673
+ drawGaugeSegmentNumber(ctx, ratio, x, y, w, h, font, fontSize) {
674
+ ctx.font = font;
675
+ ctx.textAlign = 'center';
676
+ ctx.textBaseline = 'middle';
677
+ ctx.fillStyle = '#333333';
678
+ const value = `${Math.round(ratio * 100)}`;
679
+ const cx = x + w / 2;
680
+ const cy = y + h / 2;
681
+ if (fontSize <= h && ctx.measureText(`${value}%`).width <= w) {
682
+ ctx.fillText(`${value}%`, cx, cy);
683
+ }
684
+ else if (fontSize <= h && ctx.measureText(value).width <= w) {
685
+ ctx.fillText(value, cx, cy);
686
+ }
687
+ }
688
+ update() {
689
+ this.clearGauge();
690
+ super.update();
691
+ }
481
692
  onchange(after, before) {
482
693
  if ('data' in after) {
483
694
  this.clearGauge();
@@ -492,7 +703,7 @@ class StockerRealObject3D extends MCSRealObject3D {
492
703
  clearGauge() {
493
704
  var _a, _b;
494
705
  if (this.gaugeMesh) {
495
- this.object3d.remove(this.gaugeMesh);
706
+ this.components3D.remove(this.gaugeMesh);
496
707
  (_a = this.gaugeMesh.geometry) === null || _a === void 0 ? void 0 : _a.dispose();
497
708
  const mat = this.gaugeMesh.material;
498
709
  (_b = mat === null || mat === void 0 ? void 0 : mat.map) === null || _b === void 0 ? void 0 : _b.dispose();
@@ -505,123 +716,258 @@ registerRealObjectFactory('STOCKER', (component) => new StockerRealObject3D(comp
505
716
  // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
506
717
  // Unit (이동체 · 홀더) — 낮은 depth, 불투명
507
718
  // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
508
- // Port: 상부 개방 5면 박스
509
- const PORT_CONFIG = Object.assign(Object.assign({}, DEFAULTS), { depthRatio: 0.5, defaultZPosFromParent: true, opacity: 0.5, metalness: 0.4, roughness: 0.6, defaultColor: 0xb0a090, openTop: true });
719
+ // Port: 바닥판 (PlaneGeometry)
720
+ 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
721
  registerRealObjectFactory('Port', (component) => new MCSRealObject3D(component, PORT_CONFIG));
511
722
  register('Shelf', {
512
723
  depthRatio: 0.25,
513
724
  metalness: 0.3,
514
725
  roughness: 0.7,
515
726
  defaultColor: 0xaabbcc,
727
+ zPriority: Z_FMSIM.SHELF,
516
728
  });
517
729
  // Carrier: CARRIERSTATUS에 따른 3D 애니메이션 지원
518
- const CARRIER_CONFIG = Object.assign(Object.assign({}, DEFAULTS), { depthRatio: 0.5, metalness: 0.2, roughness: 0.8, defaultColor: 0x66bb66, isVisible: (c) => !!c.state.id, sizeRatio: 0.8, texturedTop: true });
730
+ 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 });
731
+ /**
732
+ * 적층 시 다음 carrier 의 z-position 이 *_이전 carrier 의 effectiveDepth × N*
733
+ * 만큼 위에 놓인다. N=1 이면 carrier 들이 맞붙어 적층 (기존), N<1 이면 부분
734
+ * overlap (3D box 들이 겹쳐서 적층). 사용자 의도 — carrier 두께는 두꺼워도
735
+ * 적층 시 *_compact 하게 보이도록* — N 을 작게.
736
+ */
737
+ const CARRIER_STACK_STEP_RATIO = 0.9;
519
738
  class CarrierRealObject3D extends MCSRealObject3D {
520
739
  constructor(component) {
521
740
  super(component, CARRIER_CONFIG);
522
- this._animRaf = null;
523
- this._animStartTime = 0;
524
- }
525
- build() {
526
- super.build();
527
- this.startCarrierAnimation();
528
741
  }
529
742
  /**
530
- * CARRIERSTATUS의 애니메이션 설정을 읽어 3D에서 직접 실행.
531
- * things-scene의 delta 시스템과 독립적으로 동작.
743
+ * cz override — *carrier 자기 결정* 의 적층 위치.
744
+ * parent.components 안 자기 index 까지의 형제 carrier 들의 effectiveDepth
745
+ * 누적합을 base cz 위에 더한다. holder 측에 별도 layout 책임을 두지 않고,
746
+ * carrier 가 자기 stack 위치를 알아서 결정.
532
747
  */
533
- startCarrierAnimation() {
534
- var _a, _b, _c;
535
- this.stopCarrierAnimation();
536
- const comp = this.component;
537
- if (!((_a = comp.app) === null || _a === void 0 ? void 0 : _a.isViewMode))
538
- return;
539
- const carrierStatus = (_b = comp.state) === null || _b === void 0 ? void 0 : _b.CARRIERSTATUS;
540
- if (!carrierStatus)
541
- return;
542
- // 애니메이션 설정 가져오기
543
- const config = (_c = comp.getAnimationConfig) === null || _c === void 0 ? void 0 : _c.call(comp, carrierStatus);
544
- if (!(config === null || config === void 0 ? void 0 : config.animation))
545
- return;
546
- const { type, duration = 2000, scale: animScale = 1.3 } = config.animation;
547
- if (!type)
748
+ get cz() {
749
+ var _a, _b;
750
+ const base = super.cz;
751
+ const parent = this.component.parent;
752
+ if (!(parent === null || parent === void 0 ? void 0 : parent.components))
753
+ return base;
754
+ const siblings = parent.components;
755
+ const idx = siblings.indexOf(this.component);
756
+ if (idx <= 0)
757
+ return base;
758
+ let offset = 0;
759
+ for (let i = 0; i < idx; i++) {
760
+ const ro = (_a = siblings[i]) === null || _a === void 0 ? void 0 : _a._realObject;
761
+ const eff = Number(ro === null || ro === void 0 ? void 0 : ro.effectiveDepth);
762
+ const d = Number.isFinite(eff) && eff > 0 ? eff : 1;
763
+ offset += d * CARRIER_STACK_STEP_RATIO;
764
+ }
765
+ // 적층 방향 — placement (= 부모 holder 타입) 에 따라 결정.
766
+ // OHT 처럼 그리퍼 아래에 매달리는 holder 는 carrier 가 *_아래로_* 누적,
767
+ // 그 외 (Port / Shelf / Shuttle / Vehicle) 는 위로 누적.
768
+ // MCSRealObject3D.syncZPosOffset 의 OHT 분기 (-effectiveDepth + halfD) 와
769
+ // 같은 방향성 일관 적용.
770
+ const parentType = (_b = parent === null || parent === void 0 ? void 0 : parent.state) === null || _b === void 0 ? void 0 : _b.type;
771
+ const dir = parentType === 'OHT' ? -1 : 1;
772
+ return base + dir * offset;
773
+ }
774
+ }
775
+ registerRealObjectFactory('Carrier', (component) => new CarrierRealObject3D(component));
776
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
777
+ // Gauge (용량 게이지) — 얇은 바
778
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
779
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
780
+ // Gauge (용량 게이지) — 2D 렌더링을 Plane 텍스처로 3D 표시
781
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
782
+ // Gauge 컴포넌트는 3D에서 표시하지 않음 (부모 STOCKER의 3D 모델에서 직접 게이지 렌더링)
783
+ // null 반환으로 3D 오브젝트 생성을 억제하고, default factory fallback도 방지
784
+ registerRealObjectFactory('StockerCapacityBar', () => undefined);
785
+ registerRealObjectFactory('MCSGaugeCapacityBar', () => undefined);
786
+ registerRealObjectFactory('ZoneCapacityBar', () => undefined);
787
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
788
+ // PortFlow — 2D render를 Plane 텍스처로 3D 표시
789
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
790
+ class PortFlowRealObject3D extends RealObjectGroup {
791
+ get zPriority() { return Z_FMSIM.PORT_FLOW; }
792
+ get effectiveDepth() { return 0; }
793
+ get syncZPosOffset() {
794
+ var _a;
795
+ // 부모 Port/Conveyor의 zPos와 동일하게
796
+ const parent = this.component.parentObject;
797
+ if (((_a = parent === null || parent === void 0 ? void 0 : parent.state) === null || _a === void 0 ? void 0 : _a.zPos) != null)
798
+ return parent.state.zPos;
799
+ return TRANSPORT_SURFACE_HEIGHT;
800
+ }
801
+ build() {
802
+ super.build();
803
+ const { width = 10, height = 10 } = this.component.state;
804
+ const w = Math.abs(width);
805
+ const h = Math.abs(height);
806
+ if (!w || !h)
548
807
  return;
549
- this._animStartTime = performance.now();
550
- // 애니메이션 대상: group(object3d) — mesh 개별 변환 대신 group 전체를 변환하여
551
- // 회전/스케일 중심이 component의 3D 위치(= 2D 중심)와 정확히 일치하도록 함
552
- const obj = this.object3d;
553
- const animate = () => {
554
- var _a;
555
- if (!this._ownMesh)
556
- return;
557
- const elapsed = performance.now() - this._animStartTime;
558
- const progress = (elapsed % duration) / duration; // 0~1 반복
559
- switch (type) {
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);
808
+ // 2D render를 캔버스에 캡처
809
+ // 화살표 날개 + 텍스트 영역이 bounds 밖으로 나가므로 전체 영역 계산
810
+ const { left, top } = this.component.bounds;
811
+ const { fontSize = 24, labelPosition = 'top' } = this.component.state;
812
+ const arrowPad = Math.min(w, h) * 0.4;
813
+ // 텍스트 영역 확장
814
+ let padLeft = arrowPad, padRight = arrowPad, padTop = arrowPad, padBottom = arrowPad;
815
+ if (this.component.hasTextProperty) {
816
+ switch (labelPosition) {
817
+ case 'top':
818
+ padTop = Math.max(padTop, fontSize + 4);
564
819
  break;
565
- }
566
- case 'rotation': {
567
- obj.rotation.y = progress * Math.PI * 2;
820
+ case 'bottom':
821
+ padBottom = Math.max(padBottom, fontSize + 4);
568
822
  break;
569
- }
570
- case 'vibration': {
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;
823
+ case 'left':
824
+ padLeft = Math.max(padLeft, fontSize * 4 + 4);
574
825
  break;
575
- }
576
- case 'fade': {
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
- });
826
+ case 'right':
827
+ padRight = Math.max(padRight, fontSize * 4 + 4);
584
828
  break;
585
- }
586
829
  }
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
830
  }
596
- }
597
- stopCarrierAnimation() {
598
- if (this._animRaf) {
599
- cancelAnimationFrame(this._animRaf);
600
- this._animRaf = null;
831
+ const totalW = w + padLeft + padRight;
832
+ const totalH = h + padTop + padBottom;
833
+ const scale = 2;
834
+ const canvas = document.createElement('canvas');
835
+ canvas.width = totalW * scale;
836
+ canvas.height = totalH * scale;
837
+ const ctx = canvas.getContext('2d');
838
+ ctx.save();
839
+ ctx.scale(scale, scale);
840
+ ctx.translate(-left + padLeft, -top + padTop);
841
+ try {
842
+ // 화살표 렌더
843
+ this.component.render(ctx);
844
+ ctx.fillStyle = this.component.state.strokeStyle || '#666666';
845
+ ctx.fill();
846
+ // 텍스트 렌더
847
+ if (this.component.hasTextProperty && this.component.drawText) {
848
+ ;
849
+ this.component.drawText(ctx);
850
+ }
601
851
  }
602
- // group 스케일/회전 초기화
603
- this.object3d.scale.set(1, 1, 1);
604
- this.object3d.rotation.y = 0;
852
+ catch ( /* render 실패 시 빈 텍스처 */_a) { /* render 실패 시 빈 텍스처 */ }
853
+ ctx.restore();
854
+ this._texture = new THREE.CanvasTexture(canvas);
855
+ this._texture.colorSpace = THREE.SRGBColorSpace;
856
+ const geo = new THREE.PlaneGeometry(totalW, totalH);
857
+ geo.rotateX(-Math.PI / 2);
858
+ const mat = new THREE.MeshBasicMaterial({
859
+ map: this._texture,
860
+ alphaTest: 0.1,
861
+ side: THREE.DoubleSide,
862
+ });
863
+ this._mesh = new THREE.Mesh(geo, mat);
864
+ // 비대칭 패딩 보정: 플레인 센터를 컴포넌트 bounds 센터에 맞춤
865
+ this._mesh.position.x = (padRight - padLeft) / 2;
866
+ this._mesh.position.y = 1;
867
+ this._mesh.position.z = (padBottom - padTop) / 2;
868
+ this.object3d.add(this._mesh);
605
869
  }
606
870
  update() {
607
- this.stopCarrierAnimation();
608
- super.update();
871
+ var _a;
872
+ if (this._mesh) {
873
+ this.object3d.remove(this._mesh);
874
+ this._mesh.geometry.dispose();
875
+ this._mesh.material.dispose();
876
+ (_a = this._texture) === null || _a === void 0 ? void 0 : _a.dispose();
877
+ this._mesh = undefined;
878
+ this._texture = undefined;
879
+ }
880
+ this.build();
881
+ this.updateTransform();
882
+ this.updateHidden();
609
883
  }
610
- clear() {
611
- this.stopCarrierAnimation();
612
- return super.clear();
884
+ onchange(after, before) {
885
+ if ('width' in after || 'height' in after || 'orientation' in after ||
886
+ 'direction' in after || 'strokeStyle' in after || 'INOUTTYPE' in after) {
887
+ this.update();
888
+ return;
889
+ }
890
+ super.onchange(after, before);
613
891
  }
892
+ updateDimension() { }
893
+ updateAlpha() { }
614
894
  }
615
- registerRealObjectFactory('Carrier', (component) => new CarrierRealObject3D(component));
895
+ registerRealObjectFactory('PortFlow', (component) => new PortFlowRealObject3D(component));
616
896
  // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
617
- // Gauge (용량 게이지) — 얇은 바
897
+ // Node — 2D render를 바닥 Plane 텍스처로 3D 표시
618
898
  // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
619
- // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
620
- // Gauge (용량 게이지) — 2D 렌더링을 Plane 텍스처로 3D 표시
621
- // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
622
- // Gauge 컴포넌트 (StockerCapacityBar, ZoneCapacityBar, MCSGaugeCapacityBar)는
623
- // 3D에서 표시하지 않음. 대신 부모(STOCKER 등)의 3D 모델에서 직접 게이지를 렌더링.
624
- registerRealObjectFactory('StockerCapacityBar', () => null);
625
- registerRealObjectFactory('MCSGaugeCapacityBar', () => null);
626
- registerRealObjectFactory('ZoneCapacityBar', () => null);
899
+ class NodeRealObject3D extends RealObjectGroup {
900
+ get zPriority() { return Z_FMSIM.NODE; }
901
+ get effectiveDepth() { return 0; }
902
+ get syncZPosOffset() { return 0; }
903
+ build() {
904
+ super.build();
905
+ const { cx, cy } = this.component.state;
906
+ const radius = 8;
907
+ const size = radius * 2;
908
+ const scale = 2;
909
+ const pad = 2;
910
+ const canvas = document.createElement('canvas');
911
+ canvas.width = (size + pad * 2) * scale;
912
+ canvas.height = (size + pad * 2) * scale;
913
+ const ctx = canvas.getContext('2d');
914
+ ctx.save();
915
+ ctx.scale(scale, scale);
916
+ ctx.translate(-cx + radius + pad, -cy + radius + pad);
917
+ try {
918
+ this.component.render(ctx);
919
+ const { fillStyle = '#888888' } = this.component.state;
920
+ ctx.fillStyle = typeof fillStyle === 'string' ? fillStyle : '#888888';
921
+ ctx.fill();
922
+ const { strokeStyle } = this.component.state;
923
+ if (strokeStyle && typeof strokeStyle === 'string') {
924
+ ctx.strokeStyle = strokeStyle;
925
+ ctx.lineWidth = 1;
926
+ ctx.stroke();
927
+ }
928
+ }
929
+ catch ( /* render 실패 시 빈 텍스처 */_a) { /* render 실패 시 빈 텍스처 */ }
930
+ ctx.restore();
931
+ this._texture = new THREE.CanvasTexture(canvas);
932
+ this._texture.colorSpace = THREE.SRGBColorSpace;
933
+ const geo = new THREE.PlaneGeometry(size + pad * 2, size + pad * 2);
934
+ geo.rotateX(-Math.PI / 2);
935
+ const mat = new THREE.MeshBasicMaterial({
936
+ map: this._texture,
937
+ alphaTest: 0.1,
938
+ side: THREE.DoubleSide,
939
+ });
940
+ this._mesh = new THREE.Mesh(geo, mat);
941
+ this._mesh.position.y = 1;
942
+ this.object3d.add(this._mesh);
943
+ }
944
+ update() {
945
+ var _a;
946
+ if (this._mesh) {
947
+ this.object3d.remove(this._mesh);
948
+ this._mesh.geometry.dispose();
949
+ this._mesh.material.dispose();
950
+ (_a = this._texture) === null || _a === void 0 ? void 0 : _a.dispose();
951
+ this._mesh = undefined;
952
+ this._texture = undefined;
953
+ }
954
+ this.build();
955
+ this.updateTransform();
956
+ this.updateHidden();
957
+ }
958
+ onchange(after, before) {
959
+ if ('fillStyle' in after || 'strokeStyle' in after) {
960
+ this.update();
961
+ return;
962
+ }
963
+ super.onchange(after, before);
964
+ }
965
+ updateDimension() { }
966
+ updateAlpha() { }
967
+ }
968
+ registerRealObjectFactory('Node', (component) => new NodeRealObject3D(component));
969
+ // Scanner — 2D Triangle 실루엣과 정합되는 삼각형 prism.
970
+ // 이송 표면 높이의 0.2배(=6) 얇은 기둥 — 원래 값 유지, shape 만 'box' → 'triangle'.
971
+ 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 });
972
+ registerRealObjectFactory('Scanner', (component) => new MCSRealObject3D(component, SCANNER_CONFIG));
627
973
  //# sourceMappingURL=machine-3d.js.map