@operato/scene-visualizer 10.0.0-beta.8 → 10.0.0-beta.9

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.
@@ -0,0 +1,352 @@
1
+ /*
2
+ * Copyright © HatioLab Inc. All rights reserved.
3
+ *
4
+ * Stocker 3D — aisle, rail, crane, ports, enclosure, rack frames, cell stocks.
5
+ */
6
+ import * as THREE from 'three';
7
+ import * as BufferGeometryUtils from 'three/examples/jsm/utils/BufferGeometryUtils.js';
8
+ import { RealObjectGroup } from '@hatiolab/things-scene';
9
+ import { computeLayout } from './stocker.js';
10
+ const FRAME_COLOR = 0x8a8a8a;
11
+ const BOARD_COLOR = 0xcccccc;
12
+ const AISLE_COLOR = 0xd4d4c8;
13
+ const RAIL_COLOR = 0x666666;
14
+ const CRANE_COLOR = 0xff6600;
15
+ const CRANE_MAST_COLOR = 0xcc5500;
16
+ const FORK_COLOR = 0xddaa00;
17
+ const CELL_EMPTY_COLOR = 0xf0f0f0;
18
+ const CELL_FULL_COLOR = 0x4a9eff;
19
+ const CELL_RESERVED_COLOR = 0xffcc00;
20
+ const CELL_ERROR_COLOR = 0xe74c3c;
21
+ function cellColor3d(status) {
22
+ switch (status) {
23
+ case 'FULL': return CELL_FULL_COLOR;
24
+ case 'RESERVED': return CELL_RESERVED_COLOR;
25
+ case 'ERROR': return CELL_ERROR_COLOR;
26
+ default: return CELL_EMPTY_COLOR;
27
+ }
28
+ }
29
+ export class Stocker3d extends RealObjectGroup {
30
+ _cranes = [];
31
+ _cellMeshes = [];
32
+ _animRaf = 0;
33
+ get stocker() {
34
+ return this.component;
35
+ }
36
+ get position() {
37
+ const { zPos = 0 } = this.component.state;
38
+ return { x: this.cx || 0, y: zPos, z: this.cy || 0 };
39
+ }
40
+ build() {
41
+ super.build();
42
+ const state = this.component.state;
43
+ const { width = 200, height = 100, depth: totalHeight = 80, rotation = 0, aisleRatio = 0.15 } = state;
44
+ const layout = computeLayout(state);
45
+ let orientY = -rotation;
46
+ if (layout.vertical)
47
+ orientY += Math.PI / 2;
48
+ if (layout.flipped)
49
+ orientY += Math.PI;
50
+ this.object3d.rotation.y = orientY;
51
+ const alongSize = layout.along;
52
+ const acrossSize = layout.across;
53
+ const aisleDepth = acrossSize * aisleRatio;
54
+ const rackAreaDepth = acrossSize - aisleDepth;
55
+ const leftRackDepth = layout.leftRack ? rackAreaDepth / (layout.rightRack ? 2 : 1) : 0;
56
+ const rightRackDepth = layout.rightRack ? rackAreaDepth / (layout.leftRack ? 2 : 1) : 0;
57
+ const frameGeoms = [];
58
+ const boardGeoms = [];
59
+ // ── Rack sides ──
60
+ if (layout.leftRack) {
61
+ const rackZ = -(aisleDepth / 2 + leftRackDepth / 2);
62
+ this._buildRackSide('L', layout.leftRack.config, alongSize, totalHeight, leftRackDepth, rackZ, frameGeoms, boardGeoms);
63
+ }
64
+ if (layout.rightRack) {
65
+ const rackZ = aisleDepth / 2 + rightRackDepth / 2;
66
+ this._buildRackSide('R', layout.rightRack.config, alongSize, totalHeight, rightRackDepth, rackZ, frameGeoms, boardGeoms);
67
+ }
68
+ // merge frames
69
+ if (!state.hideRackFrame && frameGeoms.length > 0) {
70
+ const mat = new THREE.MeshStandardMaterial({ color: FRAME_COLOR, metalness: 0.85, roughness: 0.35, transparent: true, opacity: 0.8 });
71
+ const mesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(frameGeoms), mat);
72
+ mesh.castShadow = true;
73
+ this.object3d.add(mesh);
74
+ }
75
+ if (!state.hideRackFrame && boardGeoms.length > 0) {
76
+ const mat = new THREE.MeshStandardMaterial({ color: BOARD_COLOR, side: THREE.DoubleSide, transparent: true, opacity: 0.5 });
77
+ this.object3d.add(new THREE.Mesh(BufferGeometryUtils.mergeGeometries(boardGeoms), mat));
78
+ }
79
+ // ── Enclosure ──
80
+ const { fillStyle } = state;
81
+ if (fillStyle && fillStyle !== 'transparent' && fillStyle !== 'rgba(0,0,0,0)') {
82
+ this._buildEnclosure(alongSize, acrossSize, totalHeight, fillStyle);
83
+ }
84
+ // ── Aisle ──
85
+ const aisleGeom = new THREE.PlaneGeometry(alongSize, aisleDepth);
86
+ aisleGeom.rotateX(-Math.PI / 2);
87
+ const aisleMesh = new THREE.Mesh(aisleGeom, new THREE.MeshStandardMaterial({ color: AISLE_COLOR, roughness: 0.9 }));
88
+ aisleMesh.position.y = 0.1;
89
+ aisleMesh.receiveShadow = true;
90
+ this.object3d.add(aisleMesh);
91
+ // rail
92
+ const railMesh = new THREE.Mesh(new THREE.BoxGeometry(alongSize, 1, 2), new THREE.MeshStandardMaterial({ color: RAIL_COLOR, metalness: 0.8, roughness: 0.3 }));
93
+ railMesh.position.set(0, 0.5, 0);
94
+ this.object3d.add(railMesh);
95
+ // ── Cranes ──
96
+ const maxBays = layout.maxBays;
97
+ const levels = Math.max(state.lLevels || 0, state.rLevels || 0, 1);
98
+ this._buildCranes(alongSize, totalHeight, maxBays, levels, aisleDepth, leftRackDepth, rightRackDepth);
99
+ }
100
+ _buildRackSide(side, config, totalWidth, totalHeight, rackDepth, rackZ, frameGeoms, boardGeoms) {
101
+ const { bays, levels, depthCount } = config;
102
+ const bayWidth = totalWidth / bays;
103
+ const levelHeight = totalHeight / levels;
104
+ const cellDepthSize = rackDepth / depthCount;
105
+ const postW = Math.min(bayWidth * 0.05, 2);
106
+ const inset = 3;
107
+ const hw = totalWidth / 2 - inset;
108
+ const hd = rackDepth / 2 - inset;
109
+ // vertical posts — 수평바 + 1/2 두께로 접합부 갭 채움
110
+ const topRailY = totalHeight - inset;
111
+ const bottomRailY = inset;
112
+ const postHeight = topRailY - bottomRailY + postW;
113
+ const postGeom = new THREE.BoxGeometry(postW, postHeight, postW);
114
+ for (const [px, pz] of [[-hw, rackZ - hd], [-hw, rackZ + hd], [hw, rackZ - hd], [hw, rackZ + hd]]) {
115
+ const g = postGeom.clone();
116
+ g.translate(px, bottomRailY - postW / 2 + postHeight / 2, pz);
117
+ frameGeoms.push(g);
118
+ }
119
+ // horizontal rails — 좌우 inset, 상하는 enclosure 안쪽에 위치
120
+ const railW = totalWidth - inset * 2;
121
+ const railAlongGeom = new THREE.BoxGeometry(railW, postW, postW);
122
+ const railDepthW = rackDepth - inset * 2;
123
+ const railDepthGeom = new THREE.BoxGeometry(postW, postW, railDepthW);
124
+ for (let l = 0; l <= levels; l++) {
125
+ let y = l * levelHeight;
126
+ // bottom은 enclosure 바닥에서 올라오고, top은 천장에서 내려옴
127
+ if (l === 0)
128
+ y = inset;
129
+ else if (l === levels)
130
+ y = totalHeight - inset;
131
+ // along 방향 (전면/후면)
132
+ for (const dz of [rackZ - hd, rackZ + hd]) {
133
+ const g = railAlongGeom.clone();
134
+ g.translate(0, y, dz);
135
+ frameGeoms.push(g);
136
+ }
137
+ // depth 방향 (좌측/우측 끝)
138
+ for (const px of [-hw, hw]) {
139
+ const g = railDepthGeom.clone();
140
+ g.translate(px, y, rackZ);
141
+ frameGeoms.push(g);
142
+ }
143
+ }
144
+ // shelf boards
145
+ const boardW = totalWidth - inset * 2;
146
+ const boardD = rackDepth - inset * 2;
147
+ for (let l = 0; l < levels; l++) {
148
+ const y = l * levelHeight + 0.5;
149
+ const g = new THREE.PlaneGeometry(boardW, boardD);
150
+ g.rotateX(-Math.PI / 2);
151
+ g.translate(0, y, rackZ);
152
+ boardGeoms.push(g);
153
+ }
154
+ // cell stock
155
+ for (let l = 1; l <= levels; l++) {
156
+ for (let b = 1; b <= bays; b++) {
157
+ for (let d = 1; d <= depthCount; d++) {
158
+ const status = this.stocker.getCellStatus(side, b, l, d);
159
+ const geom = new THREE.BoxGeometry(bayWidth * 0.7, levelHeight * 0.7, cellDepthSize * 0.7);
160
+ const mat = new THREE.MeshStandardMaterial({
161
+ color: cellColor3d(status), metalness: 0.1, roughness: 0.8,
162
+ transparent: !status || status === 'EMPTY',
163
+ opacity: (!status || status === 'EMPTY') ? 0.15 : 1
164
+ });
165
+ const mesh = new THREE.Mesh(geom, mat);
166
+ mesh.position.set(-totalWidth / 2 + (b - 0.5) * bayWidth, (l - 0.5) * levelHeight, rackZ + (d - 0.5 - depthCount / 2) * cellDepthSize);
167
+ mesh.name = this.stocker.getLocationId(side, b, l, d);
168
+ this._cellMeshes.push(mesh);
169
+ this.object3d.add(mesh);
170
+ }
171
+ }
172
+ }
173
+ }
174
+ _buildEnclosure(alongSize, acrossSize, totalHeight, fillStyle) {
175
+ let color = 0xcccccc;
176
+ let opacity = 0.4;
177
+ // rgba에서 alpha 추출, 색상만 THREE.Color로
178
+ const rgbaMatch = fillStyle.match(/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*([\d.]+))?\s*\)/);
179
+ if (rgbaMatch) {
180
+ const [, r, g, b, a] = rgbaMatch;
181
+ color = new THREE.Color(parseInt(r) / 255, parseInt(g) / 255, parseInt(b) / 255).getHex();
182
+ if (a !== undefined)
183
+ opacity = parseFloat(a);
184
+ }
185
+ else {
186
+ try {
187
+ color = new THREE.Color(fillStyle).getHex();
188
+ }
189
+ catch { }
190
+ }
191
+ const mat = new THREE.MeshStandardMaterial({ color, transparent: true, opacity, side: THREE.DoubleSide, depthWrite: false });
192
+ const halfW = alongSize / 2;
193
+ const halfD = acrossSize / 2;
194
+ for (const [w, h, x, y, z, ry] of [
195
+ [alongSize, totalHeight, 0, totalHeight / 2, -halfD, 0],
196
+ [alongSize, totalHeight, 0, totalHeight / 2, halfD, 0],
197
+ [acrossSize, totalHeight, -halfW, totalHeight / 2, 0, Math.PI / 2],
198
+ [acrossSize, totalHeight, halfW, totalHeight / 2, 0, Math.PI / 2]
199
+ ]) {
200
+ const mesh = new THREE.Mesh(new THREE.PlaneGeometry(w, h), mat);
201
+ mesh.position.set(x, y, z);
202
+ mesh.rotation.y = ry;
203
+ this.object3d.add(mesh);
204
+ }
205
+ const roofGeom = new THREE.PlaneGeometry(alongSize, acrossSize);
206
+ roofGeom.rotateX(-Math.PI / 2);
207
+ const roofMesh = new THREE.Mesh(roofGeom, mat);
208
+ roofMesh.position.y = totalHeight;
209
+ this.object3d.add(roofMesh);
210
+ }
211
+ _buildCranes(alongSize, totalHeight, maxBays, levels, aisleDepth, leftRackDepth, rightRackDepth) {
212
+ const cranesData = this.stocker.cranesData;
213
+ const craneCount = Math.max(cranesData.length, this.component.state.cranes || 1);
214
+ const bayWidth = alongSize / maxBays;
215
+ const levelHeight = totalHeight / levels;
216
+ const craneHeight = totalHeight * 0.95;
217
+ const mastMat = new THREE.MeshStandardMaterial({ color: CRANE_MAST_COLOR, metalness: 0.7, roughness: 0.4 });
218
+ const carriageMat = new THREE.MeshStandardMaterial({ color: CRANE_COLOR, metalness: 0.3, roughness: 0.6 });
219
+ const forkMat = new THREE.MeshStandardMaterial({ color: FORK_COLOR, metalness: 0.5, roughness: 0.5 });
220
+ for (let i = 0; i < craneCount; i++) {
221
+ const cd = cranesData[i];
222
+ const bay = cd?.bay ?? Math.round((i + 1) * maxBays / (craneCount + 1));
223
+ const level = cd?.level ?? 1;
224
+ const side = cd?.side;
225
+ const status = cd?.status || 'IDLE';
226
+ const craneX = -alongSize / 2 + (bay - 0.5) * bayWidth;
227
+ const craneY = (level - 0.5) * levelHeight;
228
+ let targetForkZ = 0;
229
+ if ((status === 'PICKING' || status === 'PLACING') && side) {
230
+ targetForkZ = side === 'L' ? -(aisleDepth / 2 + leftRackDepth / 2) : (aisleDepth / 2 + rightRackDepth / 2);
231
+ }
232
+ const mastGroup = new THREE.Group();
233
+ mastGroup.position.x = craneX;
234
+ this.object3d.add(mastGroup);
235
+ const mastMesh = new THREE.Mesh(new THREE.BoxGeometry(3, craneHeight, 3), mastMat);
236
+ mastMesh.position.y = craneHeight / 2;
237
+ mastMesh.castShadow = true;
238
+ mastGroup.add(mastMesh);
239
+ const topBeam = new THREE.Mesh(new THREE.BoxGeometry(bayWidth * 0.3, 2, aisleDepth * 0.8), mastMat);
240
+ topBeam.position.y = craneHeight - 1;
241
+ mastGroup.add(topBeam);
242
+ const carriageGroup = new THREE.Group();
243
+ carriageGroup.position.y = craneY;
244
+ mastGroup.add(carriageGroup);
245
+ const carrW = bayWidth * 0.7, carrH = 4, carrD = aisleDepth * 0.5;
246
+ const carriageMesh = new THREE.Mesh(new THREE.BoxGeometry(carrW, carrH, carrD), carriageMat);
247
+ carriageMesh.castShadow = true;
248
+ carriageGroup.add(carriageMesh);
249
+ const forkGroup = new THREE.Group();
250
+ carriageGroup.add(forkGroup);
251
+ const forkLength = Math.max(leftRackDepth, rightRackDepth) * 0.8;
252
+ const forkW = carrW * 0.15, forkH = 1.5;
253
+ const prongGeom = new THREE.BoxGeometry(forkW, forkH, forkLength);
254
+ const lp = new THREE.Mesh(prongGeom, forkMat);
255
+ lp.position.set(-carrW * 0.25, -carrH / 2 + forkH / 2, 0);
256
+ forkGroup.add(lp);
257
+ const rp = new THREE.Mesh(prongGeom.clone(), forkMat);
258
+ rp.position.set(carrW * 0.25, -carrH / 2 + forkH / 2, 0);
259
+ forkGroup.add(rp);
260
+ const baseMesh = new THREE.Mesh(new THREE.BoxGeometry(carrW * 0.7, forkH, forkW), forkMat);
261
+ baseMesh.position.set(0, -carrH / 2 + forkH / 2, -forkLength / 2 + forkW / 2);
262
+ forkGroup.add(baseMesh);
263
+ this._cranes.push({
264
+ mastGroup, carriageGroup, forkGroup,
265
+ state: { x: craneX, y: craneY, forkZ: 0, targetX: craneX, targetY: craneY, targetForkZ, bayWidth, levelHeight, maxBays, levels, leftRackDepth, rightRackDepth, aisleDepth }
266
+ });
267
+ }
268
+ if (this._cranes.some(c => c.state.targetForkZ !== 0))
269
+ this._startCraneAnim();
270
+ }
271
+ updateCraneTargets() {
272
+ const cranesData = this.stocker.cranesData;
273
+ const alongSize = this.stocker.layout.along;
274
+ for (let i = 0; i < this._cranes.length; i++) {
275
+ const crane = this._cranes[i];
276
+ const cd = cranesData[i];
277
+ const s = crane.state;
278
+ const bay = cd?.bay ?? Math.round((i + 1) * s.maxBays / (this._cranes.length + 1));
279
+ const level = cd?.level ?? 1;
280
+ const side = cd?.side;
281
+ const status = cd?.status || 'IDLE';
282
+ s.targetX = -alongSize / 2 + (bay - 0.5) * s.bayWidth;
283
+ s.targetY = (level - 0.5) * s.levelHeight;
284
+ if ((status === 'PICKING' || status === 'PLACING') && side) {
285
+ s.targetForkZ = side === 'L' ? -(s.aisleDepth / 2 + s.leftRackDepth / 2) : (s.aisleDepth / 2 + s.rightRackDepth / 2);
286
+ }
287
+ else {
288
+ s.targetForkZ = 0;
289
+ }
290
+ }
291
+ this._startCraneAnim();
292
+ }
293
+ _startCraneAnim() {
294
+ if (this._animRaf)
295
+ return;
296
+ const animate = () => {
297
+ let allDone = true;
298
+ for (const crane of this._cranes) {
299
+ const s = crane.state, speed = 0.08;
300
+ s.x += (s.targetX - s.x) * speed;
301
+ s.y += (s.targetY - s.y) * speed;
302
+ s.forkZ += (s.targetForkZ - s.forkZ) * speed;
303
+ crane.mastGroup.position.x = s.x;
304
+ crane.carriageGroup.position.y = s.y;
305
+ crane.forkGroup.position.z = s.forkZ;
306
+ if (Math.abs(s.targetX - s.x) > 0.1 || Math.abs(s.targetY - s.y) > 0.1 || Math.abs(s.targetForkZ - s.forkZ) > 0.1) {
307
+ allDone = false;
308
+ }
309
+ else {
310
+ s.x = s.targetX;
311
+ s.y = s.targetY;
312
+ s.forkZ = s.targetForkZ;
313
+ crane.mastGroup.position.x = s.x;
314
+ crane.carriageGroup.position.y = s.y;
315
+ crane.forkGroup.position.z = s.forkZ;
316
+ }
317
+ }
318
+ if (allDone) {
319
+ this._animRaf = 0;
320
+ return;
321
+ }
322
+ this._animRaf = requestAnimationFrame(animate);
323
+ };
324
+ this._animRaf = requestAnimationFrame(animate);
325
+ }
326
+ onchange(after, before) {
327
+ if ('data' in after) {
328
+ this.updateCraneTargets();
329
+ this.update();
330
+ return;
331
+ }
332
+ if ('lBays' in after || 'lLevels' in after || 'lDepth' in after || 'rBays' in after || 'rLevels' in after || 'rDepth' in after ||
333
+ 'cranes' in after || 'depth' in after || 'aisleRatio' in after ||
334
+ 'width' in after || 'height' in after || 'locationRule' in after || 'hideRackFrame' in after || 'fillStyle' in after || 'frontSide' in after) {
335
+ this.update();
336
+ return;
337
+ }
338
+ super.onchange(after, before);
339
+ }
340
+ dispose() {
341
+ if (this._animRaf) {
342
+ cancelAnimationFrame(this._animRaf);
343
+ this._animRaf = 0;
344
+ }
345
+ this._cranes = [];
346
+ this._cellMeshes = [];
347
+ super.dispose();
348
+ }
349
+ updateDimension() { }
350
+ updateAlpha() { }
351
+ }
352
+ //# sourceMappingURL=stocker-3d.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stocker-3d.js","sourceRoot":"","sources":["../src/stocker-3d.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAC9B,OAAO,KAAK,mBAAmB,MAAM,iDAAiD,CAAA;AACtF,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAA;AAExD,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAA;AAE5C,MAAM,WAAW,GAAG,QAAQ,CAAA;AAC5B,MAAM,WAAW,GAAG,QAAQ,CAAA;AAC5B,MAAM,WAAW,GAAG,QAAQ,CAAA;AAC5B,MAAM,UAAU,GAAG,QAAQ,CAAA;AAC3B,MAAM,WAAW,GAAG,QAAQ,CAAA;AAC5B,MAAM,gBAAgB,GAAG,QAAQ,CAAA;AACjC,MAAM,UAAU,GAAG,QAAQ,CAAA;AAC3B,MAAM,gBAAgB,GAAG,QAAQ,CAAA;AACjC,MAAM,eAAe,GAAG,QAAQ,CAAA;AAChC,MAAM,mBAAmB,GAAG,QAAQ,CAAA;AACpC,MAAM,gBAAgB,GAAG,QAAQ,CAAA;AAEjC,SAAS,WAAW,CAAC,MAAe;IAClC,QAAQ,MAAM,EAAE,CAAC;QACf,KAAK,MAAM,CAAC,CAAC,OAAO,eAAe,CAAA;QACnC,KAAK,UAAU,CAAC,CAAC,OAAO,mBAAmB,CAAA;QAC3C,KAAK,OAAO,CAAC,CAAC,OAAO,gBAAgB,CAAA;QACrC,OAAO,CAAC,CAAC,OAAO,gBAAgB,CAAA;IAClC,CAAC;AACH,CAAC;AAgBD,MAAM,OAAO,SAAU,SAAQ,eAAe;IACpC,OAAO,GAAoB,EAAE,CAAA;IAC7B,WAAW,GAAiB,EAAE,CAAA;IAC9B,QAAQ,GAAG,CAAC,CAAA;IAEpB,IAAI,OAAO;QACT,OAAO,IAAI,CAAC,SAA+B,CAAA;IAC7C,CAAC;IAED,IAAI,QAAQ;QACV,MAAM,EAAE,IAAI,GAAG,CAAC,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,CAAA;QACzC,OAAO,EAAE,CAAC,EAAE,IAAI,CAAC,EAAE,IAAI,CAAC,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,IAAI,CAAC,EAAE,IAAI,CAAC,EAAE,CAAA;IACtD,CAAC;IAED,KAAK;QACH,KAAK,CAAC,KAAK,EAAE,CAAA;QAEb,MAAM,KAAK,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,CAAA;QAClC,MAAM,EAAE,KAAK,GAAG,GAAG,EAAE,MAAM,GAAG,GAAG,EAAE,KAAK,EAAE,WAAW,GAAG,EAAE,EAAE,QAAQ,GAAG,CAAC,EAAE,UAAU,GAAG,IAAI,EAAE,GAAG,KAAK,CAAA;QAErG,MAAM,MAAM,GAAG,aAAa,CAAC,KAAK,CAAC,CAAA;QACnC,IAAI,OAAO,GAAG,CAAC,QAAQ,CAAA;QACvB,IAAI,MAAM,CAAC,QAAQ;YAAE,OAAO,IAAI,IAAI,CAAC,EAAE,GAAG,CAAC,CAAA;QAC3C,IAAI,MAAM,CAAC,OAAO;YAAE,OAAO,IAAI,IAAI,CAAC,EAAE,CAAA;QACtC,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,GAAG,OAAO,CAAA;QAElC,MAAM,SAAS,GAAG,MAAM,CAAC,KAAK,CAAA;QAC9B,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,CAAA;QAChC,MAAM,UAAU,GAAG,UAAU,GAAG,UAAU,CAAA;QAC1C,MAAM,aAAa,GAAG,UAAU,GAAG,UAAU,CAAA;QAC7C,MAAM,aAAa,GAAG,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,aAAa,GAAG,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;QACtF,MAAM,cAAc,GAAG,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,aAAa,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;QAEvF,MAAM,UAAU,GAA2B,EAAE,CAAA;QAC7C,MAAM,UAAU,GAA2B,EAAE,CAAA;QAE7C,mBAAmB;QACnB,IAAI,MAAM,CAAC,QAAQ,EAAE,CAAC;YACpB,MAAM,KAAK,GAAG,CAAC,CAAC,UAAU,GAAG,CAAC,GAAG,aAAa,GAAG,CAAC,CAAC,CAAA;YACnD,IAAI,CAAC,cAAc,CAAC,GAAG,EAAE,MAAM,CAAC,QAAQ,CAAC,MAAM,EAAE,SAAS,EAAE,WAAW,EAAE,aAAa,EAAE,KAAK,EAAE,UAAU,EAAE,UAAU,CAAC,CAAA;QACxH,CAAC;QACD,IAAI,MAAM,CAAC,SAAS,EAAE,CAAC;YACrB,MAAM,KAAK,GAAG,UAAU,GAAG,CAAC,GAAG,cAAc,GAAG,CAAC,CAAA;YACjD,IAAI,CAAC,cAAc,CAAC,GAAG,EAAE,MAAM,CAAC,SAAS,CAAC,MAAM,EAAE,SAAS,EAAE,WAAW,EAAE,cAAc,EAAE,KAAK,EAAE,UAAU,EAAE,UAAU,CAAC,CAAA;QAC1H,CAAC;QAED,eAAe;QACf,IAAI,CAAC,KAAK,CAAC,aAAa,IAAI,UAAU,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAClD,MAAM,GAAG,GAAG,IAAI,KAAK,CAAC,oBAAoB,CAAC,EAAE,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,CAAA;YACrI,MAAM,IAAI,GAAG,IAAI,KAAK,CAAC,IAAI,CAAC,mBAAmB,CAAC,eAAe,CAAC,UAAU,CAAC,EAAE,GAAG,CAAC,CAAA;YACjF,IAAI,CAAC,UAAU,GAAG,IAAI,CAAA;YACtB,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;QACzB,CAAC;QAED,IAAI,CAAC,KAAK,CAAC,aAAa,IAAI,UAAU,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAClD,MAAM,GAAG,GAAG,IAAI,KAAK,CAAC,oBAAoB,CAAC,EAAE,KAAK,EAAE,WAAW,EAAE,IAAI,EAAE,KAAK,CAAC,UAAU,EAAE,WAAW,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,CAAA;YAC3H,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,KAAK,CAAC,IAAI,CAAC,mBAAmB,CAAC,eAAe,CAAC,UAAU,CAAC,EAAE,GAAG,CAAC,CAAC,CAAA;QACzF,CAAC;QAED,kBAAkB;QAClB,MAAM,EAAE,SAAS,EAAE,GAAG,KAAK,CAAA;QAC3B,IAAI,SAAS,IAAI,SAAS,KAAK,aAAa,IAAI,SAAS,KAAK,eAAe,EAAE,CAAC;YAC9E,IAAI,CAAC,eAAe,CAAC,SAAS,EAAE,UAAU,EAAE,WAAW,EAAE,SAAS,CAAC,CAAA;QACrE,CAAC;QAED,cAAc;QACd,MAAM,SAAS,GAAG,IAAI,KAAK,CAAC,aAAa,CAAC,SAAS,EAAE,UAAU,CAAC,CAAA;QAChE,SAAS,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,EAAE,GAAG,CAAC,CAAC,CAAA;QAC/B,MAAM,SAAS,GAAG,IAAI,KAAK,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,KAAK,CAAC,oBAAoB,CAAC,EAAE,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,GAAG,EAAE,CAAC,CAAC,CAAA;QACnH,SAAS,CAAC,QAAQ,CAAC,CAAC,GAAG,GAAG,CAAA;QAC1B,SAAS,CAAC,aAAa,GAAG,IAAI,CAAA;QAC9B,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,SAAS,CAAC,CAAA;QAE5B,OAAO;QACP,MAAM,QAAQ,GAAG,IAAI,KAAK,CAAC,IAAI,CAC7B,IAAI,KAAK,CAAC,WAAW,CAAC,SAAS,EAAE,CAAC,EAAE,CAAC,CAAC,EACtC,IAAI,KAAK,CAAC,oBAAoB,CAAC,EAAE,KAAK,EAAE,UAAU,EAAE,SAAS,EAAE,GAAG,EAAE,SAAS,EAAE,GAAG,EAAE,CAAC,CACtF,CAAA;QACD,QAAQ,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,CAAC,CAAA;QAChC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAA;QAE3B,eAAe;QACf,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,CAAA;QAC9B,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,OAAO,IAAI,CAAC,EAAE,KAAK,CAAC,OAAO,IAAI,CAAC,EAAE,CAAC,CAAC,CAAA;QAClE,IAAI,CAAC,YAAY,CAAC,SAAS,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,aAAa,EAAE,cAAc,CAAC,CAAA;IAEvG,CAAC;IAEO,cAAc,CACpB,IAAe,EAAE,MAA4D,EAC7E,UAAkB,EAAE,WAAmB,EAAE,SAAiB,EAAE,KAAa,EACzE,UAAkC,EAAE,UAAkC;QAEtE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,GAAG,MAAM,CAAA;QAC3C,MAAM,QAAQ,GAAG,UAAU,GAAG,IAAI,CAAA;QAClC,MAAM,WAAW,GAAG,WAAW,GAAG,MAAM,CAAA;QACxC,MAAM,aAAa,GAAG,SAAS,GAAG,UAAU,CAAA;QAC5C,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,QAAQ,GAAG,IAAI,EAAE,CAAC,CAAC,CAAA;QAC1C,MAAM,KAAK,GAAG,CAAC,CAAA;QAEf,MAAM,EAAE,GAAG,UAAU,GAAG,CAAC,GAAG,KAAK,CAAA;QACjC,MAAM,EAAE,GAAG,SAAS,GAAG,CAAC,GAAG,KAAK,CAAA;QAEhC,0CAA0C;QAC1C,MAAM,QAAQ,GAAG,WAAW,GAAG,KAAK,CAAA;QACpC,MAAM,WAAW,GAAG,KAAK,CAAA;QACzB,MAAM,UAAU,GAAG,QAAQ,GAAG,WAAW,GAAG,KAAK,CAAA;QACjD,MAAM,QAAQ,GAAG,IAAI,KAAK,CAAC,WAAW,CAAC,KAAK,EAAE,UAAU,EAAE,KAAK,CAAC,CAAA;QAChE,KAAK,MAAM,CAAC,EAAE,EAAE,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,KAAK,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,KAAK,GAAG,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,KAAK,GAAG,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,KAAK,GAAG,EAAE,CAAC,CAAC,EAAE,CAAC;YAClG,MAAM,CAAC,GAAG,QAAQ,CAAC,KAAK,EAAE,CAAA;YAC1B,CAAC,CAAC,SAAS,CAAC,EAAE,EAAE,WAAW,GAAG,KAAK,GAAG,CAAC,GAAG,UAAU,GAAG,CAAC,EAAE,EAAE,CAAC,CAAA;YAC7D,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;QACpB,CAAC;QAED,oDAAoD;QACpD,MAAM,KAAK,GAAG,UAAU,GAAG,KAAK,GAAG,CAAC,CAAA;QACpC,MAAM,aAAa,GAAG,IAAI,KAAK,CAAC,WAAW,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,CAAC,CAAA;QAChE,MAAM,UAAU,GAAG,SAAS,GAAG,KAAK,GAAG,CAAC,CAAA;QACxC,MAAM,aAAa,GAAG,IAAI,KAAK,CAAC,WAAW,CAAC,KAAK,EAAE,KAAK,EAAE,UAAU,CAAC,CAAA;QAErE,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YACjC,IAAI,CAAC,GAAG,CAAC,GAAG,WAAW,CAAA;YACvB,6CAA6C;YAC7C,IAAI,CAAC,KAAK,CAAC;gBAAE,CAAC,GAAG,KAAK,CAAA;iBACjB,IAAI,CAAC,KAAK,MAAM;gBAAE,CAAC,GAAG,WAAW,GAAG,KAAK,CAAA;YAE9C,mBAAmB;YACnB,KAAK,MAAM,EAAE,IAAI,CAAC,KAAK,GAAG,EAAE,EAAE,KAAK,GAAG,EAAE,CAAC,EAAE,CAAC;gBAC1C,MAAM,CAAC,GAAG,aAAa,CAAC,KAAK,EAAE,CAAA;gBAC/B,CAAC,CAAC,SAAS,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAA;gBACrB,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;YACpB,CAAC;YACD,qBAAqB;YACrB,KAAK,MAAM,EAAE,IAAI,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC;gBAC3B,MAAM,CAAC,GAAG,aAAa,CAAC,KAAK,EAAE,CAAA;gBAC/B,CAAC,CAAC,SAAS,CAAC,EAAE,EAAE,CAAC,EAAE,KAAK,CAAC,CAAA;gBACzB,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;YACpB,CAAC;QACH,CAAC;QAED,eAAe;QACf,MAAM,MAAM,GAAG,UAAU,GAAG,KAAK,GAAG,CAAC,CAAA;QACrC,MAAM,MAAM,GAAG,SAAS,GAAG,KAAK,GAAG,CAAC,CAAA;QACpC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YAChC,MAAM,CAAC,GAAG,CAAC,GAAG,WAAW,GAAG,GAAG,CAAA;YAC/B,MAAM,CAAC,GAAG,IAAI,KAAK,CAAC,aAAa,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;YACjD,CAAC,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,EAAE,GAAG,CAAC,CAAC,CAAA;YACvB,CAAC,CAAC,SAAS,CAAC,CAAC,EAAE,CAAC,EAAE,KAAK,CAAC,CAAA;YACxB,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;QACpB,CAAC;QAED,aAAa;QACb,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YACjC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,IAAI,EAAE,CAAC,EAAE,EAAE,CAAC;gBAC/B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,UAAU,EAAE,CAAC,EAAE,EAAE,CAAC;oBACrC,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAA;oBACxD,MAAM,IAAI,GAAG,IAAI,KAAK,CAAC,WAAW,CAAC,QAAQ,GAAG,GAAG,EAAE,WAAW,GAAG,GAAG,EAAE,aAAa,GAAG,GAAG,CAAC,CAAA;oBAC1F,MAAM,GAAG,GAAG,IAAI,KAAK,CAAC,oBAAoB,CAAC;wBACzC,KAAK,EAAE,WAAW,CAAC,MAAM,CAAC,EAAE,SAAS,EAAE,GAAG,EAAE,SAAS,EAAE,GAAG;wBAC1D,WAAW,EAAE,CAAC,MAAM,IAAI,MAAM,KAAK,OAAO;wBAC1C,OAAO,EAAE,CAAC,CAAC,MAAM,IAAI,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;qBACpD,CAAC,CAAA;oBACF,MAAM,IAAI,GAAG,IAAI,KAAK,CAAC,IAAI,CAAC,IAAI,EAAE,GAAG,CAAC,CAAA;oBACtC,IAAI,CAAC,QAAQ,CAAC,GAAG,CACf,CAAC,UAAU,GAAG,CAAC,GAAG,CAAC,CAAC,GAAG,GAAG,CAAC,GAAG,QAAQ,EACtC,CAAC,CAAC,GAAG,GAAG,CAAC,GAAG,WAAW,EACvB,KAAK,GAAG,CAAC,CAAC,GAAG,GAAG,GAAG,UAAU,GAAG,CAAC,CAAC,GAAG,aAAa,CACnD,CAAA;oBACD,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAA;oBACrD,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;oBAC3B,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;gBACzB,CAAC;YACH,CAAC;QACH,CAAC;IACH,CAAC;IAEO,eAAe,CAAC,SAAiB,EAAE,UAAkB,EAAE,WAAmB,EAAE,SAAiB;QACnG,IAAI,KAAK,GAAG,QAAQ,CAAA;QACpB,IAAI,OAAO,GAAG,GAAG,CAAA;QACjB,oCAAoC;QACpC,MAAM,SAAS,GAAG,SAAS,CAAC,KAAK,CAAC,kEAAkE,CAAC,CAAA;QACrG,IAAI,SAAS,EAAE,CAAC;YACd,MAAM,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,GAAG,SAAS,CAAA;YAChC,KAAK,GAAG,IAAI,KAAK,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,GAAG,GAAG,EAAE,QAAQ,CAAC,CAAC,CAAC,GAAG,GAAG,EAAE,QAAQ,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC,MAAM,EAAE,CAAA;YACzF,IAAI,CAAC,KAAK,SAAS;gBAAE,OAAO,GAAG,UAAU,CAAC,CAAC,CAAC,CAAA;QAC9C,CAAC;aAAM,CAAC;YACN,IAAI,CAAC;gBAAC,KAAK,GAAG,IAAI,KAAK,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,MAAM,EAAE,CAAA;YAAC,CAAC;YAAC,MAAM,CAAC,CAAA,CAAC;QAC9D,CAAC;QACD,MAAM,GAAG,GAAG,IAAI,KAAK,CAAC,oBAAoB,CAAC,EAAE,KAAK,EAAE,WAAW,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,CAAC,UAAU,EAAE,UAAU,EAAE,KAAK,EAAE,CAAC,CAAA;QAC5H,MAAM,KAAK,GAAG,SAAS,GAAG,CAAC,CAAA;QAC3B,MAAM,KAAK,GAAG,UAAU,GAAG,CAAC,CAAA;QAE5B,KAAK,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,IAAI;YAChC,CAAC,SAAS,EAAE,WAAW,EAAE,CAAC,EAAE,WAAW,GAAG,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC,CAAC;YACvD,CAAC,SAAS,EAAE,WAAW,EAAE,CAAC,EAAE,WAAW,GAAG,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC;YACtD,CAAC,UAAU,EAAE,WAAW,EAAE,CAAC,KAAK,EAAE,WAAW,GAAG,CAAC,EAAE,CAAC,EAAE,IAAI,CAAC,EAAE,GAAG,CAAC,CAAC;YAClE,CAAC,UAAU,EAAE,WAAW,EAAE,KAAK,EAAE,WAAW,GAAG,CAAC,EAAE,CAAC,EAAE,IAAI,CAAC,EAAE,GAAG,CAAC,CAAC;SACZ,EAAE,CAAC;YACxD,MAAM,IAAI,GAAG,IAAI,KAAK,CAAC,IAAI,CAAC,IAAI,KAAK,CAAC,aAAa,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,GAAG,CAAC,CAAA;YAC/D,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAA;YAC1B,IAAI,CAAC,QAAQ,CAAC,CAAC,GAAG,EAAE,CAAA;YACpB,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;QACzB,CAAC;QACD,MAAM,QAAQ,GAAG,IAAI,KAAK,CAAC,aAAa,CAAC,SAAS,EAAE,UAAU,CAAC,CAAA;QAC/D,QAAQ,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,EAAE,GAAG,CAAC,CAAC,CAAA;QAC9B,MAAM,QAAQ,GAAG,IAAI,KAAK,CAAC,IAAI,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAA;QAC9C,QAAQ,CAAC,QAAQ,CAAC,CAAC,GAAG,WAAW,CAAA;QACjC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAA;IAC7B,CAAC;IAEO,YAAY,CAAC,SAAiB,EAAE,WAAmB,EAAE,OAAe,EAAE,MAAc,EAAE,UAAkB,EAAE,aAAqB,EAAE,cAAsB;QAC7J,MAAM,UAAU,GAAG,IAAI,CAAC,OAAO,CAAC,UAAU,CAAA;QAC1C,MAAM,UAAU,GAAG,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,MAAM,EAAE,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,MAAM,IAAI,CAAC,CAAC,CAAA;QAChF,MAAM,QAAQ,GAAG,SAAS,GAAG,OAAO,CAAA;QACpC,MAAM,WAAW,GAAG,WAAW,GAAG,MAAM,CAAA;QACxC,MAAM,WAAW,GAAG,WAAW,GAAG,IAAI,CAAA;QAEtC,MAAM,OAAO,GAAG,IAAI,KAAK,CAAC,oBAAoB,CAAC,EAAE,KAAK,EAAE,gBAAgB,EAAE,SAAS,EAAE,GAAG,EAAE,SAAS,EAAE,GAAG,EAAE,CAAC,CAAA;QAC3G,MAAM,WAAW,GAAG,IAAI,KAAK,CAAC,oBAAoB,CAAC,EAAE,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,GAAG,EAAE,SAAS,EAAE,GAAG,EAAE,CAAC,CAAA;QAC1G,MAAM,OAAO,GAAG,IAAI,KAAK,CAAC,oBAAoB,CAAC,EAAE,KAAK,EAAE,UAAU,EAAE,SAAS,EAAE,GAAG,EAAE,SAAS,EAAE,GAAG,EAAE,CAAC,CAAA;QAErG,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,UAAU,EAAE,CAAC,EAAE,EAAE,CAAC;YACpC,MAAM,EAAE,GAAG,UAAU,CAAC,CAAC,CAAC,CAAA;YACxB,MAAM,GAAG,GAAG,EAAE,EAAE,GAAG,IAAI,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,OAAO,GAAG,CAAC,UAAU,GAAG,CAAC,CAAC,CAAC,CAAA;YACvE,MAAM,KAAK,GAAG,EAAE,EAAE,KAAK,IAAI,CAAC,CAAA;YAC5B,MAAM,IAAI,GAAG,EAAE,EAAE,IAAI,CAAA;YACrB,MAAM,MAAM,GAAG,EAAE,EAAE,MAAM,IAAI,MAAM,CAAA;YAEnC,MAAM,MAAM,GAAG,CAAC,SAAS,GAAG,CAAC,GAAG,CAAC,GAAG,GAAG,GAAG,CAAC,GAAG,QAAQ,CAAA;YACtD,MAAM,MAAM,GAAG,CAAC,KAAK,GAAG,GAAG,CAAC,GAAG,WAAW,CAAA;YAC1C,IAAI,WAAW,GAAG,CAAC,CAAA;YACnB,IAAI,CAAC,MAAM,KAAK,SAAS,IAAI,MAAM,KAAK,SAAS,CAAC,IAAI,IAAI,EAAE,CAAC;gBAC3D,WAAW,GAAG,IAAI,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,UAAU,GAAG,CAAC,GAAG,aAAa,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,UAAU,GAAG,CAAC,GAAG,cAAc,GAAG,CAAC,CAAC,CAAA;YAC5G,CAAC;YAED,MAAM,SAAS,GAAG,IAAI,KAAK,CAAC,KAAK,EAAE,CAAA;YACnC,SAAS,CAAC,QAAQ,CAAC,CAAC,GAAG,MAAM,CAAA;YAC7B,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,SAAS,CAAC,CAAA;YAE5B,MAAM,QAAQ,GAAG,IAAI,KAAK,CAAC,IAAI,CAAC,IAAI,KAAK,CAAC,WAAW,CAAC,CAAC,EAAE,WAAW,EAAE,CAAC,CAAC,EAAE,OAAO,CAAC,CAAA;YAClF,QAAQ,CAAC,QAAQ,CAAC,CAAC,GAAG,WAAW,GAAG,CAAC,CAAA;YACrC,QAAQ,CAAC,UAAU,GAAG,IAAI,CAAA;YAC1B,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAA;YAEvB,MAAM,OAAO,GAAG,IAAI,KAAK,CAAC,IAAI,CAAC,IAAI,KAAK,CAAC,WAAW,CAAC,QAAQ,GAAG,GAAG,EAAE,CAAC,EAAE,UAAU,GAAG,GAAG,CAAC,EAAE,OAAO,CAAC,CAAA;YACnG,OAAO,CAAC,QAAQ,CAAC,CAAC,GAAG,WAAW,GAAG,CAAC,CAAA;YACpC,SAAS,CAAC,GAAG,CAAC,OAAO,CAAC,CAAA;YAEtB,MAAM,aAAa,GAAG,IAAI,KAAK,CAAC,KAAK,EAAE,CAAA;YACvC,aAAa,CAAC,QAAQ,CAAC,CAAC,GAAG,MAAM,CAAA;YACjC,SAAS,CAAC,GAAG,CAAC,aAAa,CAAC,CAAA;YAE5B,MAAM,KAAK,GAAG,QAAQ,GAAG,GAAG,EAAE,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,UAAU,GAAG,GAAG,CAAA;YACjE,MAAM,YAAY,GAAG,IAAI,KAAK,CAAC,IAAI,CAAC,IAAI,KAAK,CAAC,WAAW,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,CAAC,EAAE,WAAW,CAAC,CAAA;YAC5F,YAAY,CAAC,UAAU,GAAG,IAAI,CAAA;YAC9B,aAAa,CAAC,GAAG,CAAC,YAAY,CAAC,CAAA;YAE/B,MAAM,SAAS,GAAG,IAAI,KAAK,CAAC,KAAK,EAAE,CAAA;YACnC,aAAa,CAAC,GAAG,CAAC,SAAS,CAAC,CAAA;YAE5B,MAAM,UAAU,GAAG,IAAI,CAAC,GAAG,CAAC,aAAa,EAAE,cAAc,CAAC,GAAG,GAAG,CAAA;YAChE,MAAM,KAAK,GAAG,KAAK,GAAG,IAAI,EAAE,KAAK,GAAG,GAAG,CAAA;YACvC,MAAM,SAAS,GAAG,IAAI,KAAK,CAAC,WAAW,CAAC,KAAK,EAAE,KAAK,EAAE,UAAU,CAAC,CAAA;YAEjE,MAAM,EAAE,GAAG,IAAI,KAAK,CAAC,IAAI,CAAC,SAAS,EAAE,OAAO,CAAC,CAAA;YAC7C,EAAE,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,KAAK,GAAG,IAAI,EAAE,CAAC,KAAK,GAAG,CAAC,GAAG,KAAK,GAAG,CAAC,EAAE,CAAC,CAAC,CAAA;YACzD,SAAS,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;YACjB,MAAM,EAAE,GAAG,IAAI,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,EAAE,OAAO,CAAC,CAAA;YACrD,EAAE,CAAC,QAAQ,CAAC,GAAG,CAAC,KAAK,GAAG,IAAI,EAAE,CAAC,KAAK,GAAG,CAAC,GAAG,KAAK,GAAG,CAAC,EAAE,CAAC,CAAC,CAAA;YACxD,SAAS,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;YACjB,MAAM,QAAQ,GAAG,IAAI,KAAK,CAAC,IAAI,CAAC,IAAI,KAAK,CAAC,WAAW,CAAC,KAAK,GAAG,GAAG,EAAE,KAAK,EAAE,KAAK,CAAC,EAAE,OAAO,CAAC,CAAA;YAC1F,QAAQ,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,KAAK,GAAG,CAAC,GAAG,KAAK,GAAG,CAAC,EAAE,CAAC,UAAU,GAAG,CAAC,GAAG,KAAK,GAAG,CAAC,CAAC,CAAA;YAC7E,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAA;YAEvB,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC;gBAChB,SAAS,EAAE,aAAa,EAAE,SAAS;gBACnC,KAAK,EAAE,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,QAAQ,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,EAAE,aAAa,EAAE,cAAc,EAAE,UAAU,EAAE;aAC5K,CAAC,CAAA;QACJ,CAAC;QACD,IAAI,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,WAAW,KAAK,CAAC,CAAC;YAAE,IAAI,CAAC,eAAe,EAAE,CAAA;IAC/E,CAAC;IAED,kBAAkB;QAChB,MAAM,UAAU,GAAG,IAAI,CAAC,OAAO,CAAC,UAAU,CAAA;QAC1C,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAA;QAC3C,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YAC7C,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,CAAA;YAC7B,MAAM,EAAE,GAAG,UAAU,CAAC,CAAC,CAAC,CAAA;YACxB,MAAM,CAAC,GAAG,KAAK,CAAC,KAAK,CAAA;YACrB,MAAM,GAAG,GAAG,EAAE,EAAE,GAAG,IAAI,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,OAAO,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAA;YAClF,MAAM,KAAK,GAAG,EAAE,EAAE,KAAK,IAAI,CAAC,CAAA;YAC5B,MAAM,IAAI,GAAG,EAAE,EAAE,IAAI,CAAA;YACrB,MAAM,MAAM,GAAG,EAAE,EAAE,MAAM,IAAI,MAAM,CAAA;YACnC,CAAC,CAAC,OAAO,GAAG,CAAC,SAAS,GAAG,CAAC,GAAG,CAAC,GAAG,GAAG,GAAG,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAA;YACrD,CAAC,CAAC,OAAO,GAAG,CAAC,KAAK,GAAG,GAAG,CAAC,GAAG,CAAC,CAAC,WAAW,CAAA;YACzC,IAAI,CAAC,MAAM,KAAK,SAAS,IAAI,MAAM,KAAK,SAAS,CAAC,IAAI,IAAI,EAAE,CAAC;gBAC3D,CAAC,CAAC,WAAW,GAAG,IAAI,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,UAAU,GAAG,CAAC,GAAG,CAAC,CAAC,aAAa,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,UAAU,GAAG,CAAC,GAAG,CAAC,CAAC,cAAc,GAAG,CAAC,CAAC,CAAA;YACtH,CAAC;iBAAM,CAAC;gBAAC,CAAC,CAAC,WAAW,GAAG,CAAC,CAAA;YAAC,CAAC;QAC9B,CAAC;QACD,IAAI,CAAC,eAAe,EAAE,CAAA;IACxB,CAAC;IAEO,eAAe;QACrB,IAAI,IAAI,CAAC,QAAQ;YAAE,OAAM;QACzB,MAAM,OAAO,GAAG,GAAG,EAAE;YACnB,IAAI,OAAO,GAAG,IAAI,CAAA;YAClB,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;gBACjC,MAAM,CAAC,GAAG,KAAK,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI,CAAA;gBACnC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC;gBAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC;gBAAC,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,CAAC,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,GAAG,KAAK,CAAA;gBAChH,KAAK,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;gBAAC,KAAK,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;gBAAC,KAAK,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,CAAA;gBAC5G,IAAI,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,OAAO,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,GAAG,IAAI,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,OAAO,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,GAAG,IAAI,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,GAAG,GAAG,EAAE,CAAC;oBAAC,OAAO,GAAG,KAAK,CAAA;gBAAC,CAAC;qBACjI,CAAC;oBAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC;oBAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC;oBAAC,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,WAAW,CAAC;oBAAC,KAAK,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;oBAAC,KAAK,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;oBAAC,KAAK,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,CAAA;gBAAC,CAAC;YAClL,CAAC;YACD,IAAI,OAAO,EAAE,CAAC;gBAAC,IAAI,CAAC,QAAQ,GAAG,CAAC,CAAC;gBAAC,OAAM;YAAC,CAAC;YAC1C,IAAI,CAAC,QAAQ,GAAG,qBAAqB,CAAC,OAAO,CAAC,CAAA;QAChD,CAAC,CAAA;QACD,IAAI,CAAC,QAAQ,GAAG,qBAAqB,CAAC,OAAO,CAAC,CAAA;IAChD,CAAC;IAED,QAAQ,CAAC,KAA8B,EAAE,MAA+B;QACtE,IAAI,MAAM,IAAI,KAAK,EAAE,CAAC;YAAC,IAAI,CAAC,kBAAkB,EAAE,CAAC;YAAC,IAAI,CAAC,MAAM,EAAE,CAAC;YAAC,OAAM;QAAC,CAAC;QACzE,IAAI,OAAO,IAAI,KAAK,IAAI,SAAS,IAAI,KAAK,IAAI,QAAQ,IAAI,KAAK,IAAI,OAAO,IAAI,KAAK,IAAI,SAAS,IAAI,KAAK,IAAI,QAAQ,IAAI,KAAK;YAC1H,QAAQ,IAAI,KAAK,IAAI,OAAO,IAAI,KAAK,IAAI,YAAY,IAAI,KAAK;YAC9D,OAAO,IAAI,KAAK,IAAI,QAAQ,IAAI,KAAK,IAAI,cAAc,IAAI,KAAK,IAAI,eAAe,IAAI,KAAK,IAAI,WAAW,IAAI,KAAK,IAAI,WAAW,IAAI,KAAK,EAAE,CAAC;YACjJ,IAAI,CAAC,MAAM,EAAE,CAAC;YAAC,OAAM;QACvB,CAAC;QACD,KAAK,CAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,CAAA;IAC/B,CAAC;IAED,OAAO;QACL,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YAAC,oBAAoB,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;YAAC,IAAI,CAAC,QAAQ,GAAG,CAAC,CAAA;QAAC,CAAC;QAC7E,IAAI,CAAC,OAAO,GAAG,EAAE,CAAC;QAAC,IAAI,CAAC,WAAW,GAAG,EAAE,CAAA;QACxC,KAAK,CAAC,OAAO,EAAE,CAAA;IACjB,CAAC;IAED,eAAe,KAAI,CAAC;IACpB,WAAW,KAAI,CAAC;CACjB","sourcesContent":["/*\n * Copyright © HatioLab Inc. All rights reserved.\n *\n * Stocker 3D — aisle, rail, crane, ports, enclosure, rack frames, cell stocks.\n */\n\nimport * as THREE from 'three'\nimport * as BufferGeometryUtils from 'three/examples/jsm/utils/BufferGeometryUtils.js'\nimport { RealObjectGroup } from '@hatiolab/things-scene'\nimport type { Stocker, StockerData, CraneData } from './stocker.js'\nimport { computeLayout } from './stocker.js'\n\nconst FRAME_COLOR = 0x8a8a8a\nconst BOARD_COLOR = 0xcccccc\nconst AISLE_COLOR = 0xd4d4c8\nconst RAIL_COLOR = 0x666666\nconst CRANE_COLOR = 0xff6600\nconst CRANE_MAST_COLOR = 0xcc5500\nconst FORK_COLOR = 0xddaa00\nconst CELL_EMPTY_COLOR = 0xf0f0f0\nconst CELL_FULL_COLOR = 0x4a9eff\nconst CELL_RESERVED_COLOR = 0xffcc00\nconst CELL_ERROR_COLOR = 0xe74c3c\n\nfunction cellColor3d(status?: string): number {\n switch (status) {\n case 'FULL': return CELL_FULL_COLOR\n case 'RESERVED': return CELL_RESERVED_COLOR\n case 'ERROR': return CELL_ERROR_COLOR\n default: return CELL_EMPTY_COLOR\n }\n}\n\ninterface CraneState {\n x: number; y: number; forkZ: number\n targetX: number; targetY: number; targetForkZ: number\n bayWidth: number; levelHeight: number; maxBays: number; levels: number\n leftRackDepth: number; rightRackDepth: number; aisleDepth: number\n}\n\ninterface CraneInstance {\n mastGroup: THREE.Group\n carriageGroup: THREE.Group\n forkGroup: THREE.Group\n state: CraneState\n}\n\nexport class Stocker3d extends RealObjectGroup {\n private _cranes: CraneInstance[] = []\n private _cellMeshes: THREE.Mesh[] = []\n private _animRaf = 0\n\n get stocker(): Stocker {\n return this.component as unknown as Stocker\n }\n\n get position() {\n const { zPos = 0 } = this.component.state\n return { x: this.cx || 0, y: zPos, z: this.cy || 0 }\n }\n\n build() {\n super.build()\n\n const state = this.component.state\n const { width = 200, height = 100, depth: totalHeight = 80, rotation = 0, aisleRatio = 0.15 } = state\n\n const layout = computeLayout(state)\n let orientY = -rotation\n if (layout.vertical) orientY += Math.PI / 2\n if (layout.flipped) orientY += Math.PI\n this.object3d.rotation.y = orientY\n\n const alongSize = layout.along\n const acrossSize = layout.across\n const aisleDepth = acrossSize * aisleRatio\n const rackAreaDepth = acrossSize - aisleDepth\n const leftRackDepth = layout.leftRack ? rackAreaDepth / (layout.rightRack ? 2 : 1) : 0\n const rightRackDepth = layout.rightRack ? rackAreaDepth / (layout.leftRack ? 2 : 1) : 0\n\n const frameGeoms: THREE.BufferGeometry[] = []\n const boardGeoms: THREE.BufferGeometry[] = []\n\n // ── Rack sides ──\n if (layout.leftRack) {\n const rackZ = -(aisleDepth / 2 + leftRackDepth / 2)\n this._buildRackSide('L', layout.leftRack.config, alongSize, totalHeight, leftRackDepth, rackZ, frameGeoms, boardGeoms)\n }\n if (layout.rightRack) {\n const rackZ = aisleDepth / 2 + rightRackDepth / 2\n this._buildRackSide('R', layout.rightRack.config, alongSize, totalHeight, rightRackDepth, rackZ, frameGeoms, boardGeoms)\n }\n\n // merge frames\n if (!state.hideRackFrame && frameGeoms.length > 0) {\n const mat = new THREE.MeshStandardMaterial({ color: FRAME_COLOR, metalness: 0.85, roughness: 0.35, transparent: true, opacity: 0.8 })\n const mesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(frameGeoms), mat)\n mesh.castShadow = true\n this.object3d.add(mesh)\n }\n\n if (!state.hideRackFrame && boardGeoms.length > 0) {\n const mat = new THREE.MeshStandardMaterial({ color: BOARD_COLOR, side: THREE.DoubleSide, transparent: true, opacity: 0.5 })\n this.object3d.add(new THREE.Mesh(BufferGeometryUtils.mergeGeometries(boardGeoms), mat))\n }\n\n // ── Enclosure ──\n const { fillStyle } = state\n if (fillStyle && fillStyle !== 'transparent' && fillStyle !== 'rgba(0,0,0,0)') {\n this._buildEnclosure(alongSize, acrossSize, totalHeight, fillStyle)\n }\n\n // ── Aisle ──\n const aisleGeom = new THREE.PlaneGeometry(alongSize, aisleDepth)\n aisleGeom.rotateX(-Math.PI / 2)\n const aisleMesh = new THREE.Mesh(aisleGeom, new THREE.MeshStandardMaterial({ color: AISLE_COLOR, roughness: 0.9 }))\n aisleMesh.position.y = 0.1\n aisleMesh.receiveShadow = true\n this.object3d.add(aisleMesh)\n\n // rail\n const railMesh = new THREE.Mesh(\n new THREE.BoxGeometry(alongSize, 1, 2),\n new THREE.MeshStandardMaterial({ color: RAIL_COLOR, metalness: 0.8, roughness: 0.3 })\n )\n railMesh.position.set(0, 0.5, 0)\n this.object3d.add(railMesh)\n\n // ── Cranes ──\n const maxBays = layout.maxBays\n const levels = Math.max(state.lLevels || 0, state.rLevels || 0, 1)\n this._buildCranes(alongSize, totalHeight, maxBays, levels, aisleDepth, leftRackDepth, rightRackDepth)\n\n }\n\n private _buildRackSide(\n side: 'L' | 'R', config: { bays: number; levels: number; depthCount: number },\n totalWidth: number, totalHeight: number, rackDepth: number, rackZ: number,\n frameGeoms: THREE.BufferGeometry[], boardGeoms: THREE.BufferGeometry[]\n ) {\n const { bays, levels, depthCount } = config\n const bayWidth = totalWidth / bays\n const levelHeight = totalHeight / levels\n const cellDepthSize = rackDepth / depthCount\n const postW = Math.min(bayWidth * 0.05, 2)\n const inset = 3\n\n const hw = totalWidth / 2 - inset\n const hd = rackDepth / 2 - inset\n\n // vertical posts — 수평바 + 1/2 두께로 접합부 갭 채움\n const topRailY = totalHeight - inset\n const bottomRailY = inset\n const postHeight = topRailY - bottomRailY + postW\n const postGeom = new THREE.BoxGeometry(postW, postHeight, postW)\n for (const [px, pz] of [[-hw, rackZ - hd], [-hw, rackZ + hd], [hw, rackZ - hd], [hw, rackZ + hd]]) {\n const g = postGeom.clone()\n g.translate(px, bottomRailY - postW / 2 + postHeight / 2, pz)\n frameGeoms.push(g)\n }\n\n // horizontal rails — 좌우 inset, 상하는 enclosure 안쪽에 위치\n const railW = totalWidth - inset * 2\n const railAlongGeom = new THREE.BoxGeometry(railW, postW, postW)\n const railDepthW = rackDepth - inset * 2\n const railDepthGeom = new THREE.BoxGeometry(postW, postW, railDepthW)\n\n for (let l = 0; l <= levels; l++) {\n let y = l * levelHeight\n // bottom은 enclosure 바닥에서 올라오고, top은 천장에서 내려옴\n if (l === 0) y = inset\n else if (l === levels) y = totalHeight - inset\n\n // along 방향 (전면/후면)\n for (const dz of [rackZ - hd, rackZ + hd]) {\n const g = railAlongGeom.clone()\n g.translate(0, y, dz)\n frameGeoms.push(g)\n }\n // depth 방향 (좌측/우측 끝)\n for (const px of [-hw, hw]) {\n const g = railDepthGeom.clone()\n g.translate(px, y, rackZ)\n frameGeoms.push(g)\n }\n }\n\n // shelf boards\n const boardW = totalWidth - inset * 2\n const boardD = rackDepth - inset * 2\n for (let l = 0; l < levels; l++) {\n const y = l * levelHeight + 0.5\n const g = new THREE.PlaneGeometry(boardW, boardD)\n g.rotateX(-Math.PI / 2)\n g.translate(0, y, rackZ)\n boardGeoms.push(g)\n }\n\n // cell stock\n for (let l = 1; l <= levels; l++) {\n for (let b = 1; b <= bays; b++) {\n for (let d = 1; d <= depthCount; d++) {\n const status = this.stocker.getCellStatus(side, b, l, d)\n const geom = new THREE.BoxGeometry(bayWidth * 0.7, levelHeight * 0.7, cellDepthSize * 0.7)\n const mat = new THREE.MeshStandardMaterial({\n color: cellColor3d(status), metalness: 0.1, roughness: 0.8,\n transparent: !status || status === 'EMPTY',\n opacity: (!status || status === 'EMPTY') ? 0.15 : 1\n })\n const mesh = new THREE.Mesh(geom, mat)\n mesh.position.set(\n -totalWidth / 2 + (b - 0.5) * bayWidth,\n (l - 0.5) * levelHeight,\n rackZ + (d - 0.5 - depthCount / 2) * cellDepthSize\n )\n mesh.name = this.stocker.getLocationId(side, b, l, d)\n this._cellMeshes.push(mesh)\n this.object3d.add(mesh)\n }\n }\n }\n }\n\n private _buildEnclosure(alongSize: number, acrossSize: number, totalHeight: number, fillStyle: string) {\n let color = 0xcccccc\n let opacity = 0.4\n // rgba에서 alpha 추출, 색상만 THREE.Color로\n const rgbaMatch = fillStyle.match(/rgba?\\(\\s*(\\d+)\\s*,\\s*(\\d+)\\s*,\\s*(\\d+)\\s*(?:,\\s*([\\d.]+))?\\s*\\)/)\n if (rgbaMatch) {\n const [, r, g, b, a] = rgbaMatch\n color = new THREE.Color(parseInt(r) / 255, parseInt(g) / 255, parseInt(b) / 255).getHex()\n if (a !== undefined) opacity = parseFloat(a)\n } else {\n try { color = new THREE.Color(fillStyle).getHex() } catch {}\n }\n const mat = new THREE.MeshStandardMaterial({ color, transparent: true, opacity, side: THREE.DoubleSide, depthWrite: false })\n const halfW = alongSize / 2\n const halfD = acrossSize / 2\n\n for (const [w, h, x, y, z, ry] of [\n [alongSize, totalHeight, 0, totalHeight / 2, -halfD, 0],\n [alongSize, totalHeight, 0, totalHeight / 2, halfD, 0],\n [acrossSize, totalHeight, -halfW, totalHeight / 2, 0, Math.PI / 2],\n [acrossSize, totalHeight, halfW, totalHeight / 2, 0, Math.PI / 2]\n ] as [number, number, number, number, number, number][]) {\n const mesh = new THREE.Mesh(new THREE.PlaneGeometry(w, h), mat)\n mesh.position.set(x, y, z)\n mesh.rotation.y = ry\n this.object3d.add(mesh)\n }\n const roofGeom = new THREE.PlaneGeometry(alongSize, acrossSize)\n roofGeom.rotateX(-Math.PI / 2)\n const roofMesh = new THREE.Mesh(roofGeom, mat)\n roofMesh.position.y = totalHeight\n this.object3d.add(roofMesh)\n }\n\n private _buildCranes(alongSize: number, totalHeight: number, maxBays: number, levels: number, aisleDepth: number, leftRackDepth: number, rightRackDepth: number) {\n const cranesData = this.stocker.cranesData\n const craneCount = Math.max(cranesData.length, this.component.state.cranes || 1)\n const bayWidth = alongSize / maxBays\n const levelHeight = totalHeight / levels\n const craneHeight = totalHeight * 0.95\n\n const mastMat = new THREE.MeshStandardMaterial({ color: CRANE_MAST_COLOR, metalness: 0.7, roughness: 0.4 })\n const carriageMat = new THREE.MeshStandardMaterial({ color: CRANE_COLOR, metalness: 0.3, roughness: 0.6 })\n const forkMat = new THREE.MeshStandardMaterial({ color: FORK_COLOR, metalness: 0.5, roughness: 0.5 })\n\n for (let i = 0; i < craneCount; i++) {\n const cd = cranesData[i]\n const bay = cd?.bay ?? Math.round((i + 1) * maxBays / (craneCount + 1))\n const level = cd?.level ?? 1\n const side = cd?.side\n const status = cd?.status || 'IDLE'\n\n const craneX = -alongSize / 2 + (bay - 0.5) * bayWidth\n const craneY = (level - 0.5) * levelHeight\n let targetForkZ = 0\n if ((status === 'PICKING' || status === 'PLACING') && side) {\n targetForkZ = side === 'L' ? -(aisleDepth / 2 + leftRackDepth / 2) : (aisleDepth / 2 + rightRackDepth / 2)\n }\n\n const mastGroup = new THREE.Group()\n mastGroup.position.x = craneX\n this.object3d.add(mastGroup)\n\n const mastMesh = new THREE.Mesh(new THREE.BoxGeometry(3, craneHeight, 3), mastMat)\n mastMesh.position.y = craneHeight / 2\n mastMesh.castShadow = true\n mastGroup.add(mastMesh)\n\n const topBeam = new THREE.Mesh(new THREE.BoxGeometry(bayWidth * 0.3, 2, aisleDepth * 0.8), mastMat)\n topBeam.position.y = craneHeight - 1\n mastGroup.add(topBeam)\n\n const carriageGroup = new THREE.Group()\n carriageGroup.position.y = craneY\n mastGroup.add(carriageGroup)\n\n const carrW = bayWidth * 0.7, carrH = 4, carrD = aisleDepth * 0.5\n const carriageMesh = new THREE.Mesh(new THREE.BoxGeometry(carrW, carrH, carrD), carriageMat)\n carriageMesh.castShadow = true\n carriageGroup.add(carriageMesh)\n\n const forkGroup = new THREE.Group()\n carriageGroup.add(forkGroup)\n\n const forkLength = Math.max(leftRackDepth, rightRackDepth) * 0.8\n const forkW = carrW * 0.15, forkH = 1.5\n const prongGeom = new THREE.BoxGeometry(forkW, forkH, forkLength)\n\n const lp = new THREE.Mesh(prongGeom, forkMat)\n lp.position.set(-carrW * 0.25, -carrH / 2 + forkH / 2, 0)\n forkGroup.add(lp)\n const rp = new THREE.Mesh(prongGeom.clone(), forkMat)\n rp.position.set(carrW * 0.25, -carrH / 2 + forkH / 2, 0)\n forkGroup.add(rp)\n const baseMesh = new THREE.Mesh(new THREE.BoxGeometry(carrW * 0.7, forkH, forkW), forkMat)\n baseMesh.position.set(0, -carrH / 2 + forkH / 2, -forkLength / 2 + forkW / 2)\n forkGroup.add(baseMesh)\n\n this._cranes.push({\n mastGroup, carriageGroup, forkGroup,\n state: { x: craneX, y: craneY, forkZ: 0, targetX: craneX, targetY: craneY, targetForkZ, bayWidth, levelHeight, maxBays, levels, leftRackDepth, rightRackDepth, aisleDepth }\n })\n }\n if (this._cranes.some(c => c.state.targetForkZ !== 0)) this._startCraneAnim()\n }\n\n updateCraneTargets() {\n const cranesData = this.stocker.cranesData\n const alongSize = this.stocker.layout.along\n for (let i = 0; i < this._cranes.length; i++) {\n const crane = this._cranes[i]\n const cd = cranesData[i]\n const s = crane.state\n const bay = cd?.bay ?? Math.round((i + 1) * s.maxBays / (this._cranes.length + 1))\n const level = cd?.level ?? 1\n const side = cd?.side\n const status = cd?.status || 'IDLE'\n s.targetX = -alongSize / 2 + (bay - 0.5) * s.bayWidth\n s.targetY = (level - 0.5) * s.levelHeight\n if ((status === 'PICKING' || status === 'PLACING') && side) {\n s.targetForkZ = side === 'L' ? -(s.aisleDepth / 2 + s.leftRackDepth / 2) : (s.aisleDepth / 2 + s.rightRackDepth / 2)\n } else { s.targetForkZ = 0 }\n }\n this._startCraneAnim()\n }\n\n private _startCraneAnim() {\n if (this._animRaf) return\n const animate = () => {\n let allDone = true\n for (const crane of this._cranes) {\n const s = crane.state, speed = 0.08\n s.x += (s.targetX - s.x) * speed; s.y += (s.targetY - s.y) * speed; s.forkZ += (s.targetForkZ - s.forkZ) * speed\n crane.mastGroup.position.x = s.x; crane.carriageGroup.position.y = s.y; crane.forkGroup.position.z = s.forkZ\n if (Math.abs(s.targetX - s.x) > 0.1 || Math.abs(s.targetY - s.y) > 0.1 || Math.abs(s.targetForkZ - s.forkZ) > 0.1) { allDone = false }\n else { s.x = s.targetX; s.y = s.targetY; s.forkZ = s.targetForkZ; crane.mastGroup.position.x = s.x; crane.carriageGroup.position.y = s.y; crane.forkGroup.position.z = s.forkZ }\n }\n if (allDone) { this._animRaf = 0; return }\n this._animRaf = requestAnimationFrame(animate)\n }\n this._animRaf = requestAnimationFrame(animate)\n }\n\n onchange(after: Record<string, unknown>, before: Record<string, unknown>) {\n if ('data' in after) { this.updateCraneTargets(); this.update(); return }\n if ('lBays' in after || 'lLevels' in after || 'lDepth' in after || 'rBays' in after || 'rLevels' in after || 'rDepth' in after ||\n 'cranes' in after || 'depth' in after || 'aisleRatio' in after ||\n 'width' in after || 'height' in after || 'locationRule' in after || 'hideRackFrame' in after || 'fillStyle' in after || 'frontSide' in after) {\n this.update(); return\n }\n super.onchange(after, before)\n }\n\n dispose() {\n if (this._animRaf) { cancelAnimationFrame(this._animRaf); this._animRaf = 0 }\n this._cranes = []; this._cellMeshes = []\n super.dispose()\n }\n\n updateDimension() {}\n updateAlpha() {}\n}\n"]}
@@ -0,0 +1,14 @@
1
+ import { RealObjectGroup } from '@hatiolab/things-scene';
2
+ import type { StockerPort } from './stocker-port.js';
3
+ export declare class StockerPort3d extends RealObjectGroup {
4
+ get port(): StockerPort;
5
+ get position(): {
6
+ x: number;
7
+ y: any;
8
+ z: number;
9
+ };
10
+ build(): void;
11
+ onchange(after: Record<string, unknown>, before: Record<string, unknown>): void;
12
+ updateDimension(): void;
13
+ updateAlpha(): void;
14
+ }
@@ -0,0 +1,80 @@
1
+ /*
2
+ * Copyright © HatioLab Inc. All rights reserved.
3
+ *
4
+ * StockerPort 3D — 독립 포트의 3D 렌더링.
5
+ */
6
+ import * as THREE from 'three';
7
+ import { RealObjectGroup } from '@hatiolab/things-scene';
8
+ const PORT_IN_COLOR = 0x27ae60;
9
+ const PORT_OUT_COLOR = 0xe67e22;
10
+ const CARRIER_COLOR = 0x4a9eff;
11
+ const EMPTY_COLOR = 0xdddddd;
12
+ export class StockerPort3d extends RealObjectGroup {
13
+ get port() {
14
+ return this.component;
15
+ }
16
+ get position() {
17
+ const { zPos = 0 } = this.component.state;
18
+ return { x: this.cx || 0, y: zPos, z: this.cy || 0 };
19
+ }
20
+ build() {
21
+ super.build();
22
+ const { width = 30, height = 20, depth = 15, rotation = 0 } = this.component.state;
23
+ const isIn = this.port.portType === 'in';
24
+ const hasCarrier = this.port.hasCarrier;
25
+ this.object3d.rotation.y = -rotation;
26
+ const portColor = isIn ? PORT_IN_COLOR : PORT_OUT_COLOR;
27
+ // 포트 본체 (테이블/컨베이어 형태)
28
+ const baseH = depth * 0.6;
29
+ const legH = depth * 0.4;
30
+ const legW = Math.min(width, height) * 0.08;
31
+ // 상판
32
+ const topGeom = new THREE.BoxGeometry(width, 2, height);
33
+ const topMat = new THREE.MeshStandardMaterial({ color: portColor, metalness: 0.3, roughness: 0.7 });
34
+ const topMesh = new THREE.Mesh(topGeom, topMat);
35
+ topMesh.position.y = legH + 1;
36
+ topMesh.castShadow = true;
37
+ this.object3d.add(topMesh);
38
+ // 다리 4개
39
+ const legGeom = new THREE.BoxGeometry(legW, legH, legW);
40
+ const legMat = new THREE.MeshStandardMaterial({ color: 0x888888, metalness: 0.6, roughness: 0.4 });
41
+ const hw = width / 2 - legW;
42
+ const hd = height / 2 - legW;
43
+ for (const [lx, lz] of [[-hw, -hd], [-hw, hd], [hw, -hd], [hw, hd]]) {
44
+ const leg = new THREE.Mesh(legGeom, legMat);
45
+ leg.position.set(lx, legH / 2, lz);
46
+ this.object3d.add(leg);
47
+ }
48
+ // carrier가 있으면 상판 위에 박스 표시
49
+ if (hasCarrier) {
50
+ const carrierW = width * 0.7;
51
+ const carrierH = depth * 0.3;
52
+ const carrierD = height * 0.7;
53
+ const carrierGeom = new THREE.BoxGeometry(carrierW, carrierH, carrierD);
54
+ const carrierMat = new THREE.MeshStandardMaterial({ color: CARRIER_COLOR, metalness: 0.1, roughness: 0.8 });
55
+ const carrierMesh = new THREE.Mesh(carrierGeom, carrierMat);
56
+ carrierMesh.position.y = legH + 2 + carrierH / 2;
57
+ carrierMesh.castShadow = true;
58
+ this.object3d.add(carrierMesh);
59
+ }
60
+ // 방향 표시 (작은 화살표 형태 — 상판 위)
61
+ const arrowLen = Math.min(width, height) * 0.3;
62
+ const arrowGeom = new THREE.ConeGeometry(arrowLen * 0.4, arrowLen, 4);
63
+ const arrowMat = new THREE.MeshStandardMaterial({ color: portColor, metalness: 0.2, roughness: 0.8 });
64
+ const arrowMesh = new THREE.Mesh(arrowGeom, arrowMat);
65
+ arrowMesh.position.set(0, legH + 3, 0);
66
+ // IN: 아래로(+z), OUT: 위로(-z)
67
+ arrowMesh.rotation.x = isIn ? 0 : Math.PI;
68
+ this.object3d.add(arrowMesh);
69
+ }
70
+ onchange(after, before) {
71
+ if ('data' in after || 'portType' in after || 'width' in after || 'height' in after || 'depth' in after) {
72
+ this.update();
73
+ return;
74
+ }
75
+ super.onchange(after, before);
76
+ }
77
+ updateDimension() { }
78
+ updateAlpha() { }
79
+ }
80
+ //# sourceMappingURL=stocker-port-3d.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stocker-port-3d.js","sourceRoot":"","sources":["../src/stocker-port-3d.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * Copyright © HatioLab Inc. All rights reserved.\n *\n * StockerPort 3D — 독립 포트의 3D 렌더링.\n */\n\nimport * as THREE from 'three'\nimport { RealObjectGroup } from '@hatiolab/things-scene'\nimport type { StockerPort } from './stocker-port.js'\n\nconst PORT_IN_COLOR = 0x27ae60\nconst PORT_OUT_COLOR = 0xe67e22\nconst CARRIER_COLOR = 0x4a9eff\nconst EMPTY_COLOR = 0xdddddd\n\nexport class StockerPort3d extends RealObjectGroup {\n\n get port(): StockerPort {\n return this.component as unknown as StockerPort\n }\n\n get position() {\n const { zPos = 0 } = this.component.state\n return { x: this.cx || 0, y: zPos, z: this.cy || 0 }\n }\n\n build() {\n super.build()\n\n const { width = 30, height = 20, depth = 15, rotation = 0 } = this.component.state\n const isIn = this.port.portType === 'in'\n const hasCarrier = this.port.hasCarrier\n\n this.object3d.rotation.y = -rotation\n\n const portColor = isIn ? PORT_IN_COLOR : PORT_OUT_COLOR\n\n // 포트 본체 (테이블/컨베이어 형태)\n const baseH = depth * 0.6\n const legH = depth * 0.4\n const legW = Math.min(width, height) * 0.08\n\n // 상판\n const topGeom = new THREE.BoxGeometry(width, 2, height)\n const topMat = new THREE.MeshStandardMaterial({ color: portColor, metalness: 0.3, roughness: 0.7 })\n const topMesh = new THREE.Mesh(topGeom, topMat)\n topMesh.position.y = legH + 1\n topMesh.castShadow = true\n this.object3d.add(topMesh)\n\n // 다리 4개\n const legGeom = new THREE.BoxGeometry(legW, legH, legW)\n const legMat = new THREE.MeshStandardMaterial({ color: 0x888888, metalness: 0.6, roughness: 0.4 })\n const hw = width / 2 - legW\n const hd = height / 2 - legW\n for (const [lx, lz] of [[-hw, -hd], [-hw, hd], [hw, -hd], [hw, hd]]) {\n const leg = new THREE.Mesh(legGeom, legMat)\n leg.position.set(lx, legH / 2, lz)\n this.object3d.add(leg)\n }\n\n // carrier가 있으면 상판 위에 박스 표시\n if (hasCarrier) {\n const carrierW = width * 0.7\n const carrierH = depth * 0.3\n const carrierD = height * 0.7\n const carrierGeom = new THREE.BoxGeometry(carrierW, carrierH, carrierD)\n const carrierMat = new THREE.MeshStandardMaterial({ color: CARRIER_COLOR, metalness: 0.1, roughness: 0.8 })\n const carrierMesh = new THREE.Mesh(carrierGeom, carrierMat)\n carrierMesh.position.y = legH + 2 + carrierH / 2\n carrierMesh.castShadow = true\n this.object3d.add(carrierMesh)\n }\n\n // 방향 표시 (작은 화살표 형태 — 상판 위)\n const arrowLen = Math.min(width, height) * 0.3\n const arrowGeom = new THREE.ConeGeometry(arrowLen * 0.4, arrowLen, 4)\n const arrowMat = new THREE.MeshStandardMaterial({ color: portColor, metalness: 0.2, roughness: 0.8 })\n const arrowMesh = new THREE.Mesh(arrowGeom, arrowMat)\n arrowMesh.position.set(0, legH + 3, 0)\n // IN: 아래로(+z), OUT: 위로(-z)\n arrowMesh.rotation.x = isIn ? 0 : Math.PI\n this.object3d.add(arrowMesh)\n }\n\n onchange(after: Record<string, unknown>, before: Record<string, unknown>) {\n if ('data' in after || 'portType' in after || 'width' in after || 'height' in after || 'depth' in after) {\n this.update()\n return\n }\n super.onchange(after, before)\n }\n\n updateDimension() {}\n updateAlpha() {}\n}\n"]}