@fmsim/machine 2.0.0-beta.3 → 2.0.0-beta.6
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.
- package/dist/carrier.js +2 -2
- package/dist/carrier.js.map +1 -1
- package/dist/factories/agv-3d.js +33 -6
- package/dist/factories/agv-3d.js.map +1 -1
- package/dist/factories/machine-3d.js +395 -56
- package/dist/factories/machine-3d.js.map +1 -1
- package/dist/factories/oht-3d.js +34 -8
- package/dist/factories/oht-3d.js.map +1 -1
- package/dist/factories/shuttle-3d.js +32 -6
- package/dist/factories/shuttle-3d.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -3
- package/src/carrier.ts +2 -2
- package/src/factories/agv-3d.ts +32 -6
- package/src/factories/machine-3d.ts +446 -40
- package/src/factories/oht-3d.ts +32 -8
- package/src/factories/shuttle-3d.ts +30 -6
|
@@ -8,7 +8,9 @@
|
|
|
8
8
|
* - Unit: 낮은 depth, 불투명
|
|
9
9
|
*/
|
|
10
10
|
import * as THREE from 'three';
|
|
11
|
-
import { RealObjectGroup, registerRealObjectFactory } from '@hatiolab/things-scene';
|
|
11
|
+
import { RealObjectGroup, registerRealObjectFactory, disposeObject3D } from '@hatiolab/things-scene';
|
|
12
|
+
import { LEGEND_CAPACITY } from '../features/mcs-status-default';
|
|
13
|
+
import { getVaueOnRanges } from '../utils/get-value-on-ranges';
|
|
12
14
|
/**
|
|
13
15
|
* 포트 기본 높이 (절대값).
|
|
14
16
|
* 모든 이송장치의 상단 높이 = 이 값.
|
|
@@ -27,6 +29,8 @@ const DEFAULTS = {
|
|
|
27
29
|
class MCSRealObject3D extends RealObjectGroup {
|
|
28
30
|
constructor(component, config) {
|
|
29
31
|
super(component);
|
|
32
|
+
// ── Change handling ──
|
|
33
|
+
this._updatePending = false;
|
|
30
34
|
this._config = config;
|
|
31
35
|
}
|
|
32
36
|
// width, height 절대값 (음수 값은 양수로 취급)
|
|
@@ -139,7 +143,6 @@ class MCSRealObject3D extends RealObjectGroup {
|
|
|
139
143
|
geo.rotateX(Math.PI / 2);
|
|
140
144
|
return this.removeBottomFace(geo);
|
|
141
145
|
}
|
|
142
|
-
// ── Build ──
|
|
143
146
|
build() {
|
|
144
147
|
var _a;
|
|
145
148
|
super.build();
|
|
@@ -172,30 +175,143 @@ class MCSRealObject3D extends RealObjectGroup {
|
|
|
172
175
|
material.polygonOffset = true;
|
|
173
176
|
material.polygonOffsetFactor = -1;
|
|
174
177
|
material.polygonOffsetUnit = -1;
|
|
175
|
-
|
|
178
|
+
// BoxGeometry material groups: +x(0), -x(1), +y(2), -y(3), +z(4), -z(5)
|
|
179
|
+
let meshMaterial = material;
|
|
180
|
+
if (shape === 'box') {
|
|
181
|
+
if (this._config.openTop) {
|
|
182
|
+
// 상단면 제거 → 5면 개방 박스
|
|
183
|
+
const invisible = new THREE.MeshBasicMaterial({ visible: false });
|
|
184
|
+
meshMaterial = [material, material, invisible, material, material, material];
|
|
185
|
+
}
|
|
186
|
+
else if (this._config.texturedTop) {
|
|
187
|
+
// 상판에 2D 렌더링 텍스처
|
|
188
|
+
const topMat = this.buildTopTexture(w, h);
|
|
189
|
+
meshMaterial = [material, material, topMat, material, material, material];
|
|
190
|
+
}
|
|
191
|
+
else if (this._config.matteTop) {
|
|
192
|
+
// 상판만 무반사
|
|
193
|
+
const matte = new THREE.MeshBasicMaterial({ color, opacity, transparent: opacity < 1 });
|
|
194
|
+
meshMaterial = [material, material, matte, material, material, material];
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
this._ownMesh = new THREE.Mesh(geometry, meshMaterial);
|
|
176
198
|
// sizeRatio < 1이면 부모 중심에 배치 (rawDepth/2), 아니면 자체 중심 (depth/2)
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
this.object3d.add(
|
|
199
|
+
this._ownMesh.position.y = ratio < 1 ? rawDepth / 2 : depth / 2;
|
|
200
|
+
this._ownMesh.castShadow = true;
|
|
201
|
+
this._ownMesh.receiveShadow = true;
|
|
202
|
+
this.object3d.add(this._ownMesh);
|
|
181
203
|
}
|
|
182
|
-
// ── Change handling ──
|
|
183
204
|
onchange(after, before) {
|
|
184
205
|
if ('width' in after ||
|
|
185
206
|
'height' in after ||
|
|
186
207
|
'depth' in after ||
|
|
187
208
|
'fillStyle' in after ||
|
|
188
209
|
'strokeStyle' in after ||
|
|
189
|
-
'id' in after
|
|
190
|
-
|
|
191
|
-
|
|
210
|
+
'id' in after ||
|
|
211
|
+
'EMPTYTYPE' in after ||
|
|
212
|
+
'CARRIERSTATUS' in after) {
|
|
213
|
+
// 연속 setState에 의한 다중 rebuild 방지: 한 프레임에 한 번만 실행
|
|
214
|
+
if (!this._updatePending) {
|
|
215
|
+
this._updatePending = true;
|
|
216
|
+
queueMicrotask(() => {
|
|
217
|
+
this._updatePending = false;
|
|
218
|
+
this.update();
|
|
219
|
+
});
|
|
220
|
+
}
|
|
192
221
|
return;
|
|
193
222
|
}
|
|
194
223
|
super.onchange(after, before);
|
|
195
224
|
}
|
|
196
|
-
|
|
225
|
+
/**
|
|
226
|
+
* 상판 텍스처: 컴포넌트의 2D render()를 캔버스에 그려서 텍스처로 사용
|
|
227
|
+
*/
|
|
228
|
+
buildTopTexture(w, h) {
|
|
229
|
+
var _a;
|
|
230
|
+
const scale = 4;
|
|
231
|
+
const cw = this.nextPow2(w * scale);
|
|
232
|
+
const ch = this.nextPow2(h * scale);
|
|
233
|
+
const canvas = document.createElement('canvas');
|
|
234
|
+
canvas.width = cw;
|
|
235
|
+
canvas.height = ch;
|
|
236
|
+
const ctx = canvas.getContext('2d');
|
|
237
|
+
const statusColor = '#' + this.resolveColor().toString(16).padStart(6, '0');
|
|
238
|
+
const emptyType = ((_a = this.component.state) === null || _a === void 0 ? void 0 : _a.EMPTYTYPE) || '';
|
|
239
|
+
const text = emptyType === 'FULL' ? 'F' : emptyType === 'EMPTY' ? 'E' : emptyType === 'EMPTYEMPTY' ? 'X' : '';
|
|
240
|
+
// 실제 사용 영역 (nextPow2 이전 크기)
|
|
241
|
+
const usedW = w * scale;
|
|
242
|
+
const usedH = h * scale;
|
|
243
|
+
// 바탕: 캐리어 상태 색상 (사용 영역만)
|
|
244
|
+
ctx.fillStyle = statusColor;
|
|
245
|
+
ctx.fillRect(0, 0, usedW, usedH);
|
|
246
|
+
// 텍스트 (사용 영역 중앙)
|
|
247
|
+
if (text) {
|
|
248
|
+
const fontSize = Math.round(Math.min(usedW, usedH) * 0.7);
|
|
249
|
+
ctx.font = `bold ${fontSize}px Arial`;
|
|
250
|
+
ctx.textAlign = 'center';
|
|
251
|
+
ctx.textBaseline = 'middle';
|
|
252
|
+
ctx.fillStyle = '#000000';
|
|
253
|
+
ctx.fillText(text, usedW / 2, usedH / 2);
|
|
254
|
+
}
|
|
255
|
+
const texture = new THREE.CanvasTexture(canvas);
|
|
256
|
+
texture.offset.set(0, 1 - usedH / ch);
|
|
257
|
+
texture.repeat.set(usedW / cw, usedH / ch);
|
|
258
|
+
texture.colorSpace = THREE.SRGBColorSpace;
|
|
259
|
+
return new THREE.MeshBasicMaterial({
|
|
260
|
+
map: texture,
|
|
261
|
+
transparent: true,
|
|
262
|
+
side: THREE.DoubleSide,
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
nextPow2(n) {
|
|
266
|
+
if (!(n > 0))
|
|
267
|
+
return 1;
|
|
268
|
+
let v = Math.ceil(n);
|
|
269
|
+
v--;
|
|
270
|
+
v |= v >> 1;
|
|
271
|
+
v |= v >> 2;
|
|
272
|
+
v |= v >> 4;
|
|
273
|
+
v |= v >> 8;
|
|
274
|
+
v |= v >> 16;
|
|
275
|
+
return v + 1;
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* 자신의 mesh만 제거 후 rebuild.
|
|
279
|
+
* 기본 update()는 clear()로 object3d의 모든 children을 제거하는데,
|
|
280
|
+
* 컨테이너(Port 등)의 경우 자식 컴포넌트(Carrier 등)의 3D 오브젝트도 함께 제거됨.
|
|
281
|
+
* 이를 방지하기 위해 자신이 생성한 mesh만 제거한다.
|
|
282
|
+
*/
|
|
283
|
+
update() {
|
|
284
|
+
// 자신의 mesh만 제거 (자식 컴포넌트 3D 오브젝트 보존)
|
|
285
|
+
if (this._ownMesh) {
|
|
286
|
+
this.object3d.remove(this._ownMesh);
|
|
287
|
+
disposeObject3D(this._ownMesh);
|
|
288
|
+
this._ownMesh = undefined;
|
|
289
|
+
}
|
|
290
|
+
this.build();
|
|
291
|
+
this.updateTransform();
|
|
292
|
+
this.updateAlpha();
|
|
293
|
+
this.updateHidden();
|
|
294
|
+
}
|
|
295
|
+
// RealObjectGroup 기본 동작 비활성화
|
|
197
296
|
updateDimension() { }
|
|
198
|
-
updateAlpha() {
|
|
297
|
+
updateAlpha() {
|
|
298
|
+
const { alpha = 1 } = this.component.state;
|
|
299
|
+
const configOpacity = this._config.opacity;
|
|
300
|
+
const effective = configOpacity * alpha;
|
|
301
|
+
this.object3d.traverse((o) => {
|
|
302
|
+
if (!o.material)
|
|
303
|
+
return;
|
|
304
|
+
const materials = Array.isArray(o.material) ? o.material : [o.material];
|
|
305
|
+
for (const m of materials) {
|
|
306
|
+
if (!m.visible)
|
|
307
|
+
continue; // openTop invisible face 무시
|
|
308
|
+
if (m.map)
|
|
309
|
+
continue; // 텍스처 material은 자체 transparent 유지
|
|
310
|
+
m.opacity = effective;
|
|
311
|
+
m.transparent = effective < 1;
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
}
|
|
199
315
|
}
|
|
200
316
|
// ── 팩토리 등록 헬퍼 ─────────────────────────────────────
|
|
201
317
|
function register(type, overrides = {}) {
|
|
@@ -207,14 +323,14 @@ function register(type, overrides = {}) {
|
|
|
207
323
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
208
324
|
register('Equipment', {
|
|
209
325
|
defaultDepth: 3 * TRANSPORT_SURFACE_HEIGHT,
|
|
210
|
-
opacity: 0.
|
|
326
|
+
opacity: 0.75,
|
|
211
327
|
metalness: 0.4,
|
|
212
328
|
roughness: 0.6,
|
|
213
329
|
defaultColor: 0x8899aa,
|
|
214
330
|
});
|
|
215
331
|
register('Buffer', {
|
|
216
332
|
defaultDepth: TRANSPORT_SURFACE_HEIGHT,
|
|
217
|
-
opacity: 0
|
|
333
|
+
opacity: 1.0,
|
|
218
334
|
metalness: 0.3,
|
|
219
335
|
roughness: 0.7,
|
|
220
336
|
defaultColor: 0x99aa88,
|
|
@@ -232,57 +348,280 @@ register('OHTLine', {
|
|
|
232
348
|
roughness: 0.3,
|
|
233
349
|
defaultColor: 0x888899,
|
|
234
350
|
});
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
351
|
+
// STOCKER: 벽면에 용량 게이지 바를 포함하는 커스텀 3D 오브젝트
|
|
352
|
+
const STOCKER_CONFIG = Object.assign(Object.assign({}, DEFAULTS), { defaultDepth: 5 * TRANSPORT_SURFACE_HEIGHT, opacity: 0.75, metalness: 0.3, roughness: 0.6, defaultColor: 0x8899bb });
|
|
353
|
+
class StockerRealObject3D extends MCSRealObject3D {
|
|
354
|
+
constructor(component) {
|
|
355
|
+
super(component, STOCKER_CONFIG);
|
|
356
|
+
}
|
|
357
|
+
build() {
|
|
358
|
+
super.build();
|
|
359
|
+
this.buildGauge();
|
|
360
|
+
}
|
|
361
|
+
buildGauge() {
|
|
362
|
+
var _a, _b;
|
|
363
|
+
const comp = this.component;
|
|
364
|
+
const data = comp.data || {};
|
|
365
|
+
const gaugeMode = ((_b = (_a = comp.root) === null || _a === void 0 ? void 0 : _a.style) === null || _b === void 0 ? void 0 : _b.stockerGaugeMode) || 'capacity';
|
|
366
|
+
const gauge = this.resolveGauge(data, gaugeMode);
|
|
367
|
+
// 테두리는 스토커의 상태 색상 (machine status)
|
|
368
|
+
const statusColor = comp.statusColor;
|
|
369
|
+
if (statusColor && statusColor.toUpperCase() !== '#F0F0F0') {
|
|
370
|
+
gauge.borderColor = statusColor;
|
|
371
|
+
}
|
|
372
|
+
const { w, h } = this.absSize;
|
|
373
|
+
const depth = this.effectiveDepth;
|
|
374
|
+
const isWidthLonger = w >= h;
|
|
375
|
+
// 게이지 캔버스 (고해상도)
|
|
376
|
+
const barW = 130;
|
|
377
|
+
const barH = 26;
|
|
378
|
+
const scale = 4;
|
|
379
|
+
const canvasW = barW * scale;
|
|
380
|
+
const canvasH = barH * scale;
|
|
381
|
+
const canvas = document.createElement('canvas');
|
|
382
|
+
canvas.width = canvasW;
|
|
383
|
+
canvas.height = canvasH;
|
|
384
|
+
const ctx = canvas.getContext('2d');
|
|
385
|
+
const pad = 12;
|
|
386
|
+
// 배경 + 테두리
|
|
387
|
+
ctx.fillStyle = '#dddddd';
|
|
388
|
+
ctx.fillRect(0, 0, canvasW, canvasH);
|
|
389
|
+
ctx.strokeStyle = gauge.borderColor;
|
|
390
|
+
ctx.lineWidth = 12;
|
|
391
|
+
ctx.strokeRect(6, 6, canvasW - 12, canvasH - 12);
|
|
392
|
+
// 세그먼트 채움 (왼→오)
|
|
393
|
+
const innerW = canvasW - pad * 2;
|
|
394
|
+
const innerH = canvasH - pad * 2;
|
|
395
|
+
let x = pad;
|
|
396
|
+
for (const seg of gauge.segments) {
|
|
397
|
+
const segW = Math.max(innerW * seg.ratio, seg.ratio > 0 ? 1 : 0);
|
|
398
|
+
if (segW <= 0)
|
|
399
|
+
continue;
|
|
400
|
+
ctx.fillStyle = seg.color;
|
|
401
|
+
ctx.fillRect(x, pad, segW, innerH);
|
|
402
|
+
x += segW;
|
|
403
|
+
}
|
|
404
|
+
// 텍스트
|
|
405
|
+
ctx.font = `bold ${10 * scale}px sans-serif`;
|
|
406
|
+
ctx.textAlign = 'center';
|
|
407
|
+
ctx.textBaseline = 'middle';
|
|
408
|
+
ctx.fillStyle = '#333333';
|
|
409
|
+
ctx.fillText(gauge.label, canvasW / 2, canvasH / 2);
|
|
410
|
+
// 텍스처 → Plane
|
|
411
|
+
const texture = new THREE.CanvasTexture(canvas);
|
|
412
|
+
texture.colorSpace = THREE.SRGBColorSpace;
|
|
413
|
+
const geo = new THREE.PlaneGeometry(barW, barH);
|
|
414
|
+
geo.rotateX(-Math.PI / 2); // 수평 (지붕 위)
|
|
415
|
+
if (!isWidthLonger)
|
|
416
|
+
geo.rotateY(Math.PI / 2);
|
|
417
|
+
const mat = new THREE.MeshBasicMaterial({
|
|
418
|
+
map: texture,
|
|
419
|
+
transparent: true,
|
|
420
|
+
side: THREE.DoubleSide,
|
|
421
|
+
});
|
|
422
|
+
mat.polygonOffset = true;
|
|
423
|
+
mat.polygonOffsetFactor = -2;
|
|
424
|
+
mat.polygonOffsetUnit = -2;
|
|
425
|
+
this.gaugeMesh = new THREE.Mesh(geo, mat);
|
|
426
|
+
this.gaugeMesh.position.set(0, depth + 0.5, 0);
|
|
427
|
+
this.object3d.add(this.gaugeMesh);
|
|
428
|
+
}
|
|
429
|
+
resolveGauge(data, mode) {
|
|
430
|
+
var _a, _b;
|
|
431
|
+
switch (mode) {
|
|
432
|
+
case 'carrier': {
|
|
433
|
+
const full = Number(data.FULL_CNT) || 0;
|
|
434
|
+
const empty = Number(data.EMPTY_CNT) || 0;
|
|
435
|
+
const emptyEmpty = Number(data.EMPTYEMPTY_CNT) || 0;
|
|
436
|
+
const total = full + empty + emptyEmpty;
|
|
437
|
+
const segments = total > 0
|
|
438
|
+
? [
|
|
439
|
+
{ ratio: full / total, color: '#4AA7FE' },
|
|
440
|
+
{ ratio: empty / total, color: '#8BDA5B' },
|
|
441
|
+
{ ratio: emptyEmpty / total, color: '#019D59' },
|
|
442
|
+
]
|
|
443
|
+
: [{ ratio: 1, color: '#dddddd' }];
|
|
444
|
+
return {
|
|
445
|
+
segments,
|
|
446
|
+
borderColor: '#4AA7FE',
|
|
447
|
+
label: total > 0 ? `F${full} E${empty} EE${emptyEmpty}` : '',
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
case 'abnormal': {
|
|
451
|
+
const normal = Number(data.CARRIER_NORMAL) || 0;
|
|
452
|
+
const abnormal = Number(data.CARRIER_ABNORMAL) || 0;
|
|
453
|
+
const total = normal + abnormal;
|
|
454
|
+
const segments = total > 0
|
|
455
|
+
? [
|
|
456
|
+
{ ratio: normal / total, color: '#8BDA5B' },
|
|
457
|
+
{ ratio: abnormal / total, color: '#F4B4B4' },
|
|
458
|
+
]
|
|
459
|
+
: [{ ratio: 1, color: '#dddddd' }];
|
|
460
|
+
return {
|
|
461
|
+
segments,
|
|
462
|
+
borderColor: abnormal > 0 ? '#F4B4B4' : '#8BDA5B',
|
|
463
|
+
label: total > 0 ? `N${normal} A${abnormal}` : '',
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
default: { // capacity
|
|
467
|
+
const cur = Number(data.CURRENTCAPACITY) || 0;
|
|
468
|
+
const max = Number(data.MAXCAPACITY) || 100;
|
|
469
|
+
const fullup = data.FULLUP === 'T';
|
|
470
|
+
const usage = Math.round((cur / max) * 100);
|
|
471
|
+
const legend = ((_b = (_a = this.component.root) === null || _a === void 0 ? void 0 : _a.style) === null || _b === void 0 ? void 0 : _b.LEGEND_CAPACITY) || LEGEND_CAPACITY;
|
|
472
|
+
const color = fullup ? 'red' : (getVaueOnRanges(usage, legend) || '#44aa44');
|
|
473
|
+
return {
|
|
474
|
+
segments: [{ ratio: usage / 100, color }],
|
|
475
|
+
borderColor: color,
|
|
476
|
+
label: `${usage}%`,
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
onchange(after, before) {
|
|
482
|
+
if ('data' in after) {
|
|
483
|
+
this.clearGauge();
|
|
484
|
+
this.buildGauge();
|
|
485
|
+
}
|
|
486
|
+
super.onchange(after, before);
|
|
487
|
+
}
|
|
488
|
+
clear() {
|
|
489
|
+
this.clearGauge();
|
|
490
|
+
return super.clear();
|
|
491
|
+
}
|
|
492
|
+
clearGauge() {
|
|
493
|
+
var _a, _b;
|
|
494
|
+
if (this.gaugeMesh) {
|
|
495
|
+
this.object3d.remove(this.gaugeMesh);
|
|
496
|
+
(_a = this.gaugeMesh.geometry) === null || _a === void 0 ? void 0 : _a.dispose();
|
|
497
|
+
const mat = this.gaugeMesh.material;
|
|
498
|
+
(_b = mat === null || mat === void 0 ? void 0 : mat.map) === null || _b === void 0 ? void 0 : _b.dispose();
|
|
499
|
+
mat === null || mat === void 0 ? void 0 : mat.dispose();
|
|
500
|
+
this.gaugeMesh = undefined;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
registerRealObjectFactory('STOCKER', (component) => new StockerRealObject3D(component));
|
|
242
505
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
243
506
|
// Unit (이동체 · 홀더) — 낮은 depth, 불투명
|
|
244
507
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
opacity: 0.5,
|
|
249
|
-
metalness: 0.4,
|
|
250
|
-
roughness: 0.6,
|
|
251
|
-
defaultColor: 0xb0a090,
|
|
252
|
-
});
|
|
508
|
+
// Port: 상부 개방 5면 박스
|
|
509
|
+
const PORT_CONFIG = Object.assign(Object.assign({}, DEFAULTS), { depthRatio: 0.5, defaultZPosFromParent: true, opacity: 0.5, metalness: 0.4, roughness: 0.6, defaultColor: 0xb0a090, openTop: true });
|
|
510
|
+
registerRealObjectFactory('Port', (component) => new MCSRealObject3D(component, PORT_CONFIG));
|
|
253
511
|
register('Shelf', {
|
|
254
512
|
depthRatio: 0.25,
|
|
255
513
|
metalness: 0.3,
|
|
256
514
|
roughness: 0.7,
|
|
257
515
|
defaultColor: 0xaabbcc,
|
|
258
516
|
});
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
}
|
|
517
|
+
// Carrier: CARRIERSTATUS에 따른 3D 애니메이션 지원
|
|
518
|
+
const CARRIER_CONFIG = Object.assign(Object.assign({}, DEFAULTS), { depthRatio: 0.5, metalness: 0.2, roughness: 0.8, defaultColor: 0x66bb66, isVisible: (c) => !!c.state.id, sizeRatio: 0.8, texturedTop: true });
|
|
519
|
+
class CarrierRealObject3D extends MCSRealObject3D {
|
|
520
|
+
constructor(component) {
|
|
521
|
+
super(component, CARRIER_CONFIG);
|
|
522
|
+
this._animRaf = null;
|
|
523
|
+
this._animStartTime = 0;
|
|
524
|
+
}
|
|
525
|
+
build() {
|
|
526
|
+
super.build();
|
|
527
|
+
this.startCarrierAnimation();
|
|
528
|
+
}
|
|
529
|
+
/**
|
|
530
|
+
* CARRIERSTATUS의 애니메이션 설정을 읽어 3D에서 직접 실행.
|
|
531
|
+
* things-scene의 delta 시스템과 독립적으로 동작.
|
|
532
|
+
*/
|
|
533
|
+
startCarrierAnimation() {
|
|
534
|
+
var _a, _b, _c;
|
|
535
|
+
this.stopCarrierAnimation();
|
|
536
|
+
const comp = this.component;
|
|
537
|
+
if (!((_a = comp.app) === null || _a === void 0 ? void 0 : _a.isViewMode))
|
|
538
|
+
return;
|
|
539
|
+
const carrierStatus = (_b = comp.state) === null || _b === void 0 ? void 0 : _b.CARRIERSTATUS;
|
|
540
|
+
if (!carrierStatus)
|
|
541
|
+
return;
|
|
542
|
+
// 애니메이션 설정 가져오기
|
|
543
|
+
const config = (_c = comp.getAnimationConfig) === null || _c === void 0 ? void 0 : _c.call(comp, carrierStatus);
|
|
544
|
+
if (!(config === null || config === void 0 ? void 0 : config.animation))
|
|
545
|
+
return;
|
|
546
|
+
const { type, duration = 2000, scale: animScale = 1.3 } = config.animation;
|
|
547
|
+
if (!type)
|
|
548
|
+
return;
|
|
549
|
+
this._animStartTime = performance.now();
|
|
550
|
+
// 애니메이션 대상: group(object3d) — mesh 개별 변환 대신 group 전체를 변환하여
|
|
551
|
+
// 회전/스케일 중심이 component의 3D 위치(= 2D 중심)와 정확히 일치하도록 함
|
|
552
|
+
const obj = this.object3d;
|
|
553
|
+
const animate = () => {
|
|
554
|
+
var _a;
|
|
555
|
+
if (!this._ownMesh)
|
|
556
|
+
return;
|
|
557
|
+
const elapsed = performance.now() - this._animStartTime;
|
|
558
|
+
const progress = (elapsed % duration) / duration; // 0~1 반복
|
|
559
|
+
switch (type) {
|
|
560
|
+
case 'heartbeat': {
|
|
561
|
+
const t = progress < 0.5 ? progress * 2 : (1 - progress) * 2;
|
|
562
|
+
const s = 1 + (animScale - 1) * t;
|
|
563
|
+
obj.scale.set(s, 1, s);
|
|
564
|
+
break;
|
|
565
|
+
}
|
|
566
|
+
case 'rotation': {
|
|
567
|
+
obj.rotation.y = progress * Math.PI * 2;
|
|
568
|
+
break;
|
|
569
|
+
}
|
|
570
|
+
case 'vibration': {
|
|
571
|
+
const theta = (_a = config.animation.theta) !== null && _a !== void 0 ? _a : 0.26;
|
|
572
|
+
const current = Math.sin(progress * Math.PI * 2) * theta;
|
|
573
|
+
obj.rotation.y = current;
|
|
574
|
+
break;
|
|
575
|
+
}
|
|
576
|
+
case 'fade': {
|
|
577
|
+
const alpha = 0.3 + 0.7 * (progress < 0.5 ? progress * 2 : (1 - progress) * 2);
|
|
578
|
+
obj.traverse((o) => {
|
|
579
|
+
if (o.material && !o.material.map) {
|
|
580
|
+
o.material.opacity = alpha;
|
|
581
|
+
o.material.transparent = true;
|
|
582
|
+
}
|
|
583
|
+
});
|
|
584
|
+
break;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
this._animRaf = requestAnimationFrame(animate);
|
|
588
|
+
};
|
|
589
|
+
this._animRaf = requestAnimationFrame(animate);
|
|
590
|
+
}
|
|
591
|
+
// 자체 애니메이션이 실행 중이면 things-scene의 syncAnimationDelta 비활성화
|
|
592
|
+
syncAnimationDelta() {
|
|
593
|
+
if (!this._animRaf) {
|
|
594
|
+
super.syncAnimationDelta();
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
stopCarrierAnimation() {
|
|
598
|
+
if (this._animRaf) {
|
|
599
|
+
cancelAnimationFrame(this._animRaf);
|
|
600
|
+
this._animRaf = null;
|
|
601
|
+
}
|
|
602
|
+
// group 스케일/회전 초기화
|
|
603
|
+
this.object3d.scale.set(1, 1, 1);
|
|
604
|
+
this.object3d.rotation.y = 0;
|
|
605
|
+
}
|
|
606
|
+
update() {
|
|
607
|
+
this.stopCarrierAnimation();
|
|
608
|
+
super.update();
|
|
609
|
+
}
|
|
610
|
+
clear() {
|
|
611
|
+
this.stopCarrierAnimation();
|
|
612
|
+
return super.clear();
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
registerRealObjectFactory('Carrier', (component) => new CarrierRealObject3D(component));
|
|
267
616
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
268
617
|
// Gauge (용량 게이지) — 얇은 바
|
|
269
618
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
metalness: 0.2,
|
|
279
|
-
roughness: 0.8,
|
|
280
|
-
defaultColor: 0x44aa44,
|
|
281
|
-
});
|
|
282
|
-
register('MCSGaugeCapacityBar', {
|
|
283
|
-
depthRatio: 0.15,
|
|
284
|
-
metalness: 0.2,
|
|
285
|
-
roughness: 0.8,
|
|
286
|
-
defaultColor: 0x44aa44,
|
|
287
|
-
});
|
|
619
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
620
|
+
// Gauge (용량 게이지) — 2D 렌더링을 Plane 텍스처로 3D 표시
|
|
621
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
622
|
+
// Gauge 컴포넌트 (StockerCapacityBar, ZoneCapacityBar, MCSGaugeCapacityBar)는
|
|
623
|
+
// 3D에서 표시하지 않음. 대신 부모(STOCKER 등)의 3D 모델에서 직접 게이지를 렌더링.
|
|
624
|
+
registerRealObjectFactory('StockerCapacityBar', () => null);
|
|
625
|
+
registerRealObjectFactory('MCSGaugeCapacityBar', () => null);
|
|
626
|
+
registerRealObjectFactory('ZoneCapacityBar', () => null);
|
|
288
627
|
//# sourceMappingURL=machine-3d.js.map
|