@fmsim/machine 2.0.0-beta.6 → 2.0.0-beta.60

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 +49 -4
  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 +8 -9
  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 +363 -132
  21. package/dist/factories/machine-3d.js.map +1 -1
  22. package/dist/factories/oht-3d.js +20 -20
  23. package/dist/factories/oht-3d.js.map +1 -1
  24. package/dist/factories/shuttle-3d.js +2 -5
  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 +48 -2
  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 +9 -10
  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 +428 -144
  63. package/src/factories/oht-3d.ts +20 -20
  64. package/src/factories/shuttle-3d.ts +2 -6
  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 * as BufferGeometryUtils from 'three/examples/jsm/utils/BufferGeometryUtils.js';
11
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
  * 모든 이송장치의 상단 높이 = 이 값.
@@ -33,9 +34,15 @@ class MCSRealObject3D extends RealObjectGroup {
33
34
  this._updatePending = false;
34
35
  this._config = config;
35
36
  }
36
- // width, height 절대값 (음수 값은 양수로 취급)
37
+ // width, height 절대값 (음수 값은 양수로 취급).
38
+ // `resizable:false` 컴포넌트(예: Scanner)는 state.width/height 가 없고
39
+ // bounds 만 있을 수 있으므로 bounds 를 fallback 으로 사용.
37
40
  get absSize() {
38
- const { width = 10, height = 10 } = this.component.state;
41
+ var _a, _b, _c, _d;
42
+ const state = (this.component.state || {});
43
+ const bounds = (this.component.bounds || {});
44
+ const width = (_b = (_a = state.width) !== null && _a !== void 0 ? _a : bounds.width) !== null && _b !== void 0 ? _b : 10;
45
+ const height = (_d = (_c = state.height) !== null && _c !== void 0 ? _c : bounds.height) !== null && _d !== void 0 ? _d : 10;
39
46
  return { w: Math.abs(width), h: Math.abs(height) };
40
47
  }
