@fmsim/machine 2.0.0-beta.5 → 2.0.0-beta.53

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 (81) hide show
  1. package/dist/agv.js +2 -2
  2. package/dist/agv.js.map +1 -1
  3. package/dist/carrier-manager.js +93 -0
  4. package/dist/carrier-manager.js.map +1 -0
  5. package/dist/carrier.js +51 -6
  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.map +1 -1
  12. package/dist/factories/agv-3d.js +38 -12
  13. package/dist/factories/agv-3d.js.map +1 -1
  14. package/dist/factories/conveyor-3d.js +166 -81
  15. package/dist/factories/conveyor-3d.js.map +1 -1
  16. package/dist/factories/crane-3d.js +5 -7
  17. package/dist/factories/crane-3d.js.map +1 -1
  18. package/dist/factories/crane-rail-3d.js +4 -6
  19. package/dist/factories/crane-rail-3d.js.map +1 -1
  20. package/dist/factories/machine-3d.js +464 -68
  21. package/dist/factories/machine-3d.js.map +1 -1
  22. package/dist/factories/oht-3d.js +52 -26
  23. package/dist/factories/oht-3d.js.map +1 -1
  24. package/dist/factories/shuttle-3d.js +32 -9
  25. package/dist/factories/shuttle-3d.js.map +1 -1
  26. package/dist/index.js +50 -34
  27. package/dist/index.js.map +1 -1
  28. package/dist/mcs-carrier-holder.js +39 -22
  29. package/dist/mcs-carrier-holder.js.map +1 -1
  30. package/dist/mcs-vehicle.js +8 -3
  31. package/dist/mcs-vehicle.js.map +1 -1
  32. package/dist/node.js +1 -1
  33. package/dist/node.js.map +1 -1
  34. package/dist/oht.js.map +1 -1
  35. package/dist/scanner.js +30 -0
  36. package/dist/scanner.js.map +1 -0
  37. package/dist/scene/root-container-override.js +9 -0
  38. package/dist/scene/root-container-override.js.map +1 -1
  39. package/dist/stocker-capacity-bar.js +2 -2
  40. package/dist/stocker-capacity-bar.js.map +1 -1
  41. package/dist/templates/index.js +2 -0
  42. package/dist/templates/index.js.map +1 -1
  43. package/dist/templates/scanner.js +20 -0
  44. package/dist/templates/scanner.js.map +1 -0
  45. package/dist/tsconfig.tsbuildinfo +1 -1
  46. package/dist/utils/get-value-on-ranges.js +2 -4
  47. package/dist/utils/get-value-on-ranges.js.map +1 -1
  48. package/dist/zone-capacity-bar.js +2 -2
  49. package/dist/zone-capacity-bar.js.map +1 -1
  50. package/jest.config.js +10 -0
  51. package/package.json +4 -3
  52. package/src/agv.ts +2 -2
  53. package/src/carrier-manager.ts +117 -0
  54. package/src/carrier.ts +50 -4
  55. package/src/conveyor-join.ts +5 -2
  56. package/src/conveyor.ts +8 -0
  57. package/src/crane.ts +1 -1
  58. package/src/factories/agv-3d.ts +38 -13
  59. package/src/factories/conveyor-3d.ts +168 -84
  60. package/src/factories/crane-3d.ts +6 -8
  61. package/src/factories/crane-rail-3d.ts +5 -7
  62. package/src/factories/machine-3d.ts +551 -62
  63. package/src/factories/oht-3d.ts +50 -26
  64. package/src/factories/shuttle-3d.ts +30 -10
  65. package/src/index.ts +56 -34
  66. package/src/mcs-carrier-holder.ts +38 -34
  67. package/src/mcs-vehicle.ts +9 -6
  68. package/src/node.ts +1 -1
  69. package/src/oht.ts +1 -1
  70. package/src/scanner.ts +29 -0
  71. package/src/scene/root-container-override.ts +12 -0
  72. package/src/stocker-capacity-bar.ts +2 -2
  73. package/src/templates/index.ts +2 -0
  74. package/src/templates/scanner.ts +20 -0
  75. package/src/utils/get-value-on-ranges.ts +2 -4
  76. package/src/zone-capacity-bar.ts +2 -2
  77. package/translations/en.json +2 -1
  78. package/translations/ja.json +2 -1
  79. package/translations/ko.json +2 -1
  80. package/translations/ms.json +2 -1
  81. package/translations/zh.json +2 -1
@@ -8,9 +8,10 @@
8
8
  * - Unit: 낮은 depth, 불투명
9
9
  */
10
10
  import * as THREE from 'three';
11
- import { RealObjectGroup, registerRealObjectFactory } from '@hatiolab/things-scene';
11
+ import * as BufferGeometryUtils from 'three/examples/jsm/utils/BufferGeometryUtils.js';
12
+ import { RealObjectGroup, registerRealObjectFactory, disposeObject3D } from '@hatiolab/things-scene';
12
13
  import { LEGEND_CAPACITY } from '../features/mcs-status-default';
