@fmsim/machine 1.0.88 → 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 (165) hide show
  1. package/dist/agv-line.js.map +1 -1
  2. package/dist/agv.js +6 -2
  3. package/dist/agv.js.map +1 -1
  4. package/dist/buffer.js.map +1 -1
  5. package/dist/carrier-manager.js +183 -0
  6. package/dist/carrier-manager.js.map +1 -0
  7. package/dist/carrier.js +105 -14
  8. package/dist/carrier.js.map +1 -1
  9. package/dist/conveyor-join.js +8 -2
  10. package/dist/conveyor-join.js.map +1 -1
  11. package/dist/conveyor.js +9 -1
  12. package/dist/conveyor.js.map +1 -1
  13. package/dist/crane-rail.js +53 -0
  14. package/dist/crane-rail.js.map +1 -0
  15. package/dist/crane.js +144 -1
  16. package/dist/crane.js.map +1 -1
  17. package/dist/equipment.js.map +1 -1
  18. package/dist/factories/agv-3d.js +147 -0
  19. package/dist/factories/agv-3d.js.map +1 -0
  20. package/dist/factories/conveyor-3d.js +345 -0
  21. package/dist/factories/conveyor-3d.js.map +1 -0
  22. package/dist/factories/crane-3d.js +186 -0
  23. package/dist/factories/crane-3d.js.map +1 -0
  24. package/dist/factories/crane-rail-3d.js +95 -0
  25. package/dist/factories/crane-rail-3d.js.map +1 -0
  26. package/dist/factories/machine-3d.js +973 -0
  27. package/dist/factories/machine-3d.js.map +1 -0
  28. package/dist/factories/oht-3d.js +177 -0
  29. package/dist/factories/oht-3d.js.map +1 -0
  30. package/dist/factories/shuttle-3d.js +159 -0
  31. package/dist/factories/shuttle-3d.js.map +1 -0
  32. package/dist/factories/z-priority-defaults.js +33 -0
  33. package/dist/factories/z-priority-defaults.js.map +1 -0
  34. package/dist/factories/z-priority.js +54 -0
  35. package/dist/factories/z-priority.js.map +1 -0
  36. package/dist/features/conveyor-mixin.js.map +1 -1
  37. package/dist/features/mcs-status-default.js +15 -1
  38. package/dist/features/mcs-status-default.js.map +1 -1
  39. package/dist/features/mcs-status-mixin.js +22 -0
  40. package/dist/features/mcs-status-mixin.js.map +1 -1
  41. package/dist/features/mcs-zone-mixin.js.map +1 -1
  42. package/dist/features/parent-object-mixin.js +15 -7
  43. package/dist/features/parent-object-mixin.js.map +1 -1
  44. package/dist/floating-container.js +5 -0
  45. package/dist/floating-container.js.map +1 -1
  46. package/dist/index.js +58 -2
  47. package/dist/index.js.map +1 -1
  48. package/dist/mcs-carrier-holder.js +56 -17
  49. package/dist/mcs-carrier-holder.js.map +1 -1
  50. package/dist/mcs-machine.js +39 -0
  51. package/dist/mcs-machine.js.map +1 -1
  52. package/dist/mcs-unit.js +20 -0
  53. package/dist/mcs-unit.js.map +1 -1
  54. package/dist/mcs-vehicle.js +10 -4
  55. package/dist/mcs-vehicle.js.map +1 -1
  56. package/dist/node.js +10 -3
  57. package/dist/node.js.map +1 -1
  58. package/dist/oht-line.js.map +1 -1
  59. package/dist/oht.js +4 -0
  60. package/dist/oht.js.map +1 -1
  61. package/dist/port-flow.js +4 -0
  62. package/dist/port-flow.js.map +1 -1
  63. package/dist/port.js +4 -0
  64. package/dist/port.js.map +1 -1
  65. package/dist/scanner.js +30 -0
  66. package/dist/scanner.js.map +1 -0
  67. package/dist/scene/root-container-override.js +160 -96
  68. package/dist/scene/root-container-override.js.map +1 -1
  69. package/dist/shelf.js +4 -0
  70. package/dist/shelf.js.map +1 -1
  71. package/dist/shuttle.js.map +1 -1
  72. package/dist/static-container.js +5 -0
  73. package/dist/static-container.js.map +1 -1
  74. package/dist/stocker-abnormal-bar.js +184 -0
  75. package/dist/stocker-abnormal-bar.js.map +1 -0
  76. package/dist/stocker-capacity-bar.js +253 -6
  77. package/dist/stocker-capacity-bar.js.map +1 -1
  78. package/dist/stocker-carrier-bar.js +198 -0
  79. package/dist/stocker-carrier-bar.js.map +1 -0
  80. package/dist/stocker.js +7 -1
  81. package/dist/stocker.js.map +1 -1
  82. package/dist/templates/backdrop.js +30 -0
  83. package/dist/templates/backdrop.js.map +1 -0
  84. package/dist/templates/decal.js +21 -0
  85. package/dist/templates/decal.js.map +1 -0
  86. package/dist/templates/index.js +7 -1
  87. package/dist/templates/index.js.map +1 -1
  88. package/dist/templates/scanner.js +20 -0
  89. package/dist/templates/scanner.js.map +1 -0
  90. package/dist/tsconfig.tsbuildinfo +1 -1
  91. package/dist/utils/crane-position.js +83 -0
  92. package/dist/utils/crane-position.js.map +1 -0
  93. package/dist/utils/data-value.js +21 -0
  94. package/dist/utils/data-value.js.map +1 -0
  95. package/dist/utils/fit-gauge-scale.js +28 -0
  96. package/dist/utils/fit-gauge-scale.js.map +1 -0
  97. package/dist/utils/get-value-on-ranges.js +2 -4
  98. package/dist/utils/get-value-on-ranges.js.map +1 -1
  99. package/dist/utils/stocker-gauge.js +78 -0
  100. package/dist/utils/stocker-gauge.js.map +1 -0
  101. package/dist/zone-capacity-bar.js +3 -3
  102. package/dist/zone-capacity-bar.js.map +1 -1
  103. package/icons/backdrop.png +0 -0
  104. package/icons/decal.png +0 -0
  105. package/jest.config.js +10 -0
  106. package/package.json +7 -6
  107. package/src/agv-line.ts +1 -1
  108. package/src/agv.ts +10 -5
  109. package/src/buffer.ts +1 -1
  110. package/src/carrier-manager.ts +223 -0
  111. package/src/carrier.ts +111 -18
  112. package/src/conveyor-join.ts +6 -3
  113. package/src/conveyor.ts +10 -2
  114. package/src/crane-rail.ts +58 -0
  115. package/src/crane.ts +182 -2
  116. package/src/equipment.ts +1 -1
  117. package/src/factories/agv-3d.ts +160 -0
  118. package/src/factories/conveyor-3d.ts +398 -0
  119. package/src/factories/crane-3d.ts +217 -0
  120. package/src/factories/crane-rail-3d.ts +115 -0
  121. package/src/factories/machine-3d.ts +1192 -0
  122. package/src/factories/oht-3d.ts +196 -0
  123. package/src/factories/shuttle-3d.ts +179 -0
  124. package/src/factories/z-priority-defaults.ts +38 -0
  125. package/src/factories/z-priority.ts +60 -0
  126. package/src/features/conveyor-mixin.ts +1 -1
  127. package/src/features/mcs-status-default.ts +15 -1
  128. package/src/features/mcs-status-mixin.ts +26 -4
  129. package/src/features/mcs-zone-mixin.ts +1 -1
  130. package/src/features/parent-object-mixin.ts +17 -8
  131. package/src/floating-container.ts +5 -0
  132. package/src/index.ts +66 -2
  133. package/src/mcs-carrier-holder.ts +58 -29
  134. package/src/mcs-machine.ts +43 -1
  135. package/src/mcs-unit.ts +25 -2
  136. package/src/mcs-vehicle.ts +12 -9
  137. package/src/node.ts +9 -3
  138. package/src/oht-line.ts +1 -1
  139. package/src/oht.ts +7 -2
  140. package/src/port-flow.ts +6 -1
  141. package/src/port.ts +6 -1
  142. package/src/scanner.ts +29 -0
  143. package/src/scene/root-container-override.ts +156 -105
  144. package/src/shelf.ts +6 -1
  145. package/src/shuttle.ts +1 -1
  146. package/src/static-container.ts +5 -0
  147. package/src/stocker-abnormal-bar.ts +247 -0
  148. package/src/stocker-capacity-bar.ts +301 -10
  149. package/src/stocker-carrier-bar.ts +262 -0
  150. package/src/stocker.ts +10 -3
  151. package/src/templates/backdrop.ts +30 -0
  152. package/src/templates/decal.ts +21 -0
  153. package/src/templates/index.ts +7 -1
  154. package/src/templates/scanner.ts +20 -0
  155. package/src/utils/crane-position.ts +105 -0
  156. package/src/utils/data-value.ts +19 -0
  157. package/src/utils/fit-gauge-scale.ts +35 -0
  158. package/src/utils/get-value-on-ranges.ts +2 -4
  159. package/src/utils/stocker-gauge.ts +111 -0
  160. package/src/zone-capacity-bar.ts +5 -5
  161. package/translations/en.json +26 -1
  162. package/translations/ja.json +26 -1
  163. package/translations/ko.json +26 -1
  164. package/translations/ms.json +26 -1
  165. package/translations/zh.json +26 -1
