@fmsim/machine 2.0.0-beta.2 → 2.0.0-beta.5
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/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@fmsim/machine",
|
|
3
3
|
"description": "Layout View를 구성하는 설비(machine)들과 유닛을 구현한 모듈입니다.",
|
|
4
4
|
"author": "heartyoh",
|
|
5
|
-
"version": "2.0.0-beta.
|
|
5
|
+
"version": "2.0.0-beta.5",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"module": "dist/index.js",
|
|
8
8
|
"things-scene": true,
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"migration": "things-factory-migration"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@fmsim/api": "^2.0.0-beta.
|
|
28
|
+
"@fmsim/api": "^2.0.0-beta.3",
|
|
29
29
|
"@hatiolab/things-scene": "^10.0.0-beta.1"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
@@ -54,5 +54,5 @@
|
|
|
54
54
|
"prettier --write"
|
|
55
55
|
]
|
|
56
56
|
},
|
|
57
|
-
"gitHead": "
|
|
57
|
+
"gitHead": "522cb3abab43f7a3c8850cab80d1b9187d6447d6"
|
|
58
58
|
}
|
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
import * as THREE from 'three'
|
|
12
12
|
import { RealObjectGroup, registerRealObjectFactory } from '@hatiolab/things-scene'
|
|
13
13
|
import type { Component } from '@hatiolab/things-scene'
|
|
14
|
+
import { LEGEND_CAPACITY } from '../features/mcs-status-default'
|
|
15
|
+
import { getVaueOnRanges } from '../utils/get-value-on-ranges'
|
|
14
16
|
|
|
15
17
|
// ── 설정 타입 ────────────────────────────────────────────
|
|
16
18
|
|
|
@@ -62,7 +64,7 @@ class MCSRealObject3D extends RealObjectGroup {
|
|
|
62
64
|
}
|
|
63
65
|
|
|
64
66
|
// width, height 절대값 (음수 값은 양수로 취급)
|
|
65
|
-
|
|
67
|
+
protected get absSize(): { w: number; h: number } {
|
|
66
68
|
const { width = 10, height = 10 } = this.component.state
|
|
67
69
|
return { w: Math.abs(width), h: Math.abs(height) }
|
|
68
70
|
}
|
|
@@ -269,7 +271,7 @@ function register(type: string, overrides: Partial<MCS3DConfig> = {}) {
|
|
|
269
271
|
|
|
270
272
|
register('Equipment', {
|
|
271
273
|
defaultDepth: 3 * TRANSPORT_SURFACE_HEIGHT,
|
|
272
|
-
opacity: 0.
|
|
274
|
+
opacity: 0.75,
|
|
273
275
|
metalness: 0.4,
|
|
274
276
|
roughness: 0.6,
|
|
275
277
|
defaultColor: 0x8899aa,
|
|
@@ -277,7 +279,7 @@ register('Equipment', {
|
|
|
277
279
|
|
|
278
280
|
register('Buffer', {
|
|
279
281
|
defaultDepth: TRANSPORT_SURFACE_HEIGHT,
|
|
280
|
-
opacity: 0
|
|
282
|
+
opacity: 1.0,
|
|
281
283
|
metalness: 0.3,
|
|
282
284
|
roughness: 0.7,
|
|
283
285
|
defaultColor: 0x99aa88,
|
|
@@ -298,13 +300,184 @@ register('OHTLine', {
|
|
|
298
300
|
defaultColor: 0x888899,
|
|
299
301
|
})
|
|
300
302
|
|
|
301
|
-
|
|
303
|
+
// STOCKER: 벽면에 용량 게이지 바를 포함하는 커스텀 3D 오브젝트
|
|
304
|
+
const STOCKER_CONFIG: MCS3DConfig = {
|
|
305
|
+
...DEFAULTS,
|
|
302
306
|
defaultDepth: 5 * TRANSPORT_SURFACE_HEIGHT,
|
|
303
|
-
opacity: 0.
|
|
307
|
+
opacity: 0.75,
|
|
304
308
|
metalness: 0.3,
|
|
305
309
|
roughness: 0.6,
|
|
306
310
|
defaultColor: 0x8899bb,
|
|
307
|
-
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
class StockerRealObject3D extends MCSRealObject3D {
|
|
314
|
+
private gaugeMesh?: THREE.Mesh
|
|
315
|
+
|
|
316
|
+
constructor(component: Component) {
|
|
317
|
+
super(component, STOCKER_CONFIG)
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
build() {
|
|
321
|
+
super.build()
|
|
322
|
+
this.buildGauge()
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
private buildGauge() {
|
|
326
|
+
const comp = this.component as any
|
|
327
|
+
const data = comp.data || {}
|
|
328
|
+
const gaugeMode = comp.root?.style?.stockerGaugeMode || 'capacity'
|
|
329
|
+
|
|
330
|
+
const gauge = this.resolveGauge(data, gaugeMode)
|
|
331
|
+
|
|
332
|
+
// 테두리는 스토커의 상태 색상 (machine status)
|
|
333
|
+
const statusColor = comp.statusColor
|
|
334
|
+
if (statusColor && statusColor.toUpperCase() !== '#F0F0F0') {
|
|
335
|
+
gauge.borderColor = statusColor
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const { w, h } = this.absSize
|
|
339
|
+
const depth = this.effectiveDepth
|
|
340
|
+
const isWidthLonger = w >= h
|
|
341
|
+
|
|
342
|
+
// 게이지 캔버스 (고해상도)
|
|
343
|
+
const barW = 130
|
|
344
|
+
const barH = 26
|
|
345
|
+
const scale = 4
|
|
346
|
+
const canvasW = barW * scale
|
|
347
|
+
const canvasH = barH * scale
|
|
348
|
+
const canvas = document.createElement('canvas')
|
|
349
|
+
canvas.width = canvasW
|
|
350
|
+
canvas.height = canvasH
|
|
351
|
+
const ctx = canvas.getContext('2d')!
|
|
352
|
+
|
|
353
|
+
const pad = 12
|
|
354
|
+
|
|
355
|
+
// 배경 + 테두리
|
|
356
|
+
ctx.fillStyle = '#dddddd'
|
|
357
|
+
ctx.fillRect(0, 0, canvasW, canvasH)
|
|
358
|
+
ctx.strokeStyle = gauge.borderColor
|
|
359
|
+
ctx.lineWidth = 12
|
|
360
|
+
ctx.strokeRect(6, 6, canvasW - 12, canvasH - 12)
|
|
361
|
+
|
|
362
|
+
// 세그먼트 채움 (왼→오)
|
|
363
|
+
const innerW = canvasW - pad * 2
|
|
364
|
+
const innerH = canvasH - pad * 2
|
|
365
|
+
let x = pad
|
|
366
|
+
for (const seg of gauge.segments) {
|
|
367
|
+
const segW = Math.max(innerW * seg.ratio, seg.ratio > 0 ? 1 : 0)
|
|
368
|
+
if (segW <= 0) continue
|
|
369
|
+
ctx.fillStyle = seg.color
|
|
370
|
+
ctx.fillRect(x, pad, segW, innerH)
|
|
371
|
+
x += segW
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// 텍스트
|
|
375
|
+
ctx.font = `bold ${10 * scale}px sans-serif`
|
|
376
|
+
ctx.textAlign = 'center'
|
|
377
|
+
ctx.textBaseline = 'middle'
|
|
378
|
+
ctx.fillStyle = '#333333'
|
|
379
|
+
ctx.fillText(gauge.label, canvasW / 2, canvasH / 2)
|
|
380
|
+
|
|
381
|
+
// 텍스처 → Plane
|
|
382
|
+
const texture = new THREE.CanvasTexture(canvas)
|
|
383
|
+
texture.colorSpace = THREE.SRGBColorSpace
|
|
384
|
+
|
|
385
|
+
const geo = new THREE.PlaneGeometry(barW, barH)
|
|
386
|
+
geo.rotateX(-Math.PI / 2) // 수평 (지붕 위)
|
|
387
|
+
if (!isWidthLonger) geo.rotateY(Math.PI / 2)
|
|
388
|
+
|
|
389
|
+
const mat = new THREE.MeshBasicMaterial({
|
|
390
|
+
map: texture,
|
|
391
|
+
transparent: true,
|
|
392
|
+
side: THREE.DoubleSide,
|
|
393
|
+
})
|
|
394
|
+
mat.polygonOffset = true
|
|
395
|
+
mat.polygonOffsetFactor = -2
|
|
396
|
+
mat.polygonOffsetUnit = -2
|
|
397
|
+
|
|
398
|
+
this.gaugeMesh = new THREE.Mesh(geo, mat)
|
|
399
|
+
this.gaugeMesh.position.set(0, depth + 0.5, 0)
|
|
400
|
+
this.object3d.add(this.gaugeMesh)
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
private resolveGauge(data: any, mode: string) {
|
|
404
|
+
switch (mode) {
|
|
405
|
+
case 'carrier': {
|
|
406
|
+
const full = Number(data.FULL_CNT) || 0
|
|
407
|
+
const empty = Number(data.EMPTY_CNT) || 0
|
|
408
|
+
const emptyEmpty = Number(data.EMPTYEMPTY_CNT) || 0
|
|
409
|
+
const total = full + empty + emptyEmpty
|
|
410
|
+
const segments = total > 0
|
|
411
|
+
? [
|
|
412
|
+
{ ratio: full / total, color: '#4AA7FE' },
|
|
413
|
+
{ ratio: empty / total, color: '#8BDA5B' },
|
|
414
|
+
{ ratio: emptyEmpty / total, color: '#019D59' },
|
|
415
|
+
]
|
|
416
|
+
: [{ ratio: 1, color: '#dddddd' }]
|
|
417
|
+
return {
|
|
418
|
+
segments,
|
|
419
|
+
borderColor: '#4AA7FE',
|
|
420
|
+
label: total > 0 ? `F${full} E${empty} EE${emptyEmpty}` : '',
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
case 'abnormal': {
|
|
424
|
+
const normal = Number(data.CARRIER_NORMAL) || 0
|
|
425
|
+
const abnormal = Number(data.CARRIER_ABNORMAL) || 0
|
|
426
|
+
const total = normal + abnormal
|
|
427
|
+
const segments = total > 0
|
|
428
|
+
? [
|
|
429
|
+
{ ratio: normal / total, color: '#8BDA5B' },
|
|
430
|
+
{ ratio: abnormal / total, color: '#F4B4B4' },
|
|
431
|
+
]
|
|
432
|
+
: [{ ratio: 1, color: '#dddddd' }]
|
|
433
|
+
return {
|
|
434
|
+
segments,
|
|
435
|
+
borderColor: abnormal > 0 ? '#F4B4B4' : '#8BDA5B',
|
|
436
|
+
label: total > 0 ? `N${normal} A${abnormal}` : '',
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
default: { // capacity
|
|
440
|
+
const cur = Number(data.CURRENTCAPACITY) || 0
|
|
441
|
+
const max = Number(data.MAXCAPACITY) || 100
|
|
442
|
+
const fullup = data.FULLUP === 'T'
|
|
443
|
+
const usage = Math.round((cur / max) * 100)
|
|
444
|
+
const legend = (this.component as any).root?.style?.LEGEND_CAPACITY || LEGEND_CAPACITY
|
|
445
|
+
const color = fullup ? 'red' : (getVaueOnRanges(usage, legend) || '#44aa44')
|
|
446
|
+
return {
|
|
447
|
+
segments: [{ ratio: usage / 100, color }],
|
|
448
|
+
borderColor: color,
|
|
449
|
+
label: `${usage}%`,
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
onchange(after: Record<string, unknown>, before: Record<string, unknown>) {
|
|
456
|
+
if ('data' in after) {
|
|
457
|
+
this.clearGauge()
|
|
458
|
+
this.buildGauge()
|
|
459
|
+
}
|
|
460
|
+
super.onchange(after, before)
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
clear() {
|
|
464
|
+
this.clearGauge()
|
|
465
|
+
return super.clear()
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
private clearGauge() {
|
|
469
|
+
if (this.gaugeMesh) {
|
|
470
|
+
this.object3d.remove(this.gaugeMesh)
|
|
471
|
+
this.gaugeMesh.geometry?.dispose()
|
|
472
|
+
const mat = this.gaugeMesh.material as THREE.MeshBasicMaterial
|
|
473
|
+
mat?.map?.dispose()
|
|
474
|
+
mat?.dispose()
|
|
475
|
+
this.gaugeMesh = undefined
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
registerRealObjectFactory('STOCKER', (component: Component) => new StockerRealObject3D(component))
|
|
308
481
|
|
|
309
482
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
310
483
|
// Unit (이동체 · 홀더) — 낮은 depth, 불투명
|
|
@@ -339,23 +512,12 @@ register('Carrier', {
|
|
|
339
512
|
// Gauge (용량 게이지) — 얇은 바
|
|
340
513
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
341
514
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
roughness: 0.8,
|
|
346
|
-
defaultColor: 0x44aa44,
|
|
347
|
-
})
|
|
348
|
-
|
|
349
|
-
register('StockerCapacityBar', {
|
|
350
|
-
depthRatio: 0.15,
|
|
351
|
-
metalness: 0.2,
|
|
352
|
-
roughness: 0.8,
|
|
353
|
-
defaultColor: 0x44aa44,
|
|
354
|
-
})
|
|
515
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
516
|
+
// Gauge (용량 게이지) — 2D 렌더링을 Plane 텍스처로 3D 표시
|
|
517
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
355
518
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
})
|
|
519
|
+
// Gauge 컴포넌트 (StockerCapacityBar, ZoneCapacityBar, MCSGaugeCapacityBar)는
|
|
520
|
+
// 3D에서 표시하지 않음. 대신 부모(STOCKER 등)의 3D 모델에서 직접 게이지를 렌더링.
|
|
521
|
+
registerRealObjectFactory('StockerCapacityBar', () => null as any)
|
|
522
|
+
registerRealObjectFactory('MCSGaugeCapacityBar', () => null as any)
|
|
523
|
+
registerRealObjectFactory('ZoneCapacityBar', () => null as any)
|