13
- import { getVaueOnRanges } from '../utils/get-value-on-ranges';
14
+ import { getValueOnRanges } from '../utils/get-value-on-ranges';
14
15
  /**
15
16
  * 포트 기본 높이 (절대값).
16
17
  * 모든 이송장치의 상단 높이 = 이 값.
@@ -29,6 +30,8 @@ const DEFAULTS = {
29
30
  class MCSRealObject3D extends RealObjectGroup {
30
31
  constructor(component, config) {
31
32
  super(component);
33
+ // ── Change handling ──
34
+ this._updatePending = false;
32
35
  this._config = config;
33
36
  }
34
37
  // width, height 절대값 (음수 값은 양수로 취급)
@@ -47,20 +50,33 @@ class MCSRealObject3D extends RealObjectGroup {
47
50
  }
48
51
  get syncZPosOffset() {
49
52
  var _a, _b;
53
+ // syncZPosOffset = placement + geometric (center origin = effectiveDepth/2)
54
+ // 중첩 보정(parent shift 상쇄)은 base RealObject.cz가 parent.geometricOffsetY를
55
+ // 빼는 방식으로 처리하므로 여기선 placement + 본인 geometric만 반환
56
+ const halfD = this.effectiveDepth / 2;
57
+ // zPosOverride: 고정 높이 (OHTLine 등)
58
+ if (this._config.zPosOverride != null) {
59
+ return this._config.zPosOverride + halfD;
60
+ }
50
61
  // Port: 이송 표면 높이에 배치
51
62
  if (this._config.defaultZPosFromParent) {
52
- return TRANSPORT_SURFACE_HEIGHT;
63
+ return TRANSPORT_SURFACE_HEIGHT + halfD;
53
64
  }
54
- // Carrier: Port 안이면 중앙, 그 외 컨테이너면 상단 위
65
+ // Carrier: 부모 유형에 따라 배치 위치 결정
55
66
  if (this._config.sizeRatio && this._config.sizeRatio < 1) {
56
67
  const parentType = (_b = (_a = this.component.parent) === null || _a === void 0 ? void 0 : _a.state) === null || _b === void 0 ? void 0 : _b.type;
57
- if (parentType === 'Port' || parentType === 'Shelf' || parentType === 'OHT' || parentType === 'Shuttle') {
58
- return 0;
68
+ // OHT: 그리퍼 아래에 매달림 — placement = -effectiveDepth(carrier top이 OHT bottom)
69
+ if (parentType === 'OHT') {
70
+ return -this.effectiveDepth + halfD;
71
+ }
72
+ if (parentType === 'Port' || parentType === 'Shelf' || parentType === 'Shuttle') {
73
+ return halfD;
59
74
  }
60
- return TRANSPORT_SURFACE_HEIGHT;
75
+ return TRANSPORT_SURFACE_HEIGHT + halfD;
61
76
  }
62
- return 0;
77
+ return halfD;
63
78
  }
79
+ // geometric 부분(center origin: depth/2)은 base 기본값 사용 — override 불필요
64
80
  // 컴포넌트의 상태 색상(statusColor) 또는 fillStyle → Three.js 색상 변환
65
81
  resolveColor() {
66
82
  const comp = this.component;
@@ -68,7 +84,7 @@ class MCSRealObject3D extends RealObjectGroup {
68
84
  // '#F0F0F0'은 상태 미설정 시 기본 fallback — 무시하고 defaultColor 사용
69
85
  try {
70
86
  const sc = comp.statusColor;
71
- if (typeof sc === 'string' && sc && sc.toUpperCase() !== '#F0F0F0')
87
+ if (typeof sc === 'string' && sc && sc.toUpperCase() !== '#F0F0F0' && sc !== 'transparent')
72
88
  return new THREE.Color(sc).getHex();
73
89
  }
74
90
  catch (_a) {
@@ -76,7 +92,7 @@ class MCSRealObject3D extends RealObjectGroup {
76
92
  }
77
93
  // 2) state.fillStyle fallback
78
94
  const { fillStyle } = this.component.state;
79
- if (fillStyle && typeof fillStyle === 'string') {
95
+ if (fillStyle && typeof fillStyle === 'string' && fillStyle !== 'transparent') {
80
96
  try {
81
97
  return new THREE.Color(fillStyle).getHex();
82
98
  }
@@ -116,7 +132,7 @@ class MCSRealObject3D extends RealObjectGroup {
116
132
  // ── Geometry builders ──
117
133
  buildBoxGeometry(w, d, h) {
118
134
  const geo = new THREE.BoxGeometry(w, d, h);
119
- return this.removeBottomFace(geo);
135
+ return this._config.keepBottomFace ? geo : this.removeBottomFace(geo);
120
136
  }
121
137
  buildHexagonGeometry(w, d, h) {
122
138
  // Buffer의 2D 형태: 좌우 끝이 꺾인 육각형
@@ -141,7 +157,11 @@ class MCSRealObject3D extends RealObjectGroup {
141
157
  geo.rotateX(Math.PI / 2);
142
158
  return this.removeBottomFace(geo);
143
159
  }
144
- // ── Build ──
160
+ buildPlaneGeometry(w, h) {
161
+ const geo = new THREE.PlaneGeometry(w, h);
162
+ geo.rotateX(-Math.PI / 2);
163
+ return geo;
164
+ }
145
165
  build() {
146
166
  var _a;
147
167
  super.build();
@@ -158,46 +178,167 @@ class MCSRealObject3D extends RealObjectGroup {
158
178
  const depth = rawDepth * ratio;
159
179
  const color = this.resolveColor();
160
180
  const { opacity, metalness, roughness, shape } = this._config;
161
- const geometry = shape === 'hexagon'
162
- ? this.buildHexagonGeometry(w, depth, h)
163
- : this.buildBoxGeometry(w, depth, h);
164
- const material = new THREE.MeshStandardMaterial({
165
- color,
166
- metalness,
167
- roughness,
168
- opacity,
169
- transparent: opacity < 1,
170
- side: opacity < 1 ? THREE.DoubleSide : THREE.FrontSide,
171
- depthWrite: opacity >= 0.9,
172
- });
181
+ const geometry = shape === 'plane'
182
+ ? this.buildPlaneGeometry(w, h)
183
+ : shape === 'hexagon'
184
+ ? this.buildHexagonGeometry(w, depth, h)
185
+ : this.buildBoxGeometry(w, depth, h);
186
+ const isPlane = shape === 'plane';
187
+ const material = isPlane
188
+ ? new THREE.MeshBasicMaterial({
189
+ color,
190
+ opacity,
191
+ transparent: opacity < 1,
192
+ side: THREE.DoubleSide,
193
+ })
194
+ : new THREE.MeshStandardMaterial(Object.assign({ color,
195
+ metalness,
196
+ roughness,
197
+ opacity, transparent: opacity < 1, side: opacity < 1 ? THREE.DoubleSide : THREE.FrontSide }, (this._config.emissiveIntensity ? { emissive: color, emissiveIntensity: this._config.emissiveIntensity } : {})));
173
198
  // z-fighting 방지: 바닥면(floor grid)보다 항상 앞에 렌더링
174
199
  material.polygonOffset = true;
175
200
  material.polygonOffsetFactor = -1;
176
201
  material.polygonOffsetUnit = -1;
177
- const mesh = new THREE.Mesh(geometry, material);
178
- // sizeRatio < 1이면 부모 중심에 배치 (rawDepth/2), 아니면 자체 중심 (depth/2)
179
- mesh.position.y = ratio < 1 ? rawDepth / 2 : depth / 2;
180
- mesh.castShadow = true;
181
- mesh.receiveShadow = true;
182
- this.object3d.add(mesh);
183
- }
184
- // ── Change handling ──
202
+ // BoxGeometry material groups: +x(0), -x(1), +y(2), -y(3), +z(4), -z(5)
203
+ let meshMaterial = material;
204
+ if (shape === 'box') {
205
+ if (this._config.openTop) {
206
+ // 상단면 제거 → 5면 개방 박스
207
+ const invisible = new THREE.MeshBasicMaterial({ visible: false });
208
+ meshMaterial = [material, material, invisible, material, material, material];
209
+ }
210
+ else if (this._config.texturedTop) {
211
+ // 상판에 2D 렌더링 텍스처
212
+ const topMat = this.buildTopTexture(w, h);
213
+ meshMaterial = [material, material, topMat, material, material, material];
214
+ }
215
+ else if (this._config.matteTop) {
216
+ // 상판만 무반사
217
+ const matte = new THREE.MeshBasicMaterial({ color, opacity, transparent: opacity < 1 });
218
+ meshMaterial = [material, material, matte, material, material, material];
219
+ }
220
+ }
221
+ this._ownMesh = new THREE.Mesh(geometry, meshMaterial);
222
+ // ownGroup 기준(bottom-up): mesh 바닥을 y=0에 맞추고 ownGroup이 -d/2로 shift해
223
+ // center-origin object3d에 정렬됨.
224
+ // box/hexagon (center-local geometry): y=rawDepth/2 (바닥을 0에 맞춤)
225
+ // plane: y=1 (바닥 살짝 띄움)
226
+ this._ownMesh.position.y = isPlane ? 1 : rawDepth / 2;
227
+ this._ownMesh.castShadow = !isPlane;
228
+ this._ownMesh.receiveShadow = true;
229
+ this.components3D.add(this._ownMesh);
230
+ }
185
231
  onchange(after, before) {
186
232
  if ('width' in after ||
187
233
  'height' in after ||
188
234
  'depth' in after ||
189
235
  'fillStyle' in after ||
190
236
  'strokeStyle' in after ||
191
- 'id' in after // Carrier: CARRIERNAME 변경 시 가시성 재판정
192
- ) {
193
- this.update();
237
+ 'id' in after ||
238
+ 'EMPTYTYPE' in after ||
239
+ 'CARRIERSTATUS' in after) {
240
+ // 연속 setState에 의한 다중 rebuild 방지: 한 프레임에 한 번만 실행
241
+ if (!this._updatePending) {
242
+ this._updatePending = true;
243
+ queueMicrotask(() => {
244
+ this._updatePending = false;
245
+ this.update();
246
+ });
247
+ }
194
248
  return;
195
249
  }
196
250
  super.onchange(after, before);
197
251
  }
198
- // RealObjectGroup 기본 동작 비활성화 (update()에서 일괄 처리)
252
+ /**
253
+ * 상판 텍스처: 컴포넌트의 2D render()를 캔버스에 그려서 텍스처로 사용
254
+ */
255
+ buildTopTexture(w, h) {
256
+ var _a;
257
+ const scale = 4;
258
+ const cw = this.nextPow2(w * scale);
259
+ const ch = this.nextPow2(h * scale);
260
+ const canvas = document.createElement('canvas');
261
+ canvas.width = cw;
262
+ canvas.height = ch;
263
+ const ctx = canvas.getContext('2d');
264
+ const statusColor = '#' + this.resolveColor().toString(16).padStart(6, '0');
265
+ const emptyType = ((_a = this.component.state) === null || _a === void 0 ? void 0 : _a.EMPTYTYPE) || '';
266
+ const text = emptyType === 'FULL' ? 'F' : emptyType === 'EMPTY' ? 'E' : emptyType === 'EMPTYEMPTY' ? 'X' : '';
267
+ // 실제 사용 영역 (nextPow2 이전 크기)
268
+ const usedW = w * scale;
269
+ const usedH = h * scale;
270
+ // 바탕: 캐리어 상태 색상 (사용 영역만)
271
+ ctx.fillStyle = statusColor;
272
+ ctx.fillRect(0, 0, usedW, usedH);
273
+ // 텍스트 (사용 영역 중앙)
274
+ if (text) {
275
+ const fontSize = Math.round(Math.min(usedW, usedH) * 0.7);
276
+ ctx.font = `bold ${fontSize}px Arial`;
277
+ ctx.textAlign = 'center';
278
+ ctx.textBaseline = 'middle';
279
+ ctx.fillStyle = '#000000';
280
+ ctx.fillText(text, usedW / 2, usedH / 2);
281
+ }
282
+ const texture = new THREE.CanvasTexture(canvas);
283
+ texture.offset.set(0, 1 - usedH / ch);
284
+ texture.repeat.set(usedW / cw, usedH / ch);
285
+ texture.colorSpace = THREE.SRGBColorSpace;
286
+ return new THREE.MeshBasicMaterial({
287
+ map: texture,
288
+ transparent: true,
289
+ side: THREE.DoubleSide,
290
+ });
291
+ }
292
+ nextPow2(n) {
293
+ if (!(n > 0))
294
+ return 1;
295
+ let v = Math.ceil(n);
296
+ v--;
297
+ v |= v >> 1;
298
+ v |= v >> 2;
299
+ v |= v >> 4;
300
+ v |= v >> 8;
301
+ v |= v >> 16;
302
+ return v + 1;
303
+ }
304
+ /**
305
+ * 자신의 mesh만 제거 후 rebuild.
306
+ * 기본 update()는 clear()로 object3d의 모든 children을 제거하는데,
307
+ * 컨테이너(Port 등)의 경우 자식 컴포넌트(Carrier 등)의 3D 오브젝트도 함께 제거됨.
308
+ * 이를 방지하기 위해 자신이 생성한 mesh만 제거한다.
309
+ */
310
+ update() {
311
+ // 자신의 mesh만 제거 (자식 컴포넌트 3D 오브젝트 보존)
312
+ if (this._ownMesh) {
313
+ this.components3D.remove(this._ownMesh);
314
+ disposeObject3D(this._ownMesh);
315
+ this._ownMesh = undefined;
316
+ }
317
+ this.build();
318
+ this.updateTransform();
319
+ this.updateAlpha();
320
+ this.updateHidden();
321
+ }
322
+ // RealObjectGroup 기본 동작 비활성화
199
323
  updateDimension() { }