41
48
  get effectiveDepth() {
@@ -49,20 +56,33 @@ class MCSRealObject3D extends RealObjectGroup {
49
56
  }
50
57
  get syncZPosOffset() {
51
58
  var _a, _b;
59
+ // syncZPosOffset = placement + geometric (center origin = effectiveDepth/2)
60
+ // 중첩 보정(parent shift 상쇄)은 base RealObject.cz가 parent.geometricOffsetY를
61
+ // 빼는 방식으로 처리하므로 여기선 placement + 본인 geometric만 반환
62
+ const halfD = this.effectiveDepth / 2;
63
+ // zPosOverride: 고정 높이 (OHTLine 등)
64
+ if (this._config.zPosOverride != null) {
65
+ return this._config.zPosOverride + halfD;
66
+ }
52
67
  // Port: 이송 표면 높이에 배치
53
68
  if (this._config.defaultZPosFromParent) {
54
- return TRANSPORT_SURFACE_HEIGHT;
69
+ return TRANSPORT_SURFACE_HEIGHT + halfD;
55
70
  }
56
- // Carrier: Port 안이면 중앙, 그 외 컨테이너면 상단 위
71
+ // Carrier: 부모 유형에 따라 배치 위치 결정
57
72
  if (this._config.sizeRatio && this._config.sizeRatio < 1) {
58
73
  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;
74
+ // OHT: 그리퍼 아래에 매달림 — placement = -effectiveDepth(carrier top이 OHT bottom)
75
+ if (parentType === 'OHT') {
76
+ return -this.effectiveDepth + halfD;
61
77
  }
62
- return TRANSPORT_SURFACE_HEIGHT;
78
+ if (parentType === 'Port' || parentType === 'Shelf' || parentType === 'Shuttle') {
79
+ return halfD;
80
+ }
81
+ return TRANSPORT_SURFACE_HEIGHT + halfD;
63
82
  }
64
- return 0;
83
+ return halfD;
65
84
  }
85
+ // geometric 부분(center origin: depth/2)은 base 기본값 사용 — override 불필요
66
86
  // 컴포넌트의 상태 색상(statusColor) 또는 fillStyle → Three.js 색상 변환
67
87
  resolveColor() {
68
88
  const comp = this.component;
@@ -70,7 +90,7 @@ class MCSRealObject3D extends RealObjectGroup {
70
90
  // '#F0F0F0'은 상태 미설정 시 기본 fallback — 무시하고 defaultColor 사용
71
91
  try {
72
92
  const sc = comp.statusColor;
73
- if (typeof sc === 'string' && sc && sc.toUpperCase() !== '#F0F0F0')
93
+ if (typeof sc === 'string' && sc && sc.toUpperCase() !== '#F0F0F0' && sc !== 'transparent')
74
94
  return new THREE.Color(sc).getHex();
75
95
  }
76
96
  catch (_a) {
@@ -78,7 +98,7 @@ class MCSRealObject3D extends RealObjectGroup {
78
98
  }
79
99
  // 2) state.fillStyle fallback
80
100
  const { fillStyle } = this.component.state;
81
- if (fillStyle && typeof fillStyle === 'string') {
101
+ if (fillStyle && typeof fillStyle === 'string' && fillStyle !== 'transparent') {
82
102
  try {
83
103
  return new THREE.Color(fillStyle).getHex();
84
104
  }
@@ -118,7 +138,7 @@ class MCSRealObject3D extends RealObjectGroup {
118
138
  // ── Geometry builders ──
119
139
  buildBoxGeometry(w, d, h) {
120
140
  const geo = new THREE.BoxGeometry(w, d, h);
121
- return this.removeBottomFace(geo);
141
+ return this._config.keepBottomFace ? geo : this.removeBottomFace(geo);
122
142
  }
123
143
  buildHexagonGeometry(w, d, h) {
124
144
  // Buffer의 2D 형태: 좌우 끝이 꺾인 육각형
@@ -143,6 +163,37 @@ class MCSRealObject3D extends RealObjectGroup {
143
163
  geo.rotateX(Math.PI / 2);
144
164
  return this.removeBottomFace(geo);
145
165
  }
166
+ buildPlaneGeometry(w, h) {
167
+ const geo = new THREE.PlaneGeometry(w, h);
168
+ geo.rotateX(-Math.PI / 2);
169
+ return geo;
170
+ }
171
+ /**
172
+ * 삼각형 prism — 2D Triangle 의 꼭짓점(state.x1/y1, x2/y2, x3/y3) 을 그대로 사용.
173
+ * things-scene 의 TriangleExtrude 와 동일한 방식 — 각 Scanner 인스턴스의
174
+ * 실제 정점 배치(방향, 비대칭 등) 가 3D 에 반영된다.
175
+ *
176
+ * `geometry.center()` 로 로컬 원점을 bounding-box 중심으로 맞추므로
177
+ * 후속 placement 와 정합.
178
+ */
179
+ buildTriangleGeometry(d) {
180
+ const state = (this.component.state || {});
181
+ const { x1 = 0, y1 = 0, x2 = 0, y2 = 0, x3 = 0, y3 = 0 } = state;
182
+ const shape = new THREE.Shape();
183
+ shape.moveTo(x1, y1);
184
+ shape.lineTo(x2, y2);
185
+ shape.lineTo(x3, y3);
186
+ shape.closePath();
187
+ const geo = new THREE.ExtrudeGeometry(shape, {
188
+ steps: 1,
189
+ depth: d,
190
+ bevelEnabled: false,
191
+ });
192
+ // bounding box 중심으로 원점 정렬 후 Y-up 좌표계로 회전
193
+ geo.center();
194
+ geo.rotateX(Math.PI / 2);
195
+ return this.removeBottomFace(geo);
196
+ }
146
197
  build() {
147
198
  var _a;
148
199
  super.build();
@@ -159,18 +210,25 @@ class MCSRealObject3D extends RealObjectGroup {
159
210
  const depth = rawDepth * ratio;
160
211
  const color = this.resolveColor();
161
212
  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
- });
213
+ const geometry = shape === 'plane'
214
+ ? this.buildPlaneGeometry(w, h)
215
+ : shape === 'hexagon'
216
+ ? this.buildHexagonGeometry(w, depth, h)
217
+ : shape === 'triangle'
218
+ ? this.buildTriangleGeometry(depth)
219
+ : this.buildBoxGeometry(w, depth, h);
220
+ const isPlane = shape === 'plane';
221
+ const material = isPlane
222
+ ? new THREE.MeshBasicMaterial({
223
+ color,
224
+ opacity,
225
+ transparent: opacity < 1,
226
+ side: THREE.DoubleSide,
227
+ })
228
+ : new THREE.MeshStandardMaterial(Object.assign({ color,
229
+ metalness,
230
+ roughness,
231
+ opacity, transparent: opacity < 1, side: opacity < 1 ? THREE.DoubleSide : THREE.FrontSide }, (this._config.emissiveIntensity ? { emissive: color, emissiveIntensity: this._config.emissiveIntensity } : {})));
174
232
  // z-fighting 방지: 바닥면(floor grid)보다 항상 앞에 렌더링
175
233
  material.polygonOffset = true;
176
234
  material.polygonOffsetFactor = -1;
@@ -195,11 +253,14 @@ class MCSRealObject3D extends RealObjectGroup {
195
253
  }
196
254
  }
197
255
  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;
256
+ // ownGroup 기준(bottom-up): mesh 바닥을 y=0에 맞추고 ownGroup이 -d/2로 shift해
257
+ // center-origin object3d에 정렬됨.
258
+ // box/hexagon (center-local geometry): y=rawDepth/2 (바닥을 0에 맞춤)
259
+ // plane: y=1 (바닥 살짝 띄움)
260
+ this._ownMesh.position.y = isPlane ? 1 : rawDepth / 2;
261
+ this._ownMesh.castShadow = !isPlane;
201
262
  this._ownMesh.receiveShadow = true;
202
- this.object3d.add(this._ownMesh);
263
+ this.components3D.add(this._ownMesh);
203
264
  }
204
265
  onchange(after, before) {
205
266
  if ('width' in after ||
@@ -283,7 +344,7 @@ class MCSRealObject3D extends RealObjectGroup {
283
344
  update() {
284
345
  // 자신의 mesh만 제거 (자식 컴포넌트 3D 오브젝트 보존)
285
346
  if (this._ownMesh) {
286
- this.object3d.remove(this._ownMesh);
347
+ this.components3D.remove(this._ownMesh);
287
348
  disposeObject3D(this._ownMesh);
288
349
  this._ownMesh = undefined;
289
350
  }
@@ -295,9 +356,9 @@ class MCSRealObject3D extends RealObjectGroup {
295
356
  // RealObjectGroup 기본 동작 비활성화
296
357
  updateDimension() { }
297
358
  updateAlpha() {
298
- const { alpha = 1 } = this.component.state;
299
- const configOpacity = this._config.opacity;
300
- const effective = configOpacity * alpha;
359
+ const state = this.component.state;
360
+ // alpha가 명시적으로 설정되어 있으면 그 값을 사용, 없으면 config 기본값 적용
361
+ const effective = ('alpha' in state) ? state.alpha : this._config.opacity;
301
362
  this.object3d.traverse((o) => {
302
363
  if (!o.material)
303
364
  return;
@@ -337,17 +398,89 @@ register('Buffer', {
337
398
  shape: 'hexagon',
338
399
  });
339
400
  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,
401
+ depthRatio: 0,
402
+ defaultDepth: 0.1,
347
403
  metalness: 0.6,
348
404
  roughness: 0.3,
349
405
  defaultColor: 0x888899,
350
406
  });
407
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
408
+ // OHTLine — 천장 I-beam 레일
409
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
410
+ const OHT_LINE_RAIL_COLOR = 0xcccccc;
411
+ const OHT_LINE_HEIGHT = 7 * TRANSPORT_SURFACE_HEIGHT; // OHT 상단과 일치
412
+ class OHTLineRealObject3D extends RealObjectGroup {
413
+ get effectiveDepth() { return 0; }
414
+ get syncZPosOffset() { return OHT_LINE_HEIGHT; }
415
+ build() {
416
+ super.build();
417
+ const { width = 10, height = 10 } = this.component.state;
418
+ const w = Math.abs(width);
419
+ const h = Math.abs(height);
420
+ if (!w || !h)
421
+ return;
422
+ // I-beam 치수: 이송 방향 = 긴 변, 폭은 2D의 1/2
423
+ const isVertical = w < h;
424
+ const shortSide = isVertical ? w : h; // 2D 원본 짧은 쪽 폭
425
+ const beltW = shortSide * 0.25; // I-beam 폭: 원본의 1/4
426
+ const beamH = Math.max(beltW * 0.5, 3); // I-beam 전체 높이
427
+ const inset = shortSide * 0.5; // 양끝 여백: 원본 폭의 1/2
428
+ const railLen = (isVertical ? h : w) - inset * 2;
429
+ const flangeH = beamH * 0.2; // 플랜지 두께
430
+ const flangeW = beltW * 0.8; // 플랜지 폭
431
+ const webH = beamH - flangeH * 2; // 웹 높이
432
+ const webW = Math.max(flangeW * 0.25, 1); // 웹 두께
433
+ const mat = new THREE.MeshStandardMaterial({
434
+ color: OHT_LINE_RAIL_COLOR,
435
+ metalness: 0.5,
436
+ roughness: 0.4,
437
+ });
438
+ const geoms = [];
439
+ // 상부 플랜지
440
+ const topFlange = isVertical
441
+ ? new THREE.BoxGeometry(flangeW, flangeH, railLen)
442
+ : new THREE.BoxGeometry(railLen, flangeH, flangeW);
443
+ topFlange.translate(0, -flangeH / 2, 0);
444
+ geoms.push(topFlange);
445
+ // 웹 (수직판)
446
+ const web = isVertical
447
+ ? new THREE.BoxGeometry(webW, webH, railLen)
448
+ : new THREE.BoxGeometry(railLen, webH, webW);
449
+ web.translate(0, -flangeH - webH / 2, 0);
450
+ geoms.push(web);
451
+ // 하부 플랜지
452
+ const botFlange = isVertical
453
+ ? new THREE.BoxGeometry(flangeW, flangeH, railLen)
454
+ : new THREE.BoxGeometry(railLen, flangeH, flangeW);
455
+ botFlange.translate(0, -flangeH - webH - flangeH / 2, 0);
456
+ geoms.push(botFlange);
457
+ this._mesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(geoms), mat);
458
+ this._mesh.castShadow = true;
459
+ this.object3d.add(this._mesh);
460
+ }
461
+ update() {
462
+ if (this._mesh) {
463
+ this.object3d.remove(this._mesh);
464
+ this._mesh.geometry.dispose();
465
+ this._mesh.material.dispose();
466
+ this._mesh = undefined;
467
+ }
468
+ this.build();
469
+ this.updateTransform();
470
+ this.updateHidden();
471
+ }
472
+ onchange(after, before) {
473
+ if ('width' in after || 'height' in after || 'depth' in after ||
474
+ 'fillStyle' in after || 'strokeStyle' in after) {
475
+ this.update();
476
+ return;
477
+ }
478
+ super.onchange(after, before);
479
+ }
480
+ updateDimension() { }
481
+ updateAlpha() { }
482
+ }
483
+ registerRealObjectFactory('OHTLine', (component) => new OHTLineRealObject3D(component));
351
484
  // STOCKER: 벽면에 용량 게이지 바를 포함하는 커스텀 3D 오브젝트
352
485
  const STOCKER_CONFIG = Object.assign(Object.assign({}, DEFAULTS), { defaultDepth: 5 * TRANSPORT_SURFACE_HEIGHT, opacity: 0.75, metalness: 0.3, roughness: 0.6, defaultColor: 0x8899bb });
353
486
  class StockerRealObject3D extends MCSRealObject3D {
@@ -423,8 +556,9 @@ class StockerRealObject3D extends MCSRealObject3D {
423
556
  mat.polygonOffsetFactor = -2;
424
557
  mat.polygonOffsetUnit = -2;
425
558
  this.gaugeMesh = new THREE.Mesh(geo, mat);
559
+ // ownGroup 기준(bottom-up): 볼륨 상단 = +depth. 그 위로 0.5 띄움.
426
560
  this.gaugeMesh.position.set(0, depth + 0.5, 0);
427
- this.object3d.add(this.gaugeMesh);
561
+ this.components3D.add(this.gaugeMesh);
428
562
  }
429
563
  resolveGauge(data, mode) {
430
564
  var _a, _b;
@@ -469,7 +603,7 @@ class StockerRealObject3D extends MCSRealObject3D {
469
603
  const fullup = data.FULLUP === 'T';
470
604
  const usage = Math.round((cur / max) * 100);
471
605
  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');
606
+ const color = fullup ? 'red' : (getValueOnRanges(usage, legend) || '#44aa44');
473
607
  return {
474
608
  segments: [{ ratio: usage / 100, color }],
475
609
  borderColor: color,
@@ -478,6 +612,10 @@ class StockerRealObject3D extends MCSRealObject3D {
478
612
  }
479
613
  }
480
614
  }
615
+ update() {
616
+ this.clearGauge();
617
+ super.update();
618
+ }
481
619
  onchange(after, before) {
482
620
  if ('data' in after) {
483
621
  this.clearGauge();
@@ -492,7 +630,7 @@ class StockerRealObject3D extends MCSRealObject3D {
492
630
  clearGauge() {
493
631
  var _a, _b;
494
632
  if (this.gaugeMesh) {
495
- this.object3d.remove(this.gaugeMesh);
633
+ this.components3D.remove(this.gaugeMesh);
496
634
  (_a = this.gaugeMesh.geometry) === null || _a === void 0 ? void 0 : _a.dispose();
497
635
  const mat = this.gaugeMesh.material;
498
636
  (_b = mat === null || mat === void 0 ? void 0 : mat.map) === null || _b === void 0 ? void 0 : _b.dispose();
@@ -505,8 +643,8 @@ registerRealObjectFactory('STOCKER', (component) => new StockerRealObject3D(comp
505
643
  // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
506
644
  // Unit (이동체 · 홀더) — 낮은 depth, 불투명
507
645
  // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
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 });
646
+ // Port: 바닥판 (PlaneGeometry)
647
+ 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' });
510
648
  registerRealObjectFactory('Port', (component) => new MCSRealObject3D(component, PORT_CONFIG));
511
649
  register('Shelf', {
512
650
  depthRatio: 0.25,
@@ -515,113 +653,206 @@ register('Shelf', {
515
653
  defaultColor: 0xaabbcc,
516
654
  });
517
655
  // 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 });
656
+ 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 });
519
657
  class CarrierRealObject3D extends MCSRealObject3D {
520
658
  constructor(component) {
521
659
  super(component, CARRIER_CONFIG);
522
- this._animRaf = null;
523
- this._animStartTime = 0;
660
+ }
661
+ }
662
+ registerRealObjectFactory('Carrier', (component) => new CarrierRealObject3D(component));
663
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
664
+ // Gauge (용량 게이지) — 얇은 바
665
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
666
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
667
+ // Gauge (용량 게이지) — 2D 렌더링을 Plane 텍스처로 3D 표시
668
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
669
+ // Gauge 컴포넌트는 3D에서 표시하지 않음 (부모 STOCKER의 3D 모델에서 직접 게이지 렌더링)
670
+ // null 반환으로 3D 오브젝트 생성을 억제하고, default factory fallback도 방지
671
+ registerRealObjectFactory('StockerCapacityBar', () => undefined);
672
+ registerRealObjectFactory('MCSGaugeCapacityBar', () => undefined);
673
+ registerRealObjectFactory('ZoneCapacityBar', () => undefined);
674
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
675
+ // PortFlow — 2D render를 Plane 텍스처로 3D 표시
676
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
677
+ class PortFlowRealObject3D extends RealObjectGroup {
678
+ get effectiveDepth() { return 0; }
679
+ get syncZPosOffset() {
680
+ var _a;
681
+ // 부모 Port/Conveyor의 zPos와 동일하게
682
+ const parent = this.component.parentObject;
683
+ if (((_a = parent === null || parent === void 0 ? void 0 : parent.state) === null || _a === void 0 ? void 0 : _a.zPos) != null)
684
+ return parent.state.zPos;
685
+ return TRANSPORT_SURFACE_HEIGHT;
524
686
  }
525
687
  build() {
526
688
  super.build();
527
- this.startCarrierAnimation();
528
- }
529
- /**
530
- * CARRIERSTATUS의 애니메이션 설정을 읽어 3D에서 직접 실행.
531
- * things-scene의 delta 시스템과 독립적으로 동작.
532
- */
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)
689
+ const { width = 10, height = 10 } = this.component.state;
690
+ const w = Math.abs(width);
691
+ const h = Math.abs(height);
692
+ if (!w || !h)
548
693
  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);
694
+ // 2D render를 캔버스에 캡처
695
+ // 화살표 날개 + 텍스트 영역이 bounds 밖으로 나가므로 전체 영역 계산
696
+ const { left, top } = this.component.bounds;
697
+ const { fontSize = 24, labelPosition = 'top' } = this.component.state;
698
+ const arrowPad = Math.min(w, h) * 0.4;
699
+ // 텍스트 영역 확장
700
+ let padLeft = arrowPad, padRight = arrowPad, padTop = arrowPad, padBottom = arrowPad;
701
+ if (this.component.hasTextProperty) {
702
+ switch (labelPosition) {
703
+ case 'top':
704
+ padTop = Math.max(padTop, fontSize + 4);
564
705
  break;
565
- }
566
- case 'rotation': {
567
- obj.rotation.y = progress * Math.PI * 2;
706
+ case 'bottom':
707
+ padBottom = Math.max(padBottom, fontSize + 4);
568
708
  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;
709
+ case 'left':
710
+ padLeft = Math.max(padLeft, fontSize * 4 + 4);
574
711
  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
- });
712
+ case 'right':
713
+ padRight = Math.max(padRight, fontSize * 4 + 4);
584
714
  break;
585
- }
586
715
  }
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
716
  }
596
- }
597
- stopCarrierAnimation() {
598
- if (this._animRaf) {
599
- cancelAnimationFrame(this._animRaf);
600
- this._animRaf = null;
717
+ const totalW = w + padLeft + padRight;
718
+ const totalH = h + padTop + padBottom;
719
+ const scale = 2;
720
+ const canvas = document.createElement('canvas');
721
+ canvas.width = totalW * scale;
722
+ canvas.height = totalH * scale;
723
+ const ctx = canvas.getContext('2d');
724
+ ctx.save();
725
+ ctx.scale(scale, scale);
726
+ ctx.translate(-left + padLeft, -top + padTop);
727
+ try {
728
+ // 화살표 렌더
729
+ this.component.render(ctx);
730
+ ctx.fillStyle = this.component.state.strokeStyle || '#666666';
731
+ ctx.fill();
732
+ // 텍스트 렌더
733
+ if (this.component.hasTextProperty && this.component.drawText) {
734
+ ;
735
+ this.component.drawText(ctx);
736
+ }
601
737
  }
602
- // group 스케일/회전 초기화
603
- this.object3d.scale.set(1, 1, 1);
604
- this.object3d.rotation.y = 0;
738
+ catch ( /* render 실패 시 빈 텍스처 */_a) { /* render 실패 시 빈 텍스처 */ }
739
+ ctx.restore();
740
+ this._texture = new THREE.CanvasTexture(canvas);
741
+ this._texture.colorSpace = THREE.SRGBColorSpace;
742
+ const geo = new THREE.PlaneGeometry(totalW, totalH);
743
+ geo.rotateX(-Math.PI / 2);
744
+ const mat = new THREE.MeshBasicMaterial({
745
+ map: this._texture,
746
+ alphaTest: 0.1,
747
+ side: THREE.DoubleSide,
748
+ });
749
+ this._mesh = new THREE.Mesh(geo, mat);
750
+ // 비대칭 패딩 보정: 플레인 센터를 컴포넌트 bounds 센터에 맞춤
751
+ this._mesh.position.x = (padRight - padLeft) / 2;
752
+ this._mesh.position.y = 1;
753
+ this._mesh.position.z = (padBottom - padTop) / 2;
754
+ this.object3d.add(this._mesh);
605
755
  }
606
756
  update() {
607
- this.stopCarrierAnimation();
608
- super.update();
757
+ var _a;
758
+ if (this._mesh) {
759
+ this.object3d.remove(this._mesh);
760
+ this._mesh.geometry.dispose();
761
+ this._mesh.material.dispose();
762
+ (_a = this._texture) === null || _a === void 0 ? void 0 : _a.dispose();
763
+ this._mesh = undefined;
764
+ this._texture = undefined;
765
+ }
766
+ this.build();
767
+ this.updateTransform();
768
+ this.updateHidden();
609
769
  }
610
- clear() {
611
- this.stopCarrierAnimation();
612
- return super.clear();
770
+ onchange(after, before) {
771
+ if ('width' in after || 'height' in after || 'orientation' in after ||
772
+ 'direction' in after || 'strokeStyle' in after || 'INOUTTYPE' in after) {
773
+ this.update();
774
+ return;
775
+ }
776
+ super.onchange(after, before);
613
777
  }
778
+ updateDimension() { }
779
+ updateAlpha() { }
614
780
  }
615
- registerRealObjectFactory('Carrier', (component) => new CarrierRealObject3D(component));
616
- // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
617
- // Gauge (용량 게이지) — 얇은 바
618
- // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
781
+ registerRealObjectFactory('PortFlow', (component) => new PortFlowRealObject3D(component));
619
782
  // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
620
- // Gauge (용량 게이지) — 2D 렌더링을 Plane 텍스처로 3D 표시
783
+ // Node — 2D render를 바닥 Plane 텍스처로 3D 표시
621
784
  // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
622
- // Gauge 컴포넌트 (StockerCapacityBar, ZoneCapacityBar, MCSGaugeCapacityBar)는
623
- // 3D에서 표시하지 않음. 대신 부모(STOCKER 등)의 3D 모델에서 직접 게이지를 렌더링.
624
- registerRealObjectFactory('StockerCapacityBar', () => null);
625
- registerRealObjectFactory('MCSGaugeCapacityBar', () => null);
626
- registerRealObjectFactory('ZoneCapacityBar', () => null);
785
+ class NodeRealObject3D extends RealObjectGroup {
786
+ get effectiveDepth() { return 0; }
787
+ get syncZPosOffset() { return 0; }
788
+ build() {
789
+ super.build();
790
+ const { cx, cy } = this.component.state;
791
+ const radius = 8;
792
+ const size = radius * 2;
793
+ const scale = 2;
794
+ const pad = 2;
795
+ const canvas = document.createElement('canvas');
796
+ canvas.width = (size + pad * 2) * scale;
797
+ canvas.height = (size + pad * 2) * scale;
798
+ const ctx = canvas.getContext('2d');
799
+ ctx.save();
800
+ ctx.scale(scale, scale);
801
+ ctx.translate(-cx + radius + pad, -cy + radius + pad);
802
+ try {
803
+ this.component.render(ctx);
804
+ const { fillStyle = '#888888' } = this.component.state;
805
+ ctx.fillStyle = typeof fillStyle === 'string' ? fillStyle : '#888888';
806
+ ctx.fill();
807
+ const { strokeStyle } = this.component.state;
808
+ if (strokeStyle && typeof strokeStyle === 'string') {
809
+ ctx.strokeStyle = strokeStyle;
810
+ ctx.lineWidth = 1;
811
+ ctx.stroke();
812
+ }
813
+ }
814
+ catch ( /* render 실패 시 빈 텍스처 */_a) { /* render 실패 시 빈 텍스처 */ }
815
+ ctx.restore();
816
+ this._texture = new THREE.CanvasTexture(canvas);
817
+ this._texture.colorSpace = THREE.SRGBColorSpace;
818
+ const geo = new THREE.PlaneGeometry(size + pad * 2, size + pad * 2);
819
+ geo.rotateX(-Math.PI / 2);
820
+ const mat = new THREE.MeshBasicMaterial({
821
+ map: this._texture,
822
+ alphaTest: 0.1,
823
+ side: THREE.DoubleSide,
824
+ });
825
+ this._mesh = new THREE.Mesh(geo, mat);
826
+ this._mesh.position.y = 1;
827
+ this.object3d.add(this._mesh);
828
+ }
829
+ update() {
830
+ var _a;
831
+ if (this._mesh) {
832
+ this.object3d.remove(this._mesh);
833
+ this._mesh.geometry.dispose();
834
+ this._mesh.material.dispose();
835
+ (_a = this._texture) === null || _a === void 0 ? void 0 : _a.dispose();
836
+ this._mesh = undefined;
837
+ this._texture = undefined;
838
+ }
839
+ this.build();
840
+ this.updateTransform();
841
+ this.updateHidden();
842
+ }
843
+ onchange(after, before) {
844
+ if ('fillStyle' in after || 'strokeStyle' in after) {
845
+ this.update();
846
+ return;
847
+ }
848
+ super.onchange(after, before);
849
+ }
850
+ updateDimension() { }
851
+ updateAlpha() { }
852
+ }
853
+ registerRealObjectFactory('Node', (component) => new NodeRealObject3D(component));
854
+ // Scanner — 2D Triangle 실루엣과 정합되는 삼각형 prism.
855
+ // 이송 표면 높이의 0.2배(=6) 얇은 기둥 — 원래 값 유지, shape 만 'box' → 'triangle'.
856
+ 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' });
857
+ registerRealObjectFactory('Scanner', (component) => new MCSRealObject3D(component, SCANNER_CONFIG));
627
858
  //# sourceMappingURL=machine-3d.js.map