@@ -0,0 +1,147 @@
1
+ /**
2
+ * AGV 3D Factory
3
+ *
4
+ * 범퍼 + 본체 + 플랫폼 3단 구조:
5
+ * - 하단 범퍼 (10%, 본체보다 약간 넓음, #222222)
6
+ * - 본체 (58%, RoundedBoxGeometry, statusColor 적용)
7
+ * - 간격 (4%)
8
+ * - 상단 플랫폼 (28%, 본체보다 안쪽, #333333)
9
+ *
10
+ * Based on operato-scene Vehicle3D, adapted for fmsim:
11
+ * - extends RealObjectGroup (Conveyor3D와 동일 패턴)
12
+ * - statusColor from MCSStatusMixin
13
+ * - 볼륨 중심 원점 (things-scene 기본 컨벤션) - mesh 로컬 [-d/2, +d/2]
14
+ */
15
+ import * as THREE from 'three';
16
+ import { RoundedBoxGeometry } from 'three/examples/jsm/geometries/RoundedBoxGeometry.js';
17
+ import { RealObjectGroup, registerRealObjectFactory, disposeObject3D } from '@hatiolab/things-scene';
18
+ import { TRANSPORT_SURFACE_HEIGHT } from './machine-3d.js';
19
+ const BUMPER_COLOR = 0x222222;
20
+ const PLATFORM_COLOR = 0x333333;
21
+ const DEFAULT_BODY_COLOR = 0x6688cc;
22
+ export class AGV3D extends RealObjectGroup {
23
+ constructor() {
24
+ super(...arguments);
25
+ this._ownMeshes = [];
26
+ }
27
+ get effectiveDepth() {
28
+ const { depth } = this.component.state;
29
+ return depth || TRANSPORT_SURFACE_HEIGHT;
30
+ }
31
+ resolveColor() {
32
+ const comp = this.component;
33
+ // 1) MCSStatusMixin.statusColor (legend 기반 상태 색상)
34
+ // '#F0F0F0'은 상태 미설정 시 기본 fallback — 무시하고 defaultColor 사용
35
+ try {
36
+ const sc = comp.statusColor;
37
+ if (typeof sc === 'string' && sc && sc.toUpperCase() !== '#F0F0F0' && sc !== 'transparent')
38
+ return new THREE.Color(sc).getHex();
39
+ }
40
+ catch (_a) {
41
+ /* statusColor 미구현 시 무시 */
42
+ }
43
+ // 2) state.fillStyle fallback
44
+ const { fillStyle } = this.component.state;
45
+ if (fillStyle && typeof fillStyle === 'string' && fillStyle !== 'transparent') {
46
+ try {
47
+ return new THREE.Color(fillStyle).getHex();
48
+ }
49
+ catch (_b) {
50
+ /* 파싱 실패 시 무시 */
51
+ }
52
+ }
53
+ return DEFAULT_BODY_COLOR;
54
+ }
55
+ build() {
56
+ super.build();
57
+ const { width = 10, height = 10 } = this.component.state;
58
+ const w = Math.abs(width);
59
+ const h = Math.abs(height);
60
+ const d = this.effectiveDepth;
61
+ if (!w || !h)
62
+ return;
63
+ // 비율: 하단 범퍼 10%, 본체 58%, 간격 4%, 플랫폼 28%
64
+ const bumperH = d * 0.1;
65
+ const bodyH = d * 0.58;
66
+ const gapH = d * 0.04;
67
+ const platH = d * 0.28;
68
+ const cornerR = Math.min(w, h, bodyH) * 0.12;
69
+ // ── 하단 범퍼 밴드 (본체보다 약간 넓게 돌출) ──
70
+ const bumperPad = Math.min(w, h) * 0.03;
71
+ const bumperCornerR = Math.min(w + bumperPad * 2, h + bumperPad * 2, bumperH) * 0.2;
72
+ const bumperGeo = new RoundedBoxGeometry(w + bumperPad * 2, bumperH, h + bumperPad * 2, 4, bumperCornerR);
73
+ const bumperMat = new THREE.MeshStandardMaterial({
74
+ color: BUMPER_COLOR,
75
+ roughness: 0.8,
76
+ metalness: 0.1,
77
+ });
78
+ const bumper = new THREE.Mesh(bumperGeo, bumperMat);
79
+ bumper.position.y = bumperH / 2;
80
+ bumper.castShadow = true;
81
+ bumper.receiveShadow = true;
82
+ // ── 본체 (둥근 모서리, statusColor 적용) ──
83
+ const bodyColor = this.resolveColor();
84
+ const bodyGeo = new RoundedBoxGeometry(w, bodyH, h, 4, cornerR);
85
+ const bodyMat = new THREE.MeshStandardMaterial({
86
+ color: bodyColor,
87
+ roughness: 0.3,
88
+ metalness: 0.5,
89
+ });
90
+ const body = new THREE.Mesh(bodyGeo, bodyMat);
91
+ body.position.y = bumperH + bodyH / 2;
92
+ body.castShadow = true;
93
+ body.receiveShadow = true;
94
+ // ── 상단 플랫폼 (본체보다 안쪽, 어두운색) ──
95
+ const platInset = Math.min(w, h) * 0.04;
96
+ const platCornerR = Math.min(w - platInset * 2, h - platInset * 2, platH) * 0.15;
97
+ const platGeo = new RoundedBoxGeometry(w - platInset * 2, platH, h - platInset * 2, 4, platCornerR);
98
+ const platMat = new THREE.MeshStandardMaterial({
99
+ color: PLATFORM_COLOR,
100
+ roughness: 0.5,
101
+ metalness: 0.15,
102
+ });
103
+ const plat = new THREE.Mesh(platGeo, platMat);
104
+ plat.position.y = bumperH + bodyH + gapH + platH / 2;
105
+ plat.castShadow = true;
106
+ plat.receiveShadow = true;
107
+ this._ownMeshes = [bumper, body, plat];
108
+ // ownGroup이 center-origin ↔ bottom-up 좌표계 변환을 자동 처리
109
+ for (const m of this._ownMeshes)
110
+ this.components3D.add(m);
111
+ }
112
+ updateDimension() { }
113
+ update() {
114
+ // 자신의 mesh만 제거 (Carrier 등 자식 컴포넌트 3D 보존)
115
+ for (const m of this._ownMeshes) {
116
+ this.components3D.remove(m);
117
+ disposeObject3D(m);
118
+ }
119
+ this._ownMeshes = [];
120
+ this.build();
121
+ this.updateTransform();
122
+ this.updateAlpha();
123
+ this.updateHidden();
124
+ }
125
+ updateAlpha() {
126
+ const { alpha = 1 } = this.component.state;
127
+ for (const m of this._ownMeshes) {
128
+ const materials = Array.isArray(m.material) ? m.material : [m.material];
129
+ for (const mat of materials) {
130
+ ;
131
+ mat.opacity = alpha;
132
+ mat.transparent = alpha < 1;
133
+ }
134
+ }
135
+ }
136
+ onchange(after, before) {
137
+ if ('width' in after || 'height' in after || 'depth' in after ||
138
+ 'fillStyle' in after || 'strokeStyle' in after || 'id' in after ||
139
+ 'EMPTYTYPE' in after || 'CARRIERSTATUS' in after) {
140
+ this.update();
141
+ return;
142
+ }
143
+ super.onchange(after, before);
144
+ }
145
+ }
146
+ registerRealObjectFactory('AGV', (component) => new AGV3D(component));
147
+ //# sourceMappingURL=agv-3d.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agv-3d.js","sourceRoot":"","sources":["../../src/factories/agv-3d.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * AGV 3D Factory\n *\n * 범퍼 + 본체 + 플랫폼 3단 구조:\n * - 하단 범퍼 (10%, 본체보다 약간 넓음, #222222)\n * - 본체 (58%, RoundedBoxGeometry, statusColor 적용)\n * - 간격 (4%)\n * - 상단 플랫폼 (28%, 본체보다 안쪽, #333333)\n *\n * Based on operato-scene Vehicle3D, adapted for fmsim:\n * - extends RealObjectGroup (Conveyor3D와 동일 패턴)\n * - statusColor from MCSStatusMixin\n * - 볼륨 중심 원점 (things-scene 기본 컨벤션) - mesh 로컬 [-d/2, +d/2]\n */\n\nimport * as THREE from 'three'\nimport { RoundedBoxGeometry } from 'three/examples/jsm/geometries/RoundedBoxGeometry.js'\nimport { RealObjectGroup, registerRealObjectFactory, disposeObject3D } from '@hatiolab/things-scene'\nimport type { Component } from '@hatiolab/things-scene'\nimport { TRANSPORT_SURFACE_HEIGHT } from './machine-3d.js'\n\nconst BUMPER_COLOR = 0x222222\nconst PLATFORM_COLOR = 0x333333\nconst DEFAULT_BODY_COLOR = 0x6688cc\n\nexport class AGV3D extends RealObjectGroup {\n private _ownMeshes: THREE.Mesh[] = []\n get effectiveDepth(): number {\n const { depth } = this.component.state\n return depth || TRANSPORT_SURFACE_HEIGHT\n }\n\n private resolveColor(): number {\n const comp = this.component as any\n\n // 1) MCSStatusMixin.statusColor (legend 기반 상태 색상)\n // '#F0F0F0'은 상태 미설정 시 기본 fallback — 무시하고 defaultColor 사용\n try {\n const sc = comp.statusColor\n if (typeof sc === 'string' && sc && sc.toUpperCase() !== '#F0F0F0' && sc !== 'transparent') return new THREE.Color(sc).getHex()\n } catch {\n /* statusColor 미구현 시 무시 */\n }\n\n // 2) state.fillStyle fallback\n const { fillStyle } = this.component.state\n if (fillStyle && typeof fillStyle === 'string' && fillStyle !== 'transparent') {\n try {\n return new THREE.Color(fillStyle).getHex()\n } catch {\n /* 파싱 실패 시 무시 */\n }\n }\n\n return DEFAULT_BODY_COLOR\n }\n\n build() {\n super.build()\n\n const { width = 10, height = 10 } = this.component.state\n const w = Math.abs(width)\n const h = Math.abs(height)\n const d = this.effectiveDepth\n if (!w || !h) return\n\n // 비율: 하단 범퍼 10%, 본체 58%, 간격 4%, 플랫폼 28%\n const bumperH = d * 0.1\n const bodyH = d * 0.58\n const gapH = d * 0.04\n const platH = d * 0.28\n\n const cornerR = Math.min(w, h, bodyH) * 0.12\n\n // ── 하단 범퍼 밴드 (본체보다 약간 넓게 돌출) ──\n const bumperPad = Math.min(w, h) * 0.03\n const bumperCornerR = Math.min(w + bumperPad * 2, h + bumperPad * 2, bumperH) * 0.2\n const bumperGeo = new RoundedBoxGeometry(w + bumperPad * 2, bumperH, h + bumperPad * 2, 4, bumperCornerR)\n const bumperMat = new THREE.MeshStandardMaterial({\n color: BUMPER_COLOR,\n roughness: 0.8,\n metalness: 0.1,\n })\n const bumper = new THREE.Mesh(bumperGeo, bumperMat)\n bumper.position.y = bumperH / 2\n bumper.castShadow = true\n bumper.receiveShadow = true\n\n // ── 본체 (둥근 모서리, statusColor 적용) ──\n const bodyColor = this.resolveColor()\n const bodyGeo = new RoundedBoxGeometry(w, bodyH, h, 4, cornerR)\n const bodyMat = new THREE.MeshStandardMaterial({\n color: bodyColor,\n roughness: 0.3,\n metalness: 0.5,\n })\n const body = new THREE.Mesh(bodyGeo, bodyMat)\n body.position.y = bumperH + bodyH / 2\n body.castShadow = true\n body.receiveShadow = true\n\n // ── 상단 플랫폼 (본체보다 안쪽, 어두운색) ──\n const platInset = Math.min(w, h) * 0.04\n const platCornerR = Math.min(w - platInset * 2, h - platInset * 2, platH) * 0.15\n const platGeo = new RoundedBoxGeometry(w - platInset * 2, platH, h - platInset * 2, 4, platCornerR)\n const platMat = new THREE.MeshStandardMaterial({\n color: PLATFORM_COLOR,\n roughness: 0.5,\n metalness: 0.15,\n })\n const plat = new THREE.Mesh(platGeo, platMat)\n plat.position.y = bumperH + bodyH + gapH + platH / 2\n plat.castShadow = true\n plat.receiveShadow = true\n\n this._ownMeshes = [bumper, body, plat]\n // ownGroup이 center-origin ↔ bottom-up 좌표계 변환을 자동 처리\n for (const m of this._ownMeshes) this.components3D.add(m)\n }\n\n updateDimension() {}\n\n update() {\n // 자신의 mesh만 제거 (Carrier 등 자식 컴포넌트 3D 보존)\n for (const m of this._ownMeshes) {\n this.components3D.remove(m)\n disposeObject3D(m)\n }\n this._ownMeshes = []\n\n this.build()\n this.updateTransform()\n this.updateAlpha()\n this.updateHidden()\n }\n\n updateAlpha() {\n const { alpha = 1 } = this.component.state\n for (const m of this._ownMeshes) {\n const materials = Array.isArray(m.material) ? m.material : [m.material]\n for (const mat of materials) {\n ;(mat as THREE.MeshStandardMaterial).opacity = alpha\n ;(mat as THREE.MeshStandardMaterial).transparent = alpha < 1\n }\n }\n }\n\n onchange(after: Record<string, unknown>, before: Record<string, unknown>) {\n if ('width' in after || 'height' in after || 'depth' in after ||\n 'fillStyle' in after || 'strokeStyle' in after || 'id' in after ||\n 'EMPTYTYPE' in after || 'CARRIERSTATUS' in after) {\n this.update()\n return\n }\n\n super.onchange(after, before)\n }\n}\n\nregisterRealObjectFactory('AGV', (component: Component) => new AGV3D(component))\n"]}
@@ -0,0 +1,345 @@
1
+ /**
2
+ * Conveyor 3D Factory
3
+ *
4
+ * 'conveyor' type (default): side rails + cylindrical rollers + 4 legs + cross-bracing
5
+ * 'belt' type: side rails + two end drums + belt wrapping + 4 legs + cross-bracing
6
+ * 'rail' type: side rails + two parallel rails + 4 legs + cross-bracing
7
+ *
8
+ * Based on operato-scene conveyor-3d.ts, adapted for fmsim:
9
+ * - conveyorType is string ('', 'conveyor', 'belt', 'rail') instead of numeric
10
+ * - statusColor from MCSStatusMixin instead of value-based lookup
11
+ * - rollWidth auto-calculated: Math.max(10, Math.min(width, height) / 2)
12
+ */
13
+ import * as THREE from 'three';
14
+ import * as BufferGeometryUtils from 'three/examples/jsm/utils/BufferGeometryUtils.js';
15
+ import { RealObjectGroup, registerRealObjectFactory } from '@hatiolab/things-scene';
16
+ import { TRANSPORT_SURFACE_HEIGHT } from './machine-3d.js';
17
+ import { Z_FMSIM } from './z-priority';
18
+ const FRAME_COLOR = 0x888899;
19
+ const ROLLER_COLOR = 0xaaaabc;
20
+ const DEFAULT_COLOR = 0x999999;
21
+ export class Conveyor3D extends RealObjectGroup {
22
+ get zPriority() { return Z_FMSIM.CONVEYOR; }
23
+ get effectiveDepth() {
24
+ const { depth } = this.component.state;
25
+ return depth || TRANSPORT_SURFACE_HEIGHT;
26
+ }
27
+ resolveColor() {
28
+ const comp = this.component;
29
+ // 1) MCSStatusMixin.statusColor (legend 기반 상태 색상)
30
+ // '#F0F0F0'은 상태 미설정 시 기본 fallback — 무시하고 defaultColor 사용
31
+ try {
32
+ const sc = comp.statusColor;
33
+ if (typeof sc === 'string' && sc && sc.toUpperCase() !== '#F0F0F0' && sc !== 'transparent')
34
+ return new THREE.Color(sc).getHex();
35
+ }
36
+ catch (_a) {
37
+ /* statusColor 미구현 시 무시 */
38
+ }
39
+ // 2) state.fillStyle fallback
40
+ const { fillStyle } = this.component.state;
41
+ if (fillStyle && typeof fillStyle === 'string' && fillStyle !== 'transparent') {
42
+ try {
43
+ return new THREE.Color(fillStyle).getHex();
44
+ }
45
+ catch (_b) {
46
+ /* 파싱 실패 시 무시 */
47
+ }
48
+ }
49
+ return DEFAULT_COLOR;
50
+ }
51
+ build() {
52
+ super.build();
53
+ const { width: rawWidth, height: rawHeight, conveyorType, orientation } = this.component.state;
54
+ const depth = this.effectiveDepth;
55
+ if (!rawWidth || !rawHeight)
56
+ return;
57
+ // orientation: 'horizontal' = 롤러가 Z축(세로), 'vertical' = 롤러가 X축(가로)
58
+ // default = 긴 방향에 수직 (긴 축을 따라 이송)
59
+ const isVertical = orientation === 'vertical' ? true
60
+ : orientation === 'horizontal' ? false
61
+ : rawWidth < rawHeight; // default: 세로가 길면 롤러는 가로(X축)
62
+ // isVertical=true → 롤러 축이 X(가로), 이송 방향이 Z(세로)
63
+ // isVertical=false → 롤러 축이 Z(세로), 이송 방향이 X(가로)
64
+ const width = rawWidth;
65
+ const height = rawHeight;
66
+ // 2D와 동일 로직으로 rollWidth 산출
67
+ const rollWidth = Math.max(10, Math.min(width, height) / 2);
68
+ const rollerDiameter = Math.max(rollWidth, 2);
69
+ const frameH = Math.min(rollerDiameter, depth);
70
+ const legH = Math.max(depth - frameH, 0);
71
+ // isVertical일 때 이송 방향이 Z축: 레일은 Z축을 따라, 벨트폭은 width
72
+ // !isVertical일 때 이송 방향이 X축: 레일은 X축을 따라, 벨트폭은 height
73
+ const beltWidth = isVertical ? width : height;
74
+ const railLen = isVertical ? height : width;
75
+ const railW = Math.max(beltWidth * 0.06, 2);
76
+ const legThickness = Math.max(railW * 0.8, 2);
77
+ const frameMaterial = new THREE.MeshStandardMaterial({
78
+ color: FRAME_COLOR,
79
+ metalness: 0.85,
80
+ roughness: 0.35,
81
+ });
82
+ // --- Frame: side rails + legs + cross-bracing ---
83
+ // Y 좌표: y=0(바닥) ~ y=depth(상단). 레일 상단이 depth에 위치.
84
+ const frameGeometries = [];
85
+ const railY = depth - frameH / 2;
86
+ // Side rails — 이송 방향을 따라, 벨트 양측에 배치
87
+ if (isVertical) {
88
+ // 레일이 Z축(세로)을 따라, ±X에 배치
89
+ for (const xSign of [-1, 1]) {
90
+ const rail = new THREE.BoxGeometry(railW, frameH, height);
91
+ rail.translate(xSign * (width / 2 - railW / 2), railY, 0);
92
+ frameGeometries.push(rail);
93
+ }
94
+ }
95
+ else {
96
+ // 레일이 X축(가로)을 따라, ±Z에 배치
97
+ for (const zSign of [-1, 1]) {
98
+ const rail = new THREE.BoxGeometry(width, frameH, railW);
99
+ rail.translate(0, railY, zSign * (height / 2 - railW / 2));
100
+ frameGeometries.push(rail);
101
+ }
102
+ }
103
+ // 4 legs at corners
104
+ if (legH > 0) {
105
+ const legTopY = depth - frameH;
106
+ const legCenterY = legTopY - legH / 2;
107
+ // 다리 위치: 레일 안쪽에 정렬
108
+ const legXPos = isVertical ? (width / 2 - railW / 2) : (width / 2 - legThickness / 2);
109
+ const legZPos = isVertical ? (height / 2 - legThickness / 2) : (height / 2 - railW / 2);
110
+ for (const xSign of [-1, 1]) {
111
+ for (const zSign of [-1, 1]) {
112
+ const leg = new THREE.BoxGeometry(legThickness, legH, legThickness);
113
+ leg.translate(xSign * legXPos, legCenterY, zSign * legZPos);
114
+ frameGeometries.push(leg);
115
+ }
116
+ }
117
+ // Cross-bracing
118
+ const braceH = legThickness * 0.6;
119
+ const braceW = legThickness * 0.6;
120
+ const braceY = legTopY - legH * 0.35;
121
+ // 이송 방향 브레이스 (레일과 평행)
122
+ if (isVertical) {
123
+ for (const xSign of [-1, 1]) {
124
+ const brace = new THREE.BoxGeometry(braceW, braceH, height - legThickness);
125
+ brace.translate(xSign * legXPos, braceY, 0);
126
+ frameGeometries.push(brace);
127
+ }
128
+ }
129
+ else {
130
+ for (const zSign of [-1, 1]) {
131
+ const brace = new THREE.BoxGeometry(width - legThickness, braceH, braceW);
132
+ brace.translate(0, braceY, zSign * legZPos);
133
+ frameGeometries.push(brace);
134
+ }
135
+ }
136
+ // 벨트폭 방향 브레이스 (레일과 수직)
137
+ if (isVertical) {
138
+ for (const zSign of [-1, 1]) {
139
+ const brace = new THREE.BoxGeometry(width - railW, braceH, braceW);
140
+ brace.translate(0, braceY, zSign * legZPos);
141
+ frameGeometries.push(brace);
142
+ }
143
+ }
144
+ else {
145
+ for (const xSign of [-1, 1]) {
146
+ const brace = new THREE.BoxGeometry(braceW, braceH, height - railW);
147
+ brace.translate(xSign * legXPos, braceY, 0);
148
+ frameGeometries.push(brace);
149
+ }
150
+ }
151
+ }
152
+ const frameMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(frameGeometries), frameMaterial);
153
+ frameMesh.castShadow = true;
154
+ frameMesh.receiveShadow = true;
155
+ this.components3D.add(frameMesh);
156
+ // --- Rollers, Belt, or Rails ---
157
+ if (conveyorType === 'rail') {
158
+ this.buildRails(width, height, depth, frameH, railW, isVertical);
159
+ }
160
+ else if (conveyorType === 'belt') {
161
+ this.buildBelt(width, height, depth, frameH, railW, isVertical);
162
+ }
163
+ else {
164
+ this.buildRollers(width, height, depth, frameH, railW, rollWidth, isVertical);
165
+ }
166
+ }
167
+ buildRollers(width, height, depth, frameH, railW, rollWidth, isVertical = false) {
168
+ const rollerRadius = Math.max(rollWidth / 8, 1);
169
+ const diameter = rollerRadius * 2;
170
+ const step = diameter * 1.05;
171
+ const rollerY = depth - rollerRadius;
172
+ const color = this.resolveColor();
173
+ const rollerGeometries = [];
174
+ if (isVertical) {
175
+ // 롤러 축 = X축(가로), 이송 방향 = Z축(세로)
176
+ const rollerLength = width - railW * 2 - 0.5;
177
+ const count = Math.max(1, Math.floor(height / step));
178
+ const totalSpan = (count - 1) * step;
179
+ const startZ = -totalSpan / 2;
180
+ for (let i = 0; i < count; i++) {
181
+ const z = startZ + i * step;
182
+ const roller = new THREE.CylinderGeometry(rollerRadius, rollerRadius, rollerLength, 16);
183
+ roller.rotateZ(Math.PI / 2);
184
+ roller.translate(0, rollerY, z);
185
+ rollerGeometries.push(roller);
186
+ }
187
+ }
188
+ else {
189
+ // 롤러 축 = Z축(세로), 이송 방향 = X축(가로) — 기존 동작
190
+ const rollerLength = height - railW * 2 - 0.5;
191
+ const count = Math.max(1, Math.floor(width / step));
192
+ const totalSpan = (count - 1) * step;
193
+ const startX = -totalSpan / 2;
194
+ for (let i = 0; i < count; i++) {
195
+ const x = startX + i * step;
196
+ const roller = new THREE.CylinderGeometry(rollerRadius, rollerRadius, rollerLength, 16);
197
+ roller.rotateX(Math.PI / 2);
198
+ roller.translate(x, rollerY, 0);
199
+ rollerGeometries.push(roller);
200
+ }
201
+ }
202
+ if (rollerGeometries.length > 0) {
203
+ const rollerMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(rollerGeometries), new THREE.MeshStandardMaterial({ color, metalness: 0.9, roughness: 0.2 }));
204
+ rollerMesh.castShadow = true;
205
+ this.components3D.add(rollerMesh);
206
+ }
207
+ }
208
+ buildBelt(width, height, depth, frameH, railW, isVertical = false) {
209
+ const drumRadius = frameH * 0.38;
210
+ const beltY = depth - drumRadius;
211
+ const color = this.resolveColor();
212
+ // 이송 방향 길이와 벨트폭
213
+ const transportLen = isVertical ? height : width;
214
+ const beltWidth = isVertical ? width : height;
215
+ const drumLength = beltWidth - railW * 2;
216
+ // Two end drums
217
+ const drumGeometries = [];
218
+ const drumInset = drumRadius * 1.5;
219
+ const drum1 = -transportLen / 2 + drumInset;
220
+ const drum2 = transportLen / 2 - drumInset;
221
+ for (const dp of [drum1, drum2]) {
222
+ const drum = new THREE.CylinderGeometry(drumRadius, drumRadius, drumLength, 16);
223
+ if (isVertical) {
224
+ drum.rotateZ(Math.PI / 2);
225
+ drum.translate(0, beltY, dp);
226
+ }
227
+ else {
228
+ drum.rotateX(Math.PI / 2);
229
+ drum.translate(dp, beltY, 0);
230
+ }
231
+ drumGeometries.push(drum);
232
+ }
233
+ const drumMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(drumGeometries), new THREE.MeshStandardMaterial({ color: ROLLER_COLOR, metalness: 0.9, roughness: 0.2 }));
234
+ drumMesh.castShadow = true;
235
+ this.components3D.add(drumMesh);
236
+ // Belt surface
237
+ const beltThickness = drumRadius * 0.12;
238
+ const beltMaterial = new THREE.MeshStandardMaterial({
239
+ color: color,
240
+ metalness: 0.0,
241
+ roughness: 0.9,
242
+ });
243
+ // Flat belt top surface spanning between drums
244
+ const flatLen = drum2 - drum1;
245
+ const flatGeo = isVertical
246
+ ? new THREE.BoxGeometry(drumLength, beltThickness, flatLen)
247
+ : new THREE.BoxGeometry(flatLen, beltThickness, drumLength);
248
+ const flatMesh = new THREE.Mesh(flatGeo, beltMaterial);
249
+ flatMesh.position.set(0, beltY + drumRadius, 0);
250
+ flatMesh.castShadow = true;
251
+ this.components3D.add(flatMesh);
252
+ // Belt wrap around drums (segmented half-cylinder)
253
+ const wrapSegments = 12;
254
+ for (const [dp, angleStart] of [
255
+ [drum1, Math.PI / 2],
256
+ [drum2, -Math.PI / 2],
257
+ ]) {
258
+ const wrapGeometries = [];
259
+ for (let i = 0; i < wrapSegments; i++) {
260
+ const a0 = angleStart + (Math.PI * i) / wrapSegments;
261
+ const a1 = angleStart + (Math.PI * (i + 1)) / wrapSegments;
262
+ const r = drumRadius + beltThickness / 2;
263
+ const p0 = dp + Math.cos(a0) * r;
264
+ const y0 = beltY + Math.sin(a0) * r;
265
+ const p1 = dp + Math.cos(a1) * r;
266
+ const y1 = beltY + Math.sin(a1) * r;
267
+ const segLen = Math.sqrt((p1 - p0) ** 2 + (y1 - y0) ** 2) * 1.05;
268
+ const segAngle = Math.atan2(y1 - y0, p1 - p0);
269
+ if (isVertical) {
270
+ const seg = new THREE.BoxGeometry(drumLength, beltThickness, segLen);
271
+ seg.rotateX(-segAngle);
272
+ seg.translate(0, (y0 + y1) / 2, (p0 + p1) / 2);
273
+ wrapGeometries.push(seg);
274
+ }
275
+ else {
276
+ const seg = new THREE.BoxGeometry(segLen, beltThickness, drumLength);
277
+ seg.rotateZ(segAngle);
278
+ seg.translate((p0 + p1) / 2, (y0 + y1) / 2, 0);
279
+ wrapGeometries.push(seg);
280
+ }
281
+ }
282
+ const wrapMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(wrapGeometries), beltMaterial);
283
+ wrapMesh.castShadow = true;
284
+ this.components3D.add(wrapMesh);
285
+ }
286
+ // Flat belt bottom (return path)
287
+ const bottomGeo = isVertical
288
+ ? new THREE.BoxGeometry(drumLength * 0.85, beltThickness, flatLen)
289
+ : new THREE.BoxGeometry(flatLen, beltThickness, drumLength * 0.85);
290
+ const bottomMesh = new THREE.Mesh(bottomGeo, beltMaterial);
291
+ bottomMesh.position.set(0, beltY - drumRadius, 0);
292
+ this.components3D.add(bottomMesh);
293
+ }
294
+ buildRails(width, height, depth, frameH, railW, isVertical = false) {
295
+ const color = this.resolveColor();
296
+ const railHeight = frameH * 0.3;
297
+ const railY = depth - railHeight / 2;
298
+ const beltWidth = isVertical ? width : height;
299
+ const transportLen = isVertical ? height : width;
300
+ const innerWidth = beltWidth - railW * 2;
301
+ // 두 레일 간격: 내부 폭의 60%
302
+ const railSpacing = innerWidth * 0.6;
303
+ const railMaterial = new THREE.MeshStandardMaterial({
304
+ color,
305
+ metalness: 0.7,
306
+ roughness: 0.3,
307
+ });
308
+ const railGeometries = [];
309
+ // Two parallel rails running along transport direction
310
+ if (isVertical) {
311
+ for (const xSign of [-1, 1]) {
312
+ const rail = new THREE.BoxGeometry(railW * 0.8, railHeight, transportLen - railW * 2);
313
+ rail.translate(xSign * (railSpacing / 2), railY, 0);
314
+ railGeometries.push(rail);
315
+ }
316
+ }
317
+ else {
318
+ for (const zSign of [-1, 1]) {
319
+ const rail = new THREE.BoxGeometry(transportLen - railW * 2, railHeight, railW * 0.8);
320
+ rail.translate(0, railY, zSign * (railSpacing / 2));
321
+ railGeometries.push(rail);
322
+ }
323
+ }
324
+ const railMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(railGeometries), railMaterial);
325
+ railMesh.castShadow = true;
326
+ this.components3D.add(railMesh);
327
+ }
328
+ updateDimension() { }
329
+ updateAlpha() { }
330
+ onchange(after, before) {
331
+ if ('conveyorType' in after ||
332
+ 'orientation' in after ||
333
+ 'width' in after ||
334
+ 'height' in after ||
335
+ 'depth' in after ||
336
+ 'fillStyle' in after ||
337
+ 'strokeStyle' in after) {
338
+ this.update();
339
+ return;
340
+ }
341
+ super.onchange(after, before);
342
+ }
343
+ }
344
+ registerRealObjectFactory('Conveyor', (component) => new Conveyor3D(component));
345
+ //# sourceMappingURL=conveyor-3d.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"conveyor-3d.js","sourceRoot":"","sources":["../../src/factories/conveyor-3d.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAC9B,OAAO,KAAK,mBAAmB,MAAM,iDAAiD,CAAA;AACtF,OAAO,EAAE,eAAe,EAAE,yBAAyB,EAAE,MAAM,wBAAwB,CAAA;AAEnF,OAAO,EAAE,wBAAwB,EAAE,MAAM,iBAAiB,CAAA;AAC1D,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AAEtC,MAAM,WAAW,GAAG,QAAQ,CAAA;AAC5B,MAAM,YAAY,GAAG,QAAQ,CAAA;AAC7B,MAAM,aAAa,GAAG,QAAQ,CAAA;AAE9B,MAAM,OAAO,UAAW,SAAQ,eAAe;IAC7C,IAAI,SAAS,KAAa,OAAO,OAAO,CAAC,QAAQ,CAAA,CAAC,CAAC;IAEnD,IAAI,cAAc;QAChB,MAAM,EAAE,KAAK,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,CAAA;QACtC,OAAO,KAAK,IAAI,wBAAwB,CAAA;IAC1C,CAAC;IAEO,YAAY;QAClB,MAAM,IAAI,GAAG,IAAI,CAAC,SAAgB,CAAA;QAElC,kDAAkD;QAClD,4DAA4D;QAC5D,IAAI,CAAC;YACH,MAAM,EAAE,GAAG,IAAI,CAAC,WAAW,CAAA;YAC3B,IAAI,OAAO,EAAE,KAAK,QAAQ,IAAI,EAAE,IAAI,EAAE,CAAC,WAAW,EAAE,KAAK,SAAS,IAAI,EAAE,KAAK,aAAa;gBAAE,OAAO,IAAI,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,MAAM,EAAE,CAAA;QACjI,CAAC;QAAC,WAAM,CAAC;YACP,0BAA0B;QAC5B,CAAC;QAED,8BAA8B;QAC9B,MAAM,EAAE,SAAS,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,CAAA;QAC1C,IAAI,SAAS,IAAI,OAAO,SAAS,KAAK,QAAQ,IAAI,SAAS,KAAK,aAAa,EAAE,CAAC;YAC9E,IAAI,CAAC;gBACH,OAAO,IAAI,KAAK,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,MAAM,EAAE,CAAA;YAC5C,CAAC;YAAC,WAAM,CAAC;gBACP,gBAAgB;YAClB,CAAC;QACH,CAAC;QAED,OAAO,aAAa,CAAA;IACtB,CAAC;IAED,KAAK;QACH,KAAK,CAAC,KAAK,EAAE,CAAA;QAEb,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,YAAY,EAAE,WAAW,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,CAAA;QAC9F,MAAM,KAAK,GAAG,IAAI,CAAC,cAAc,CAAA;QACjC,IAAI,CAAC,QAAQ,IAAI,CAAC,SAAS;YAAE,OAAM;QAEnC,kEAAkE;QAClE,kCAAkC;QAClC,MAAM,UAAU,GAAG,WAAW,KAAK,UAAU,CAAC,CAAC,CAAC,IAAI;YAClD,CAAC,CAAC,WAAW,KAAK,YAAY,CAAC,CAAC,CAAC,KAAK;gBACtC,CAAC,CAAC,QAAQ,GAAG,SAAS,CAAA,CAAE,6BAA6B;QACvD,8CAA8C;QAC9C,+CAA+C;QAC/C,MAAM,KAAK,GAAG,QAAQ,CAAA;QACtB,MAAM,MAAM,GAAG,SAAS,CAAA;QAExB,2BAA2B;QAC3B,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC,CAAA;QAC3D,MAAM,cAAc,GAAG,IAAI,CAAC,GAAG,CAAC,SAAS,EAAE,CAAC,CAAC,CAAA;QAC7C,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,cAAc,EAAE,KAAK,CAAC,CAAA;QAC9C,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,GAAG,MAAM,EAAE,CAAC,CAAC,CAAA;QAExC,kDAAkD;QAClD,oDAAoD;QACpD,MAAM,SAAS,GAAG,UAAU,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAA;QAC7C,MAAM,OAAO,GAAG,UAAU,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAA;QAC3C,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,SAAS,GAAG,IAAI,EAAE,CAAC,CAAC,CAAA;QAC3C,MAAM,YAAY,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,GAAG,GAAG,EAAE,CAAC,CAAC,CAAA;QAE7C,MAAM,aAAa,GAAG,IAAI,KAAK,CAAC,oBAAoB,CAAC;YACnD,KAAK,EAAE,WAAW;YAClB,SAAS,EAAE,IAAI;YACf,SAAS,EAAE,IAAI;SAChB,CAAC,CAAA;QAEF,mDAAmD;QACnD,iDAAiD;QACjD,MAAM,eAAe,GAA2B,EAAE,CAAA;QAClD,MAAM,KAAK,GAAG,KAAK,GAAG,MAAM,GAAG,CAAC,CAAA;QAEhC,oCAAoC;QACpC,IAAI,UAAU,EAAE,CAAC;YACf,yBAAyB;YACzB,KAAK,MAAM,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC;gBAC5B,MAAM,IAAI,GAAG,IAAI,KAAK,CAAC,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,CAAA;gBACzD,IAAI,CAAC,SAAS,CAAC,KAAK,GAAG,CAAC,KAAK,GAAG,CAAC,GAAG,KAAK,GAAG,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,CAAA;gBACzD,eAAe,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;YAC5B,CAAC;QACH,CAAC;aAAM,CAAC;YACN,yBAAyB;YACzB,KAAK,MAAM,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC;gBAC5B,MAAM,IAAI,GAAG,IAAI,KAAK,CAAC,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,CAAC,CAAA;gBACxD,IAAI,CAAC,SAAS,CAAC,CAAC,EAAE,KAAK,EAAE,KAAK,GAAG,CAAC,MAAM,GAAG,CAAC,GAAG,KAAK,GAAG,CAAC,CAAC,CAAC,CAAA;gBAC1D,eAAe,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;YAC5B,CAAC;QACH,CAAC;QAED,oBAAoB;QACpB,IAAI,IAAI,GAAG,CAAC,EAAE,CAAC;YACb,MAAM,OAAO,GAAG,KAAK,GAAG,MAAM,CAAA;YAC9B,MAAM,UAAU,GAAG,OAAO,GAAG,IAAI,GAAG,CAAC,CAAA;YAErC,mBAAmB;YACnB,MAAM,OAAO,GAAG,UAAU,CAAC,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,GAAG,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,GAAG,YAAY,GAAG,CAAC,CAAC,CAAA;YACrF,MAAM,OAAO,GAAG,UAAU,CAAC,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,GAAG,YAAY,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,GAAG,KAAK,GAAG,CAAC,CAAC,CAAA;YAEvF,KAAK,MAAM,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC;gBAC5B,KAAK,MAAM,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC;oBAC5B,MAAM,GAAG,GAAG,IAAI,KAAK,CAAC,WAAW,CAAC,YAAY,EAAE,IAAI,EAAE,YAAY,CAAC,CAAA;oBACnE,GAAG,CAAC,SAAS,CAAC,KAAK,GAAG,OAAO,EAAE,UAAU,EAAE,KAAK,GAAG,OAAO,CAAC,CAAA;oBAC3D,eAAe,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;gBAC3B,CAAC;YACH,CAAC;YAED,gBAAgB;YAChB,MAAM,MAAM,GAAG,YAAY,GAAG,GAAG,CAAA;YACjC,MAAM,MAAM,GAAG,YAAY,GAAG,GAAG,CAAA;YACjC,MAAM,MAAM,GAAG,OAAO,GAAG,IAAI,GAAG,IAAI,CAAA;YAEpC,sBAAsB;YACtB,IAAI,UAAU,EAAE,CAAC;gBACf,KAAK,MAAM,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC;oBAC5B,MAAM,KAAK,GAAG,IAAI,KAAK,CAAC,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,YAAY,CAAC,CAAA;oBAC1E,KAAK,CAAC,SAAS,CAAC,KAAK,GAAG,OAAO,EAAE,MAAM,EAAE,CAAC,CAAC,CAAA;oBAC3C,eAAe,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;gBAC7B,CAAC;YACH,CAAC;iBAAM,CAAC;gBACN,KAAK,MAAM,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC;oBAC5B,MAAM,KAAK,GAAG,IAAI,KAAK,CAAC,WAAW,CAAC,KAAK,GAAG,YAAY,EAAE,MAAM,EAAE,MAAM,CAAC,CAAA;oBACzE,KAAK,CAAC,SAAS,CAAC,CAAC,EAAE,MAAM,EAAE,KAAK,GAAG,OAAO,CAAC,CAAA;oBAC3C,eAAe,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;gBAC7B,CAAC;YACH,CAAC;YAED,uBAAuB;YACvB,IAAI,UAAU,EAAE,CAAC;gBACf,KAAK,MAAM,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC;oBAC5B,MAAM,KAAK,GAAG,IAAI,KAAK,CAAC,WAAW,CAAC,KAAK,GAAG,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,CAAA;oBAClE,KAAK,CAAC,SAAS,CAAC,CAAC,EAAE,MAAM,EAAE,KAAK,GAAG,OAAO,CAAC,CAAA;oBAC3C,eAAe,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;gBAC7B,CAAC;YACH,CAAC;iBAAM,CAAC;gBACN,KAAK,MAAM,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC;oBAC5B,MAAM,KAAK,GAAG,IAAI,KAAK,CAAC,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,KAAK,CAAC,CAAA;oBACnE,KAAK,CAAC,SAAS,CAAC,KAAK,GAAG,OAAO,EAAE,MAAM,EAAE,CAAC,CAAC,CAAA;oBAC3C,eAAe,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;gBAC7B,CAAC;YACH,CAAC;QACH,CAAC;QAED,MAAM,SAAS,GAAG,IAAI,KAAK,CAAC,IAAI,CAAC,mBAAmB,CAAC,eAAe,CAAC,eAAe,CAAC,EAAE,aAAa,CAAC,CAAA;QACrG,SAAS,CAAC,UAAU,GAAG,IAAI,CAAA;QAC3B,SAAS,CAAC,aAAa,GAAG,IAAI,CAAA;QAC9B,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,SAAS,CAAC,CAAA;QAEhC,kCAAkC;QAClC,IAAI,YAAY,KAAK,MAAM,EAAE,CAAC;YAC5B,IAAI,CAAC,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,CAAC,CAAA;QAClE,CAAC;aAAM,IAAI,YAAY,KAAK,MAAM,EAAE,CAAC;YACnC,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,CAAC,CAAA;QACjE,CAAC;aAAM,CAAC;YACN,IAAI,CAAC,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,EAAE,UAAU,CAAC,CAAA;QAC/E,CAAC;IACH,CAAC;IAEO,YAAY,CAClB,KAAa,EACb,MAAc,EACd,KAAa,EACb,MAAc,EACd,KAAa,EACb,SAAiB,EACjB,aAAsB,KAAK;QAE3B,MAAM,YAAY,GAAG,IAAI,CAAC,GAAG,CAAC,SAAS,GAAG,CAAC,EAAE,CAAC,CAAC,CAAA;QAC/C,MAAM,QAAQ,GAAG,YAAY,GAAG,CAAC,CAAA;QACjC,MAAM,IAAI,GAAG,QAAQ,GAAG,IAAI,CAAA;QAC5B,MAAM,OAAO,GAAG,KAAK,GAAG,YAAY,CAAA;QACpC,MAAM,KAAK,GAAG,IAAI,CAAC,YAAY,EAAE,CAAA;QAEjC,MAAM,gBAAgB,GAA2B,EAAE,CAAA;QAEnD,IAAI,UAAU,EAAE,CAAC;YACf,gCAAgC;YAChC,MAAM,YAAY,GAAG,KAAK,GAAG,KAAK,GAAG,CAAC,GAAG,GAAG,CAAA;YAC5C,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC,CAAA;YACpD,MAAM,SAAS,GAAG,CAAC,KAAK,GAAG,CAAC,CAAC,GAAG,IAAI,CAAA;YACpC,MAAM,MAAM,GAAG,CAAC,SAAS,GAAG,CAAC,CAAA;YAE7B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,EAAE,CAAC,EAAE,EAAE,CAAC;gBAC/B,MAAM,CAAC,GAAG,MAAM,GAAG,CAAC,GAAG,IAAI,CAAA;gBAC3B,MAAM,MAAM,GAAG,IAAI,KAAK,CAAC,gBAAgB,CAAC,YAAY,EAAE,YAAY,EAAE,YAAY,EAAE,EAAE,CAAC,CAAA;gBACvF,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,GAAG,CAAC,CAAC,CAAA;gBAC3B,MAAM,CAAC,SAAS,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,CAAA;gBAC/B,gBAAgB,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;YAC/B,CAAC;QACH,CAAC;aAAM,CAAC;YACN,wCAAwC;YACxC,MAAM,YAAY,GAAG,MAAM,GAAG,KAAK,GAAG,CAAC,GAAG,GAAG,CAAA;YAC7C,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,IAAI,CAAC,CAAC,CAAA;YACnD,MAAM,SAAS,GAAG,CAAC,KAAK,GAAG,CAAC,CAAC,GAAG,IAAI,CAAA;YACpC,MAAM,MAAM,GAAG,CAAC,SAAS,GAAG,CAAC,CAAA;YAE7B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,EAAE,CAAC,EAAE,EAAE,CAAC;gBAC/B,MAAM,CAAC,GAAG,MAAM,GAAG,CAAC,GAAG,IAAI,CAAA;gBAC3B,MAAM,MAAM,GAAG,IAAI,KAAK,CAAC,gBAAgB,CAAC,YAAY,EAAE,YAAY,EAAE,YAAY,EAAE,EAAE,CAAC,CAAA;gBACvF,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,GAAG,CAAC,CAAC,CAAA;gBAC3B,MAAM,CAAC,SAAS,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,CAAA;gBAC/B,gBAAgB,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;YAC/B,CAAC;QACH,CAAC;QAED,IAAI,gBAAgB,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAChC,MAAM,UAAU,GAAG,IAAI,KAAK,CAAC,IAAI,CAC/B,mBAAmB,CAAC,eAAe,CAAC,gBAAgB,CAAC,EACrD,IAAI,KAAK,CAAC,oBAAoB,CAAC,EAAE,KAAK,EAAE,SAAS,EAAE,GAAG,EAAE,SAAS,EAAE,GAAG,EAAE,CAAC,CAC1E,CAAA;YACD,UAAU,CAAC,UAAU,GAAG,IAAI,CAAA;YAC5B,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,UAAU,CAAC,CAAA;QACnC,CAAC;IACH,CAAC;IAEO,SAAS,CAAC,KAAa,EAAE,MAAc,EAAE,KAAa,EAAE,MAAc,EAAE,KAAa,EAAE,aAAsB,KAAK;QACxH,MAAM,UAAU,GAAG,MAAM,GAAG,IAAI,CAAA;QAChC,MAAM,KAAK,GAAG,KAAK,GAAG,UAAU,CAAA;QAChC,MAAM,KAAK,GAAG,IAAI,CAAC,YAAY,EAAE,CAAA;QAEjC,gBAAgB;QAChB,MAAM,YAAY,GAAG,UAAU,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAA;QAChD,MAAM,SAAS,GAAG,UAAU,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAA;QAC7C,MAAM,UAAU,GAAG,SAAS,GAAG,KAAK,GAAG,CAAC,CAAA;QAExC,gBAAgB;QAChB,MAAM,cAAc,GAA2B,EAAE,CAAA;QACjD,MAAM,SAAS,GAAG,UAAU,GAAG,GAAG,CAAA;QAClC,MAAM,KAAK,GAAG,CAAC,YAAY,GAAG,CAAC,GAAG,SAAS,CAAA;QAC3C,MAAM,KAAK,GAAG,YAAY,GAAG,CAAC,GAAG,SAAS,CAAA;QAE1C,KAAK,MAAM,EAAE,IAAI,CAAC,KAAK,EAAE,KAAK,CAAC,EAAE,CAAC;YAChC,MAAM,IAAI,GAAG,IAAI,KAAK,CAAC,gBAAgB,CAAC,UAAU,EAAE,UAAU,EAAE,UAAU,EAAE,EAAE,CAAC,CAAA;YAC/E,IAAI,UAAU,EAAE,CAAC;gBACf,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,GAAG,CAAC,CAAC,CAAA;gBACzB,IAAI,CAAC,SAAS,CAAC,CAAC,EAAE,KAAK,EAAE,EAAE,CAAC,CAAA;YAC9B,CAAC;iBAAM,CAAC;gBACN,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,GAAG,CAAC,CAAC,CAAA;gBACzB,IAAI,CAAC,SAAS,CAAC,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC,CAAA;YAC9B,CAAC;YACD,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QAC3B,CAAC;QAED,MAAM,QAAQ,GAAG,IAAI,KAAK,CAAC,IAAI,CAC7B,mBAAmB,CAAC,eAAe,CAAC,cAAc,CAAC,EACnD,IAAI,KAAK,CAAC,oBAAoB,CAAC,EAAE,KAAK,EAAE,YAAY,EAAE,SAAS,EAAE,GAAG,EAAE,SAAS,EAAE,GAAG,EAAE,CAAC,CACxF,CAAA;QACD,QAAQ,CAAC,UAAU,GAAG,IAAI,CAAA;QAC1B,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAA;QAE/B,eAAe;QACf,MAAM,aAAa,GAAG,UAAU,GAAG,IAAI,CAAA;QACvC,MAAM,YAAY,GAAG,IAAI,KAAK,CAAC,oBAAoB,CAAC;YAClD,KAAK,EAAE,KAAK;YACZ,SAAS,EAAE,GAAG;YACd,SAAS,EAAE,GAAG;SACf,CAAC,CAAA;QAEF,+CAA+C;QAC/C,MAAM,OAAO,GAAG,KAAK,GAAG,KAAK,CAAA;QAC7B,MAAM,OAAO,GAAG,UAAU;YACxB,CAAC,CAAC,IAAI,KAAK,CAAC,WAAW,CAAC,UAAU,EAAE,aAAa,EAAE,OAAO,CAAC;YAC3D,CAAC,CAAC,IAAI,KAAK,CAAC,WAAW,CAAC,OAAO,EAAE,aAAa,EAAE,UAAU,CAAC,CAAA;QAC7D,MAAM,QAAQ,GAAG,IAAI,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,YAAY,CAAC,CAAA;QACtD,QAAQ,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,GAAG,UAAU,EAAE,CAAC,CAAC,CAAA;QAC/C,QAAQ,CAAC,UAAU,GAAG,IAAI,CAAA;QAC1B,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAA;QAE/B,mDAAmD;QACnD,MAAM,YAAY,GAAG,EAAE,CAAA;QACvB,KAAK,MAAM,CAAC,EAAE,EAAE,UAAU,CAAC,IAAI;YAC7B,CAAC,KAAK,EAAE,IAAI,CAAC,EAAE,GAAG,CAAC,CAAC;YACpB,CAAC,KAAK,EAAE,CAAC,IAAI,CAAC,EAAE,GAAG,CAAC,CAAC;SACA,EAAE,CAAC;YACxB,MAAM,cAAc,GAA2B,EAAE,CAAA;YACjD,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,YAAY,EAAE,CAAC,EAAE,EAAE,CAAC;gBACtC,MAAM,EAAE,GAAG,UAAU,GAAG,CAAC,IAAI,CAAC,EAAE,GAAG,CAAC,CAAC,GAAG,YAAY,CAAA;gBACpD,MAAM,EAAE,GAAG,UAAU,GAAG,CAAC,IAAI,CAAC,EAAE,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,YAAY,CAAA;gBAC1D,MAAM,CAAC,GAAG,UAAU,GAAG,aAAa,GAAG,CAAC,CAAA;gBAExC,MAAM,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,CAAA;gBAChC,MAAM,EAAE,GAAG,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,CAAA;gBACnC,MAAM,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,CAAA;gBAChC,MAAM,EAAE,GAAG,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,CAAA;gBAEnC,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,CAAC,GAAG,IAAI,CAAA;gBAChE,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,GAAG,EAAE,EAAE,EAAE,GAAG,EAAE,CAAC,CAAA;gBAE7C,IAAI,UAAU,EAAE,CAAC;oBACf,MAAM,GAAG,GAAG,IAAI,KAAK,CAAC,WAAW,CAAC,UAAU,EAAE,aAAa,EAAE,MAAM,CAAC,CAAA;oBACpE,GAAG,CAAC,OAAO,CAAC,CAAC,QAAQ,CAAC,CAAA;oBACtB,GAAG,CAAC,SAAS,CAAC,CAAC,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAA;oBAC9C,cAAc,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;gBAC1B,CAAC;qBAAM,CAAC;oBACN,MAAM,GAAG,GAAG,IAAI,KAAK,CAAC,WAAW,CAAC,MAAM,EAAE,aAAa,EAAE,UAAU,CAAC,CAAA;oBACpE,GAAG,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAA;oBACrB,GAAG,CAAC,SAAS,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAA;oBAC9C,cAAc,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;gBAC1B,CAAC;YACH,CAAC;YACD,MAAM,QAAQ,GAAG,IAAI,KAAK,CAAC,IAAI,CAAC,mBAAmB,CAAC,eAAe,CAAC,cAAc,CAAC,EAAE,YAAY,CAAC,CAAA;YAClG,QAAQ,CAAC,UAAU,GAAG,IAAI,CAAA;YAC1B,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAA;QACjC,CAAC;QAED,iCAAiC;QACjC,MAAM,SAAS,GAAG,UAAU;YAC1B,CAAC,CAAC,IAAI,KAAK,CAAC,WAAW,CAAC,UAAU,GAAG,IAAI,EAAE,aAAa,EAAE,OAAO,CAAC;YAClE,CAAC,CAAC,IAAI,KAAK,CAAC,WAAW,CAAC,OAAO,EAAE,aAAa,EAAE,UAAU,GAAG,IAAI,CAAC,CAAA;QACpE,MAAM,UAAU,GAAG,IAAI,KAAK,CAAC,IAAI,CAAC,SAAS,EAAE,YAAY,CAAC,CAAA;QAC1D,UAAU,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,GAAG,UAAU,EAAE,CAAC,CAAC,CAAA;QACjD,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,UAAU,CAAC,CAAA;IACnC,CAAC;IAEO,UAAU,CAAC,KAAa,EAAE,MAAc,EAAE,KAAa,EAAE,MAAc,EAAE,KAAa,EAAE,aAAsB,KAAK;QACzH,MAAM,KAAK,GAAG,IAAI,CAAC,YAAY,EAAE,CAAA;QACjC,MAAM,UAAU,GAAG,MAAM,GAAG,GAAG,CAAA;QAC/B,MAAM,KAAK,GAAG,KAAK,GAAG,UAAU,GAAG,CAAC,CAAA;QACpC,MAAM,SAAS,GAAG,UAAU,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAA;QAC7C,MAAM,YAAY,GAAG,UAAU,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAA;QAChD,MAAM,UAAU,GAAG,SAAS,GAAG,KAAK,GAAG,CAAC,CAAA;QACxC,qBAAqB;QACrB,MAAM,WAAW,GAAG,UAAU,GAAG,GAAG,CAAA;QAEpC,MAAM,YAAY,GAAG,IAAI,KAAK,CAAC,oBAAoB,CAAC;YAClD,KAAK;YACL,SAAS,EAAE,GAAG;YACd,SAAS,EAAE,GAAG;SACf,CAAC,CAAA;QAEF,MAAM,cAAc,GAA2B,EAAE,CAAA;QAEjD,uDAAuD;QACvD,IAAI,UAAU,EAAE,CAAC;YACf,KAAK,MAAM,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC;gBAC5B,MAAM,IAAI,GAAG,IAAI,KAAK,CAAC,WAAW,CAAC,KAAK,GAAG,GAAG,EAAE,UAAU,EAAE,YAAY,GAAG,KAAK,GAAG,CAAC,CAAC,CAAA;gBACrF,IAAI,CAAC,SAAS,CAAC,KAAK,GAAG,CAAC,WAAW,GAAG,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,CAAA;gBACnD,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;YAC3B,CAAC;QACH,CAAC;aAAM,CAAC;YACN,KAAK,MAAM,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC;gBAC5B,MAAM,IAAI,GAAG,IAAI,KAAK,CAAC,WAAW,CAAC,YAAY,GAAG,KAAK,GAAG,CAAC,EAAE,UAAU,EAAE,KAAK,GAAG,GAAG,CAAC,CAAA;gBACrF,IAAI,CAAC,SAAS,CAAC,CAAC,EAAE,KAAK,EAAE,KAAK,GAAG,CAAC,WAAW,GAAG,CAAC,CAAC,CAAC,CAAA;gBACnD,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;YAC3B,CAAC;QACH,CAAC;QAED,MAAM,QAAQ,GAAG,IAAI,KAAK,CAAC,IAAI,CAAC,mBAAmB,CAAC,eAAe,CAAC,cAAc,CAAC,EAAE,YAAY,CAAC,CAAA;QAClG,QAAQ,CAAC,UAAU,GAAG,IAAI,CAAA;QAC1B,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAA;IACjC,CAAC;IAED,eAAe,KAAI,CAAC;IACpB,WAAW,KAAI,CAAC;IAEhB,QAAQ,CAAC,KAA8B,EAAE,MAA+B;QACtE,IACE,cAAc,IAAI,KAAK;YACvB,aAAa,IAAI,KAAK;YACtB,OAAO,IAAI,KAAK;YAChB,QAAQ,IAAI,KAAK;YACjB,OAAO,IAAI,KAAK;YAChB,WAAW,IAAI,KAAK;YACpB,aAAa,IAAI,KAAK,EACtB,CAAC;YACD,IAAI,CAAC,MAAM,EAAE,CAAA;YACb,OAAM;QACR,CAAC;QACD,KAAK,CAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,CAAA;IAC/B,CAAC;CACF;AAED,yBAAyB,CAAC,UAAU,EAAE,CAAC,SAAoB,EAAE,EAAE,CAAC,IAAI,UAAU,CAAC,SAAS,CAAC,CAAC,CAAA","sourcesContent":["/**\n * Conveyor 3D Factory\n *\n * 'conveyor' type (default): side rails + cylindrical rollers + 4 legs + cross-bracing\n * 'belt' type: side rails + two end drums + belt wrapping + 4 legs + cross-bracing\n * 'rail' type: side rails + two parallel rails + 4 legs + cross-bracing\n *\n * Based on operato-scene conveyor-3d.ts, adapted for fmsim:\n * - conveyorType is string ('', 'conveyor', 'belt', 'rail') instead of numeric\n * - statusColor from MCSStatusMixin instead of value-based lookup\n * - rollWidth auto-calculated: Math.max(10, Math.min(width, height) / 2)\n */\n\nimport * as THREE from 'three'\nimport * as BufferGeometryUtils from 'three/examples/jsm/utils/BufferGeometryUtils.js'\nimport { RealObjectGroup, registerRealObjectFactory } from '@hatiolab/things-scene'\nimport type { Component } from '@hatiolab/things-scene'\nimport { TRANSPORT_SURFACE_HEIGHT } from './machine-3d.js'\nimport { Z_FMSIM } from './z-priority'\n\nconst FRAME_COLOR = 0x888899\nconst ROLLER_COLOR = 0xaaaabc\nconst DEFAULT_COLOR = 0x999999\n\nexport class Conveyor3D extends RealObjectGroup {\n get zPriority(): number { return Z_FMSIM.CONVEYOR }\n\n get effectiveDepth(): number {\n const { depth } = this.component.state\n return depth || TRANSPORT_SURFACE_HEIGHT\n }\n\n private resolveColor(): number {\n const comp = this.component as any\n\n // 1) MCSStatusMixin.statusColor (legend 기반 상태 색상)\n // '#F0F0F0'은 상태 미설정 시 기본 fallback — 무시하고 defaultColor 사용\n try {\n const sc = comp.statusColor\n if (typeof sc === 'string' && sc && sc.toUpperCase() !== '#F0F0F0' && sc !== 'transparent') return new THREE.Color(sc).getHex()\n } catch {\n /* statusColor 미구현 시 무시 */\n }\n\n // 2) state.fillStyle fallback\n const { fillStyle } = this.component.state\n if (fillStyle && typeof fillStyle === 'string' && fillStyle !== 'transparent') {\n try {\n return new THREE.Color(fillStyle).getHex()\n } catch {\n /* 파싱 실패 시 무시 */\n }\n }\n\n return DEFAULT_COLOR\n }\n\n build() {\n super.build()\n\n const { width: rawWidth, height: rawHeight, conveyorType, orientation } = this.component.state\n const depth = this.effectiveDepth\n if (!rawWidth || !rawHeight) return\n\n // orientation: 'horizontal' = 롤러가 Z축(세로), 'vertical' = 롤러가 X축(가로)\n // default = 긴 방향에 수직 (긴 축을 따라 이송)\n const isVertical = orientation === 'vertical' ? true\n : orientation === 'horizontal' ? false\n : rawWidth < rawHeight // default: 세로가 길면 롤러는 가로(X축)\n // isVertical=true → 롤러 축이 X(가로), 이송 방향이 Z(세로)\n // isVertical=false → 롤러 축이 Z(세로), 이송 방향이 X(가로)\n const width = rawWidth\n const height = rawHeight\n\n // 2D와 동일 로직으로 rollWidth 산출\n const rollWidth = Math.max(10, Math.min(width, height) / 2)\n const rollerDiameter = Math.max(rollWidth, 2)\n const frameH = Math.min(rollerDiameter, depth)\n const legH = Math.max(depth - frameH, 0)\n\n // isVertical일 때 이송 방향이 Z축: 레일은 Z축을 따라, 벨트폭은 width\n // !isVertical일 때 이송 방향이 X축: 레일은 X축을 따라, 벨트폭은 height\n const beltWidth = isVertical ? width : height\n const railLen = isVertical ? height : width\n const railW = Math.max(beltWidth * 0.06, 2)\n const legThickness = Math.max(railW * 0.8, 2)\n\n const frameMaterial = new THREE.MeshStandardMaterial({\n color: FRAME_COLOR,\n metalness: 0.85,\n roughness: 0.35,\n })\n\n // --- Frame: side rails + legs + cross-bracing ---\n // Y 좌표: y=0(바닥) ~ y=depth(상단). 레일 상단이 depth에 위치.\n const frameGeometries: THREE.BufferGeometry[] = []\n const railY = depth - frameH / 2\n\n // Side rails — 이송 방향을 따라, 벨트 양측에 배치\n if (isVertical) {\n // 레일이 Z축(세로)을 따라, ±X에 배치\n for (const xSign of [-1, 1]) {\n const rail = new THREE.BoxGeometry(railW, frameH, height)\n rail.translate(xSign * (width / 2 - railW / 2), railY, 0)\n frameGeometries.push(rail)\n }\n } else {\n // 레일이 X축(가로)을 따라, ±Z에 배치\n for (const zSign of [-1, 1]) {\n const rail = new THREE.BoxGeometry(width, frameH, railW)\n rail.translate(0, railY, zSign * (height / 2 - railW / 2))\n frameGeometries.push(rail)\n }\n }\n\n // 4 legs at corners\n if (legH > 0) {\n const legTopY = depth - frameH\n const legCenterY = legTopY - legH / 2\n\n // 다리 위치: 레일 안쪽에 정렬\n const legXPos = isVertical ? (width / 2 - railW / 2) : (width / 2 - legThickness / 2)\n const legZPos = isVertical ? (height / 2 - legThickness / 2) : (height / 2 - railW / 2)\n\n for (const xSign of [-1, 1]) {\n for (const zSign of [-1, 1]) {\n const leg = new THREE.BoxGeometry(legThickness, legH, legThickness)\n leg.translate(xSign * legXPos, legCenterY, zSign * legZPos)\n frameGeometries.push(leg)\n }\n }\n\n // Cross-bracing\n const braceH = legThickness * 0.6\n const braceW = legThickness * 0.6\n const braceY = legTopY - legH * 0.35\n\n // 이송 방향 브레이스 (레일과 평행)\n if (isVertical) {\n for (const xSign of [-1, 1]) {\n const brace = new THREE.BoxGeometry(braceW, braceH, height - legThickness)\n brace.translate(xSign * legXPos, braceY, 0)\n frameGeometries.push(brace)\n }\n } else {\n for (const zSign of [-1, 1]) {\n const brace = new THREE.BoxGeometry(width - legThickness, braceH, braceW)\n brace.translate(0, braceY, zSign * legZPos)\n frameGeometries.push(brace)\n }\n }\n\n // 벨트폭 방향 브레이스 (레일과 수직)\n if (isVertical) {\n for (const zSign of [-1, 1]) {\n const brace = new THREE.BoxGeometry(width - railW, braceH, braceW)\n brace.translate(0, braceY, zSign * legZPos)\n frameGeometries.push(brace)\n }\n } else {\n for (const xSign of [-1, 1]) {\n const brace = new THREE.BoxGeometry(braceW, braceH, height - railW)\n brace.translate(xSign * legXPos, braceY, 0)\n frameGeometries.push(brace)\n }\n }\n }\n\n const frameMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(frameGeometries), frameMaterial)\n frameMesh.castShadow = true\n frameMesh.receiveShadow = true\n this.components3D.add(frameMesh)\n\n // --- Rollers, Belt, or Rails ---\n if (conveyorType === 'rail') {\n this.buildRails(width, height, depth, frameH, railW, isVertical)\n } else if (conveyorType === 'belt') {\n this.buildBelt(width, height, depth, frameH, railW, isVertical)\n } else {\n this.buildRollers(width, height, depth, frameH, railW, rollWidth, isVertical)\n }\n }\n\n private buildRollers(\n width: number,\n height: number,\n depth: number,\n frameH: number,\n railW: number,\n rollWidth: number,\n isVertical: boolean = false\n ) {\n const rollerRadius = Math.max(rollWidth / 8, 1)\n const diameter = rollerRadius * 2\n const step = diameter * 1.05\n const rollerY = depth - rollerRadius\n const color = this.resolveColor()\n\n const rollerGeometries: THREE.BufferGeometry[] = []\n\n if (isVertical) {\n // 롤러 축 = X축(가로), 이송 방향 = Z축(세로)\n const rollerLength = width - railW * 2 - 0.5\n const count = Math.max(1, Math.floor(height / step))\n const totalSpan = (count - 1) * step\n const startZ = -totalSpan / 2\n\n for (let i = 0; i < count; i++) {\n const z = startZ + i * step\n const roller = new THREE.CylinderGeometry(rollerRadius, rollerRadius, rollerLength, 16)\n roller.rotateZ(Math.PI / 2)\n roller.translate(0, rollerY, z)\n rollerGeometries.push(roller)\n }\n } else {\n // 롤러 축 = Z축(세로), 이송 방향 = X축(가로) — 기존 동작\n const rollerLength = height - railW * 2 - 0.5\n const count = Math.max(1, Math.floor(width / step))\n const totalSpan = (count - 1) * step\n const startX = -totalSpan / 2\n\n for (let i = 0; i < count; i++) {\n const x = startX + i * step\n const roller = new THREE.CylinderGeometry(rollerRadius, rollerRadius, rollerLength, 16)\n roller.rotateX(Math.PI / 2)\n roller.translate(x, rollerY, 0)\n rollerGeometries.push(roller)\n }\n }\n\n if (rollerGeometries.length > 0) {\n const rollerMesh = new THREE.Mesh(\n BufferGeometryUtils.mergeGeometries(rollerGeometries),\n new THREE.MeshStandardMaterial({ color, metalness: 0.9, roughness: 0.2 })\n )\n rollerMesh.castShadow = true\n this.components3D.add(rollerMesh)\n }\n }\n\n private buildBelt(width: number, height: number, depth: number, frameH: number, railW: number, isVertical: boolean = false) {\n const drumRadius = frameH * 0.38\n const beltY = depth - drumRadius\n const color = this.resolveColor()\n\n // 이송 방향 길이와 벨트폭\n const transportLen = isVertical ? height : width\n const beltWidth = isVertical ? width : height\n const drumLength = beltWidth - railW * 2\n\n // Two end drums\n const drumGeometries: THREE.BufferGeometry[] = []\n const drumInset = drumRadius * 1.5\n const drum1 = -transportLen / 2 + drumInset\n const drum2 = transportLen / 2 - drumInset\n\n for (const dp of [drum1, drum2]) {\n const drum = new THREE.CylinderGeometry(drumRadius, drumRadius, drumLength, 16)\n if (isVertical) {\n drum.rotateZ(Math.PI / 2)\n drum.translate(0, beltY, dp)\n } else {\n drum.rotateX(Math.PI / 2)\n drum.translate(dp, beltY, 0)\n }\n drumGeometries.push(drum)\n }\n\n const drumMesh = new THREE.Mesh(\n BufferGeometryUtils.mergeGeometries(drumGeometries),\n new THREE.MeshStandardMaterial({ color: ROLLER_COLOR, metalness: 0.9, roughness: 0.2 })\n )\n drumMesh.castShadow = true\n this.components3D.add(drumMesh)\n\n // Belt surface\n const beltThickness = drumRadius * 0.12\n const beltMaterial = new THREE.MeshStandardMaterial({\n color: color,\n metalness: 0.0,\n roughness: 0.9,\n })\n\n // Flat belt top surface spanning between drums\n const flatLen = drum2 - drum1\n const flatGeo = isVertical\n ? new THREE.BoxGeometry(drumLength, beltThickness, flatLen)\n : new THREE.BoxGeometry(flatLen, beltThickness, drumLength)\n const flatMesh = new THREE.Mesh(flatGeo, beltMaterial)\n flatMesh.position.set(0, beltY + drumRadius, 0)\n flatMesh.castShadow = true\n this.components3D.add(flatMesh)\n\n // Belt wrap around drums (segmented half-cylinder)\n const wrapSegments = 12\n for (const [dp, angleStart] of [\n [drum1, Math.PI / 2],\n [drum2, -Math.PI / 2],\n ] as [number, number][]) {\n const wrapGeometries: THREE.BufferGeometry[] = []\n for (let i = 0; i < wrapSegments; i++) {\n const a0 = angleStart + (Math.PI * i) / wrapSegments\n const a1 = angleStart + (Math.PI * (i + 1)) / wrapSegments\n const r = drumRadius + beltThickness / 2\n\n const p0 = dp + Math.cos(a0) * r\n const y0 = beltY + Math.sin(a0) * r\n const p1 = dp + Math.cos(a1) * r\n const y1 = beltY + Math.sin(a1) * r\n\n const segLen = Math.sqrt((p1 - p0) ** 2 + (y1 - y0) ** 2) * 1.05\n const segAngle = Math.atan2(y1 - y0, p1 - p0)\n\n if (isVertical) {\n const seg = new THREE.BoxGeometry(drumLength, beltThickness, segLen)\n seg.rotateX(-segAngle)\n seg.translate(0, (y0 + y1) / 2, (p0 + p1) / 2)\n wrapGeometries.push(seg)\n } else {\n const seg = new THREE.BoxGeometry(segLen, beltThickness, drumLength)\n seg.rotateZ(segAngle)\n seg.translate((p0 + p1) / 2, (y0 + y1) / 2, 0)\n wrapGeometries.push(seg)\n }\n }\n const wrapMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(wrapGeometries), beltMaterial)\n wrapMesh.castShadow = true\n this.components3D.add(wrapMesh)\n }\n\n // Flat belt bottom (return path)\n const bottomGeo = isVertical\n ? new THREE.BoxGeometry(drumLength * 0.85, beltThickness, flatLen)\n : new THREE.BoxGeometry(flatLen, beltThickness, drumLength * 0.85)\n const bottomMesh = new THREE.Mesh(bottomGeo, beltMaterial)\n bottomMesh.position.set(0, beltY - drumRadius, 0)\n this.components3D.add(bottomMesh)\n }\n\n private buildRails(width: number, height: number, depth: number, frameH: number, railW: number, isVertical: boolean = false) {\n const color = this.resolveColor()\n const railHeight = frameH * 0.3\n const railY = depth - railHeight / 2\n const beltWidth = isVertical ? width : height\n const transportLen = isVertical ? height : width\n const innerWidth = beltWidth - railW * 2\n // 두 레일 간격: 내부 폭의 60%\n const railSpacing = innerWidth * 0.6\n\n const railMaterial = new THREE.MeshStandardMaterial({\n color,\n metalness: 0.7,\n roughness: 0.3,\n })\n\n const railGeometries: THREE.BufferGeometry[] = []\n\n // Two parallel rails running along transport direction\n if (isVertical) {\n for (const xSign of [-1, 1]) {\n const rail = new THREE.BoxGeometry(railW * 0.8, railHeight, transportLen - railW * 2)\n rail.translate(xSign * (railSpacing / 2), railY, 0)\n railGeometries.push(rail)\n }\n } else {\n for (const zSign of [-1, 1]) {\n const rail = new THREE.BoxGeometry(transportLen - railW * 2, railHeight, railW * 0.8)\n rail.translate(0, railY, zSign * (railSpacing / 2))\n railGeometries.push(rail)\n }\n }\n\n const railMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(railGeometries), railMaterial)\n railMesh.castShadow = true\n this.components3D.add(railMesh)\n }\n\n updateDimension() {}\n updateAlpha() {}\n\n onchange(after: Record<string, unknown>, before: Record<string, unknown>) {\n if (\n 'conveyorType' in after ||\n 'orientation' in after ||\n 'width' in after ||\n 'height' in after ||\n 'depth' in after ||\n 'fillStyle' in after ||\n 'strokeStyle' in after\n ) {\n this.update()\n return\n }\n super.onchange(after, before)\n }\n}\n\nregisterRealObjectFactory('Conveyor', (component: Component) => new Conveyor3D(component))\n"]}