200
- updateAlpha() { }
324
+ updateAlpha() {
325
+ const state = this.component.state;
326
+ // alpha가 명시적으로 설정되어 있으면 그 값을 사용, 없으면 config 기본값 적용
327
+ const effective = ('alpha' in state) ? state.alpha : this._config.opacity;
328
+ this.object3d.traverse((o) => {
329
+ if (!o.material)
330
+ return;
331
+ const materials = Array.isArray(o.material) ? o.material : [o.material];
332
+ for (const m of materials) {
333
+ if (!m.visible)
334
+ continue; // openTop invisible face 무시
335
+ if (m.map)
336
+ continue; // 텍스처 material은 자체 transparent 유지
337
+ m.opacity = effective;
338
+ m.transparent = effective < 1;
339
+ }
340
+ });
341
+ }
201
342
  }
202
343
  // ── 팩토리 등록 헬퍼 ─────────────────────────────────────
203
344
  function register(type, overrides = {}) {
@@ -223,17 +364,89 @@ register('Buffer', {
223
364
  shape: 'hexagon',
224
365
  });
225
366
  register('AGVLine', {
226
- depthRatio: 0.12,
227
- metalness: 0.6,
228
- roughness: 0.3,
229
- defaultColor: 0x888899,
230
- });
231
- register('OHTLine', {
232
- depthRatio: 0.12,
367
+ depthRatio: 0,
368
+ defaultDepth: 0.1,
233
369
  metalness: 0.6,
234
370
  roughness: 0.3,
235
371
  defaultColor: 0x888899,
236
372
  });
373
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
374
+ // OHTLine — 천장 I-beam 레일
375
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
376
+ const OHT_LINE_RAIL_COLOR = 0xcccccc;
377
+ const OHT_LINE_HEIGHT = 7 * TRANSPORT_SURFACE_HEIGHT; // OHT 상단과 일치
378
+ class OHTLineRealObject3D extends RealObjectGroup {
379
+ get effectiveDepth() { return 0; }
380
+ get syncZPosOffset() { return OHT_LINE_HEIGHT; }
381
+ build() {
382
+ super.build();
383
+ const { width = 10, height = 10 } = this.component.state;
384
+ const w = Math.abs(width);
385
+ const h = Math.abs(height);
386
+ if (!w || !h)
387
+ return;
388
+ // I-beam 치수: 이송 방향 = 긴 변, 폭은 2D의 1/2
389
+ const isVertical = w < h;
390
+ const shortSide = isVertical ? w : h; // 2D 원본 짧은 쪽 폭
391
+ const beltW = shortSide * 0.25; // I-beam 폭: 원본의 1/4
392
+ const beamH = Math.max(beltW * 0.5, 3); // I-beam 전체 높이
393
+ const inset = shortSide * 0.5; // 양끝 여백: 원본 폭의 1/2
394
+ const railLen = (isVertical ? h : w) - inset * 2;
395
+ const flangeH = beamH * 0.2; // 플랜지 두께
396
+ const flangeW = beltW * 0.8; // 플랜지 폭
397
+ const webH = beamH - flangeH * 2; // 웹 높이
398
+ const webW = Math.max(flangeW * 0.25, 1); // 웹 두께
399
+ const mat = new THREE.MeshStandardMaterial({
400
+ color: OHT_LINE_RAIL_COLOR,
401
+ metalness: 0.5,
402
+ roughness: 0.4,
403
+ });
404
+ const geoms = [];
405
+ // 상부 플랜지
406
+ const topFlange = isVertical
407
+ ? new THREE.BoxGeometry(flangeW, flangeH, railLen)
408
+ : new THREE.BoxGeometry(railLen, flangeH, flangeW);
409
+ topFlange.translate(0, -flangeH / 2, 0);
410
+ geoms.push(topFlange);
411
+ // 웹 (수직판)
412
+ const web = isVertical
413
+ ? new THREE.BoxGeometry(webW, webH, railLen)
414
+ : new THREE.BoxGeometry(railLen, webH, webW);
415
+ web.translate(0, -flangeH - webH / 2, 0);
416
+ geoms.push(web);
417
+ // 하부 플랜지
418
+ const botFlange = isVertical
419
+ ? new THREE.BoxGeometry(flangeW, flangeH, railLen)
420
+ : new THREE.BoxGeometry(railLen, flangeH, flangeW);
421
+ botFlange.translate(0, -flangeH - webH - flangeH / 2, 0);
422
+ geoms.push(botFlange);
423
+ this._mesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(geoms), mat);
424
+ this._mesh.castShadow = true;
425
+ this.object3d.add(this._mesh);
426
+ }
427
+ update() {
428
+ if (this._mesh) {
429
+ this.object3d.remove(this._mesh);
430
+ this._mesh.geometry.dispose();
431
+ this._mesh.material.dispose();
432
+ this._mesh = undefined;
433
+ }
434
+ this.build();
435
+ this.updateTransform();
436
+ this.updateHidden();
437
+ }
438
+ onchange(after, before) {
439
+ if ('width' in after || 'height' in after || 'depth' in after ||
440
+ 'fillStyle' in after || 'strokeStyle' in after) {
441
+ this.update();
442
+ return;
443
+ }
444
+ super.onchange(after, before);
445
+ }
446
+ updateDimension() { }
447
+ updateAlpha() { }
448
+ }
449
+ registerRealObjectFactory('OHTLine', (component) => new OHTLineRealObject3D(component));
237
450
  // STOCKER: 벽면에 용량 게이지 바를 포함하는 커스텀 3D 오브젝트
238
451
  const STOCKER_CONFIG = Object.assign(Object.assign({}, DEFAULTS), { defaultDepth: 5 * TRANSPORT_SURFACE_HEIGHT, opacity: 0.75, metalness: 0.3, roughness: 0.6, defaultColor: 0x8899bb });
239
452
  class StockerRealObject3D extends MCSRealObject3D {
@@ -309,8 +522,9 @@ class StockerRealObject3D extends MCSRealObject3D {
309
522
  mat.polygonOffsetFactor = -2;
310
523
  mat.polygonOffsetUnit = -2;
311
524
  this.gaugeMesh = new THREE.Mesh(geo, mat);
525
+ // ownGroup 기준(bottom-up): 볼륨 상단 = +depth. 그 위로 0.5 띄움.
312
526
  this.gaugeMesh.position.set(0, depth + 0.5, 0);
313
- this.object3d.add(this.gaugeMesh);
527
+ this.components3D.add(this.gaugeMesh);
314
528
  }
315
529
  resolveGauge(data, mode) {
316
530
  var _a, _b;
@@ -355,7 +569,7 @@ class StockerRealObject3D extends MCSRealObject3D {
355
569
  const fullup = data.FULLUP === 'T';
356
570
  const usage = Math.round((cur / max) * 100);
357
571
  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;
358
- const color = fullup ? 'red' : (getVaueOnRanges(usage, legend) || '#44aa44');
572
+ const color = fullup ? 'red' : (getValueOnRanges(usage, legend) || '#44aa44');
359
573
  return {
360
574
  segments: [{ ratio: usage / 100, color }],
361
575
  borderColor: color,
@@ -364,6 +578,10 @@ class StockerRealObject3D extends MCSRealObject3D {
364
578
  }
365
579
  }
366
580
  }
581
+ update() {
582
+ this.clearGauge();
583
+ super.update();
584
+ }
367
585
  onchange(after, before) {
368
586
  if ('data' in after) {
369
587
  this.clearGauge();
@@ -378,7 +596,7 @@ class StockerRealObject3D extends MCSRealObject3D {
378
596
  clearGauge() {
379
597
  var _a, _b;
380
598
  if (this.gaugeMesh) {
381
- this.object3d.remove(this.gaugeMesh);
599
+ this.components3D.remove(this.gaugeMesh);
382
600
  (_a = this.gaugeMesh.geometry) === null || _a === void 0 ? void 0 : _a.dispose();
383
601
  const mat = this.gaugeMesh.material;
384
602
  (_b = mat === null || mat === void 0 ? void 0 : mat.map) === null || _b === void 0 ? void 0 : _b.dispose();
@@ -391,37 +609,215 @@ registerRealObjectFactory('STOCKER', (component) => new StockerRealObject3D(comp
391
609
  // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
392
610
  // Unit (이동체 · 홀더) — 낮은 depth, 불투명
393
611
  // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
394
- register('Port', {
395
- depthRatio: 0.5,
396
- defaultZPosFromParent: true,
397
- opacity: 0.5,
398
- metalness: 0.4,
399
- roughness: 0.6,
400
- defaultColor: 0xb0a090,
401
- });
612
+ // Port: 바닥판 (PlaneGeometry)
613
+ 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' });
614
+ registerRealObjectFactory('Port', (component) => new MCSRealObject3D(component, PORT_CONFIG));
402
615
  register('Shelf', {
403
616
  depthRatio: 0.25,
404
617
  metalness: 0.3,
405
618
  roughness: 0.7,
406
619
  defaultColor: 0xaabbcc,
407
620
  });
408
- register('Carrier', {
409
- depthRatio: 0.5,
410
- metalness: 0.2,
411
- roughness: 0.8,
412
- defaultColor: 0x66bb66,
413
- isVisible: (c) => !!c.state.id,
414
- sizeRatio: 0.8,
415
- });
621
+ // Carrier: CARRIERSTATUS에 따른 3D 애니메이션 지원
622
+ 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 });
623
+ class CarrierRealObject3D extends MCSRealObject3D {
624
+ constructor(component) {
625
+ super(component, CARRIER_CONFIG);
626
+ }
627
+ }
628
+ registerRealObjectFactory('Carrier', (component) => new CarrierRealObject3D(component));
416
629
  // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
417
630
  // Gauge (용량 게이지) — 얇은 바
418
631
  // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
419
632
  // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
420
633
  // Gauge (용량 게이지) — 2D 렌더링을 Plane 텍스처로 3D 표시
421
634
  // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
422
- // Gauge 컴포넌트 (StockerCapacityBar, ZoneCapacityBar, MCSGaugeCapacityBar)는
423
- // 3D에서 표시하지 않음. 대신 부모(STOCKER 등)의 3D 모델에서 직접 게이지를 렌더링.
424
- registerRealObjectFactory('StockerCapacityBar', () => null);
425
- registerRealObjectFactory('MCSGaugeCapacityBar', () => null);
426
- registerRealObjectFactory('ZoneCapacityBar', () => null);
635
+ // Gauge 컴포넌트는 3D에서 표시하지 않음 (부모 STOCKER의 3D 모델에서 직접 게이지 렌더링)
636
+ // null 반환으로 3D 오브젝트 생성을 억제하고, default factory fallback도 방지
637
+ registerRealObjectFactory('StockerCapacityBar', () => undefined);
638
+ registerRealObjectFactory('MCSGaugeCapacityBar', () => undefined);
639
+ registerRealObjectFactory('ZoneCapacityBar', () => undefined);
640
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
641
+ // PortFlow — 2D render를 Plane 텍스처로 3D 표시
642
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
643
+ class PortFlowRealObject3D extends RealObjectGroup {
644
+ get effectiveDepth() { return 0; }
645
+ get syncZPosOffset() {
646
+ var _a;
647
+ // 부모 Port/Conveyor의 zPos와 동일하게
648
+ const parent = this.component.parentObject;
649
+ if (((_a = parent === null || parent === void 0 ? void 0 : parent.state) === null || _a === void 0 ? void 0 : _a.zPos) != null)
650
+ return parent.state.zPos;
651
+ return TRANSPORT_SURFACE_HEIGHT;
652
+ }
653
+ build() {
654
+ super.build();
655
+ const { width = 10, height = 10 } = this.component.state;
656
+ const w = Math.abs(width);
657
+ const h = Math.abs(height);
658
+ if (!w || !h)
659
+ return;
660
+ // 2D render를 캔버스에 캡처
661
+ // 화살표 날개 + 텍스트 영역이 bounds 밖으로 나가므로 전체 영역 계산
662
+ const { left, top } = this.component.bounds;
663
+ const { fontSize = 24, labelPosition = 'top' } = this.component.state;
664
+ const arrowPad = Math.min(w, h) * 0.4;
665
+ // 텍스트 영역 확장
666
+ let padLeft = arrowPad, padRight = arrowPad, padTop = arrowPad, padBottom = arrowPad;
667
+ if (this.component.hasTextProperty) {
668
+ switch (labelPosition) {
669
+ case 'top':
670
+ padTop = Math.max(padTop, fontSize + 4);
671
+ break;
672
+ case 'bottom':
673
+ padBottom = Math.max(padBottom, fontSize + 4);
674
+ break;
675
+ case 'left':
676
+ padLeft = Math.max(padLeft, fontSize * 4 + 4);
677
+ break;
678
+ case 'right':
679
+ padRight = Math.max(padRight, fontSize * 4 + 4);
680
+ break;
681
+ }
682
+ }
683
+ const totalW = w + padLeft + padRight;
684
+ const totalH = h + padTop + padBottom;
685
+ const scale = 2;
686
+ const canvas = document.createElement('canvas');
687
+ canvas.width = totalW * scale;
688
+ canvas.height = totalH * scale;
689
+ const ctx = canvas.getContext('2d');
690
+ ctx.save();
691
+ ctx.scale(scale, scale);
692
+ ctx.translate(-left + padLeft, -top + padTop);
693
+ try {
694
+ // 화살표 렌더
695
+ this.component.render(ctx);
696
+ ctx.fillStyle = this.component.state.strokeStyle || '#666666';
697
+ ctx.fill();
698
+ // 텍스트 렌더
699
+ if (this.component.hasTextProperty && this.component.drawText) {
700
+ ;
701
+ this.component.drawText(ctx);
702
+ }
703
+ }
704
+ catch ( /* render 실패 시 빈 텍스처 */_a) { /* render 실패 시 빈 텍스처 */ }
705
+ ctx.restore();
706
+ this._texture = new THREE.CanvasTexture(canvas);
707
+ this._texture.colorSpace = THREE.SRGBColorSpace;
708
+ const geo = new THREE.PlaneGeometry(totalW, totalH);
709
+ geo.rotateX(-Math.PI / 2);
710
+ const mat = new THREE.MeshBasicMaterial({
711
+ map: this._texture,
712
+ alphaTest: 0.1,
713
+ side: THREE.DoubleSide,
714
+ });
715
+ this._mesh = new THREE.Mesh(geo, mat);
716
+ // 비대칭 패딩 보정: 플레인 센터를 컴포넌트 bounds 센터에 맞춤
717
+ this._mesh.position.x = (padRight - padLeft) / 2;
718
+ this._mesh.position.y = 1;
719
+ this._mesh.position.z = (padBottom - padTop) / 2;
720
+ this.object3d.add(this._mesh);
721
+ }
722
+ update() {
723
+ var _a;
724
+ if (this._mesh) {
725
+ this.object3d.remove(this._mesh);
726
+ this._mesh.geometry.dispose();
727
+ this._mesh.material.dispose();
728
+ (_a = this._texture) === null || _a === void 0 ? void 0 : _a.dispose();
729
+ this._mesh = undefined;
730
+ this._texture = undefined;
731
+ }
732
+ this.build();
733
+ this.updateTransform();
734
+ this.updateHidden();
735
+ }
736
+ onchange(after, before) {
737
+ if ('width' in after || 'height' in after || 'orientation' in after ||
738
+ 'direction' in after || 'strokeStyle' in after || 'INOUTTYPE' in after) {
739
+ this.update();
740
+ return;
741
+ }
742
+ super.onchange(after, before);
743
+ }
744
+ updateDimension() { }
745
+ updateAlpha() { }
746
+ }
747
+ registerRealObjectFactory('PortFlow', (component) => new PortFlowRealObject3D(component));
748
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
749
+ // Node — 2D render를 바닥 Plane 텍스처로 3D 표시
750
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
751
+ class NodeRealObject3D extends RealObjectGroup {
752
+ get effectiveDepth() { return 0; }
753
+ get syncZPosOffset() { return 0; }
754
+ build() {
755
+ super.build();
756
+ const { cx, cy } = this.component.state;
757
+ const radius = 8;
758
+ const size = radius * 2;
759
+ const scale = 2;
760
+ const pad = 2;
761
+ const canvas = document.createElement('canvas');
762
+ canvas.width = (size + pad * 2) * scale;
763
+ canvas.height = (size + pad * 2) * scale;
764
+ const ctx = canvas.getContext('2d');
765
+ ctx.save();
766
+ ctx.scale(scale, scale);
767
+ ctx.translate(-cx + radius + pad, -cy + radius + pad);
768
+ try {
769
+ this.component.render(ctx);
770
+ const { fillStyle = '#888888' } = this.component.state;
771
+ ctx.fillStyle = typeof fillStyle === 'string' ? fillStyle : '#888888';
772
+ ctx.fill();
773
+ const { strokeStyle } = this.component.state;
774
+ if (strokeStyle && typeof strokeStyle === 'string') {
775
+ ctx.strokeStyle = strokeStyle;
776
+ ctx.lineWidth = 1;
777
+ ctx.stroke();
778
+ }
779
+ }
780
+ catch ( /* render 실패 시 빈 텍스처 */_a) { /* render 실패 시 빈 텍스처 */ }
781
+ ctx.restore();
782
+ this._texture = new THREE.CanvasTexture(canvas);
783
+ this._texture.colorSpace = THREE.SRGBColorSpace;
784
+ const geo = new THREE.PlaneGeometry(size + pad * 2, size + pad * 2);
785
+ geo.rotateX(-Math.PI / 2);
786
+ const mat = new THREE.MeshBasicMaterial({
787
+ map: this._texture,
788
+ alphaTest: 0.1,
789
+ side: THREE.DoubleSide,
790
+ });
791
+ this._mesh = new THREE.Mesh(geo, mat);
792
+ this._mesh.position.y = 1;
793
+ this.object3d.add(this._mesh);
794
+ }
795
+ update() {
796
+ var _a;
797
+ if (this._mesh) {
798
+ this.object3d.remove(this._mesh);
799
+ this._mesh.geometry.dispose();
800
+ this._mesh.material.dispose();
801
+ (_a = this._texture) === null || _a === void 0 ? void 0 : _a.dispose();
802
+ this._mesh = undefined;
803
+ this._texture = undefined;
804
+ }
805
+ this.build();
806
+ this.updateTransform();
807
+ this.updateHidden();
808
+ }
809
+ onchange(after, before) {
810
+ if ('fillStyle' in after || 'strokeStyle' in after) {
811
+ this.update();
812
+ return;
813
+ }
814
+ super.onchange(after, before);
815
+ }
816
+ updateDimension() { }
817
+ updateAlpha() { }
818
+ }
819
+ registerRealObjectFactory('Node', (component) => new NodeRealObject3D(component));
820
+ // Scanner — 이송 표면 높이의 기둥 형태
821
+ 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: 'box' });
822
+ registerRealObjectFactory('Scanner', (component) => new MCSRealObject3D(component, SCANNER_CONFIG));
427
823
  //# sourceMappingURL=machine-3d.js.map