@fmsim/machine 2.0.0-beta.95 → 2.0.0-beta.97
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/factories/machine-3d.js +14 -22
- package/dist/factories/machine-3d.js.map +1 -1
- package/dist/scene/root-container-override.js +55 -0
- package/dist/scene/root-container-override.js.map +1 -1
- package/dist/stocker-capacity-bar.js +145 -3
- package/dist/stocker-capacity-bar.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/utils/stocker-gauge.js +62 -0
- package/dist/utils/stocker-gauge.js.map +1 -0
- package/package.json +2 -2
- package/src/factories/machine-3d.ts +18 -21
- package/src/scene/root-container-override.ts +57 -0
- package/src/stocker-capacity-bar.ts +173 -4
- package/src/utils/stocker-gauge.ts +81 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* 스토커 게이지 세그먼트 계산 (2D/3D 공통 단일 소스).
|
|
3
|
+
*
|
|
4
|
+
* 2D(StockerCapacityBar) 와 3D(machine-3d buildGauge) 가 각자 세그먼트 비율/색상을
|
|
5
|
+
* 중복 계산하면 필드/색상 drift 가 발생한다(실제로 carrier 필드명이 어긋난 전례).
|
|
6
|
+
* 이 모듈이 carrier/abnormal 세그먼트 계산의 단일 소스이며 순수 함수라 단위 테스트가
|
|
7
|
+
* 용이하다. 표현(라벨 포맷·watermark·테두리색)은 렌더러별로 다르므로 여기서 다루지 않는다.
|
|
8
|
+
*
|
|
9
|
+
* canonical 데이터 필드(2D 기준):
|
|
10
|
+
* - carrier : CARRIER_FULL / CARRIER_EMPTY / CARRIER_EMPTY_EMPTY
|
|
11
|
+
* - abnormal : CARRIER_NORMAL / CARRIER_ABNORMAL
|
|
12
|
+
*/
|
|
13
|
+
/** carrier 세그먼트 색상 (2D/3D 공통). */
|
|
14
|
+
export const CARRIER_COLORS = {
|
|
15
|
+
full: '#4AA7FE',
|
|
16
|
+
empty: '#8BDA5B',
|
|
17
|
+
emptyEmpty: '#019D59'
|
|
18
|
+
};
|
|
19
|
+
/** abnormal 세그먼트 색상 (2D/3D 공통). */
|
|
20
|
+
export const ABNORMAL_COLORS = {
|
|
21
|
+
normal: '#8BDA5B',
|
|
22
|
+
abnormal: '#F4B4B4'
|
|
23
|
+
};
|
|
24
|
+
function num(v) {
|
|
25
|
+
const n = Number(v);
|
|
26
|
+
return Number.isFinite(n) && n > 0 ? n : 0;
|
|
27
|
+
}
|
|
28
|
+
/** 캐리어 상태 세그먼트 (Full / Empty / EmptyEmpty). total 0 이면 모든 ratio 0. */
|
|
29
|
+
export function carrierSegments(full, empty, emptyEmpty) {
|
|
30
|
+
const f = num(full);
|
|
31
|
+
const e = num(empty);
|
|
32
|
+
const ee = num(emptyEmpty);
|
|
33
|
+
const total = f + e + ee;
|
|
34
|
+
const r = (c) => (total > 0 ? c / total : 0);
|
|
35
|
+
return [
|
|
36
|
+
{ label: 'Full', count: f, ratio: r(f), color: CARRIER_COLORS.full },
|
|
37
|
+
{ label: 'Empty', count: e, ratio: r(e), color: CARRIER_COLORS.empty },
|
|
38
|
+
{ label: 'Empty\nEmpty', count: ee, ratio: r(ee), color: CARRIER_COLORS.emptyEmpty }
|
|
39
|
+
];
|
|
40
|
+
}
|
|
41
|
+
/** 캐리어 정상/비정상 세그먼트 (Normal / Abnormal). total 0 이면 모든 ratio 0. */
|
|
42
|
+
export function abnormalSegments(normal, abnormal) {
|
|
43
|
+
const n = num(normal);
|
|
44
|
+
const a = num(abnormal);
|
|
45
|
+
const total = n + a;
|
|
46
|
+
const r = (c) => (total > 0 ? c / total : 0);
|
|
47
|
+
return [
|
|
48
|
+
{ label: 'Normal', count: n, ratio: r(n), color: ABNORMAL_COLORS.normal },
|
|
49
|
+
{ label: 'Abnormal', count: a, ratio: r(a), color: ABNORMAL_COLORS.abnormal }
|
|
50
|
+
];
|
|
51
|
+
}
|
|
52
|
+
/** 캐리어 세그먼트 — canonical raw data(CARRIER_*) 에서 직접. 3D 처럼 raw data 소스용. */
|
|
53
|
+
export function carrierSegmentsFromData(data) {
|
|
54
|
+
const d = data && typeof data === 'object' ? data : {};
|
|
55
|
+
return carrierSegments(d.CARRIER_FULL, d.CARRIER_EMPTY, d.CARRIER_EMPTY_EMPTY);
|
|
56
|
+
}
|
|
57
|
+
/** 비정상 세그먼트 — canonical raw data(CARRIER_*) 에서 직접. */
|
|
58
|
+
export function abnormalSegmentsFromData(data) {
|
|
59
|
+
const d = data && typeof data === 'object' ? data : {};
|
|
60
|
+
return abnormalSegments(d.CARRIER_NORMAL, d.CARRIER_ABNORMAL);
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=stocker-gauge.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stocker-gauge.js","sourceRoot":"","sources":["../../src/utils/stocker-gauge.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAeH,kCAAkC;AAClC,MAAM,CAAC,MAAM,cAAc,GAAG;IAC5B,IAAI,EAAE,SAAS;IACf,KAAK,EAAE,SAAS;IAChB,UAAU,EAAE,SAAS;CACtB,CAAA;AAED,mCAAmC;AACnC,MAAM,CAAC,MAAM,eAAe,GAAG;IAC7B,MAAM,EAAE,SAAS;IACjB,QAAQ,EAAE,SAAS;CACpB,CAAA;AAED,SAAS,GAAG,CAAC,CAAU;IACrB,MAAM,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,CAAA;IACnB,OAAO,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;AAC5C,CAAC;AAED,sEAAsE;AACtE,MAAM,UAAU,eAAe,CAAC,IAAa,EAAE,KAAc,EAAE,UAAmB;IAChF,MAAM,CAAC,GAAG,GAAG,CAAC,IAAI,CAAC,CAAA;IACnB,MAAM,CAAC,GAAG,GAAG,CAAC,KAAK,CAAC,CAAA;IACpB,MAAM,EAAE,GAAG,GAAG,CAAC,UAAU,CAAC,CAAA;IAC1B,MAAM,KAAK,GAAG,CAAC,GAAG,CAAC,GAAG,EAAE,CAAA;IACxB,MAAM,CAAC,GAAG,CAAC,CAAS,EAAE,EAAE,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;IACpD,OAAO;QACL,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,cAAc,CAAC,IAAI,EAAE;QACpE,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,cAAc,CAAC,KAAK,EAAE;QACtE,EAAE,KAAK,EAAE,cAAc,EAAE,KAAK,EAAE,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,KAAK,EAAE,cAAc,CAAC,UAAU,EAAE;KACrF,CAAA;AACH,CAAC;AAED,kEAAkE;AAClE,MAAM,UAAU,gBAAgB,CAAC,MAAe,EAAE,QAAiB;IACjE,MAAM,CAAC,GAAG,GAAG,CAAC,MAAM,CAAC,CAAA;IACrB,MAAM,CAAC,GAAG,GAAG,CAAC,QAAQ,CAAC,CAAA;IACvB,MAAM,KAAK,GAAG,CAAC,GAAG,CAAC,CAAA;IACnB,MAAM,CAAC,GAAG,CAAC,CAAS,EAAE,EAAE,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;IACpD,OAAO;QACL,EAAE,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,eAAe,CAAC,MAAM,EAAE;QACzE,EAAE,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,eAAe,CAAC,QAAQ,EAAE;KAC9E,CAAA;AACH,CAAC;AAED,0EAA0E;AAC1E,MAAM,UAAU,uBAAuB,CAAC,IAAS;IAC/C,MAAM,CAAC,GAAG,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAA;IACtD,OAAO,eAAe,CAAC,CAAC,CAAC,YAAY,EAAE,CAAC,CAAC,aAAa,EAAE,CAAC,CAAC,mBAAmB,CAAC,CAAA;AAChF,CAAC;AAED,sDAAsD;AACtD,MAAM,UAAU,wBAAwB,CAAC,IAAS;IAChD,MAAM,CAAC,GAAG,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAA;IACtD,OAAO,gBAAgB,CAAC,CAAC,CAAC,cAAc,EAAE,CAAC,CAAC,gBAAgB,CAAC,CAAA;AAC/D,CAAC","sourcesContent":["/*\n * 스토커 게이지 세그먼트 계산 (2D/3D 공통 단일 소스).\n *\n * 2D(StockerCapacityBar) 와 3D(machine-3d buildGauge) 가 각자 세그먼트 비율/색상을\n * 중복 계산하면 필드/색상 drift 가 발생한다(실제로 carrier 필드명이 어긋난 전례).\n * 이 모듈이 carrier/abnormal 세그먼트 계산의 단일 소스이며 순수 함수라 단위 테스트가\n * 용이하다. 표현(라벨 포맷·watermark·테두리색)은 렌더러별로 다르므로 여기서 다루지 않는다.\n *\n * canonical 데이터 필드(2D 기준):\n * - carrier : CARRIER_FULL / CARRIER_EMPTY / CARRIER_EMPTY_EMPTY\n * - abnormal : CARRIER_NORMAL / CARRIER_ABNORMAL\n */\n\nexport type StockerGaugeMode = 'capacity' | 'carrier' | 'abnormal'\n\nexport interface GaugeSegment {\n /** 세그먼트 라벨(2D per-segment 표시용, '\\n' 은 줄바꿈). */\n label: string\n /** 카운트 원본값. */\n count: number\n /** 전체 대비 비율 (0~1). total 이 0 이면 0. */\n ratio: number\n /** 채움 색상. */\n color: string\n}\n\n/** carrier 세그먼트 색상 (2D/3D 공통). */\nexport const CARRIER_COLORS = {\n full: '#4AA7FE',\n empty: '#8BDA5B',\n emptyEmpty: '#019D59'\n}\n\n/** abnormal 세그먼트 색상 (2D/3D 공통). */\nexport const ABNORMAL_COLORS = {\n normal: '#8BDA5B',\n abnormal: '#F4B4B4'\n}\n\nfunction num(v: unknown): number {\n const n = Number(v)\n return Number.isFinite(n) && n > 0 ? n : 0\n}\n\n/** 캐리어 상태 세그먼트 (Full / Empty / EmptyEmpty). total 0 이면 모든 ratio 0. */\nexport function carrierSegments(full: unknown, empty: unknown, emptyEmpty: unknown): GaugeSegment[] {\n const f = num(full)\n const e = num(empty)\n const ee = num(emptyEmpty)\n const total = f + e + ee\n const r = (c: number) => (total > 0 ? c / total : 0)\n return [\n { label: 'Full', count: f, ratio: r(f), color: CARRIER_COLORS.full },\n { label: 'Empty', count: e, ratio: r(e), color: CARRIER_COLORS.empty },\n { label: 'Empty\\nEmpty', count: ee, ratio: r(ee), color: CARRIER_COLORS.emptyEmpty }\n ]\n}\n\n/** 캐리어 정상/비정상 세그먼트 (Normal / Abnormal). total 0 이면 모든 ratio 0. */\nexport function abnormalSegments(normal: unknown, abnormal: unknown): GaugeSegment[] {\n const n = num(normal)\n const a = num(abnormal)\n const total = n + a\n const r = (c: number) => (total > 0 ? c / total : 0)\n return [\n { label: 'Normal', count: n, ratio: r(n), color: ABNORMAL_COLORS.normal },\n { label: 'Abnormal', count: a, ratio: r(a), color: ABNORMAL_COLORS.abnormal }\n ]\n}\n\n/** 캐리어 세그먼트 — canonical raw data(CARRIER_*) 에서 직접. 3D 처럼 raw data 소스용. */\nexport function carrierSegmentsFromData(data: any): GaugeSegment[] {\n const d = data && typeof data === 'object' ? data : {}\n return carrierSegments(d.CARRIER_FULL, d.CARRIER_EMPTY, d.CARRIER_EMPTY_EMPTY)\n}\n\n/** 비정상 세그먼트 — canonical raw data(CARRIER_*) 에서 직접. */\nexport function abnormalSegmentsFromData(data: any): GaugeSegment[] {\n const d = data && typeof data === 'object' ? data : {}\n return abnormalSegments(d.CARRIER_NORMAL, d.CARRIER_ABNORMAL)\n}\n"]}
|
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.97",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"module": "dist/index.js",
|
|
8
8
|
"things-scene": true,
|
|
@@ -55,5 +55,5 @@
|
|
|
55
55
|
"prettier --write"
|
|
56
56
|
]
|
|
57
57
|
},
|
|
58
|
-
"gitHead": "
|
|
58
|
+
"gitHead": "f994681c569562e248ea20178a7749af6c4e050d"
|
|
59
59
|
}
|
|
@@ -15,6 +15,12 @@ import type { Component } from '@hatiolab/things-scene'
|
|
|
15
15
|
import { LEGEND_CAPACITY } from '../features/mcs-status-default'
|
|
16
16
|
import { getValueOnRanges } from '../utils/get-value-on-ranges'
|
|
17
17
|
import { fitGaugeScale } from '../utils/fit-gauge-scale'
|
|
18
|
+
import {
|
|
19
|
+
carrierSegmentsFromData,
|
|
20
|
+
abnormalSegmentsFromData,
|
|
21
|
+
CARRIER_COLORS,
|
|
22
|
+
ABNORMAL_COLORS
|
|
23
|
+
} from '../utils/stocker-gauge'
|
|
18
24
|
import { Z_FMSIM } from './z-priority'
|
|
19
25
|
|
|
20
26
|
// ── 설정 타입 ────────────────────────────────────────────
|
|
@@ -654,7 +660,7 @@ class StockerRealObject3D extends MCSRealObject3D {
|
|
|
654
660
|
private buildGauge() {
|
|
655
661
|
const comp = this.component as any
|
|
656
662
|
const data = comp.data || {}
|
|
657
|
-
const gaugeMode = comp.root?.
|
|
663
|
+
const gaugeMode = comp.root?.stockerGaugeMode || 'capacity'
|
|
658
664
|
|
|
659
665
|
const gauge = this.resolveGauge(data, gaugeMode)
|
|
660
666
|
|
|
@@ -741,37 +747,28 @@ class StockerRealObject3D extends MCSRealObject3D {
|
|
|
741
747
|
private resolveGauge(data: any, mode: string) {
|
|
742
748
|
switch (mode) {
|
|
743
749
|
case 'carrier': {
|
|
744
|
-
|
|
745
|
-
const
|
|
746
|
-
const
|
|
747
|
-
const total = full + empty + emptyEmpty
|
|
750
|
+
// 세그먼트 비율/색상은 2D 와 공유되는 단일 소스에서 계산 (필드/색상 drift 방지)
|
|
751
|
+
const [f, e, ee] = carrierSegmentsFromData(data)
|
|
752
|
+
const total = f.count + e.count + ee.count
|
|
748
753
|
const segments = total > 0
|
|
749
|
-
? [
|
|
750
|
-
{ ratio: full / total, color: '#4AA7FE' },
|
|
751
|
-
{ ratio: empty / total, color: '#8BDA5B' },
|
|
752
|
-
{ ratio: emptyEmpty / total, color: '#019D59' },
|
|
753
|
-
]
|
|
754
|
+
? [f, e, ee].map(s => ({ ratio: s.ratio, color: s.color }))
|
|
754
755
|
: [{ ratio: 1, color: '#dddddd' }]
|
|
755
756
|
return {
|
|
756
757
|
segments,
|
|
757
|
-
borderColor:
|
|
758
|
-
label: total > 0 ? `F${
|
|
758
|
+
borderColor: CARRIER_COLORS.full,
|
|
759
|
+
label: total > 0 ? `F${f.count} E${e.count} EE${ee.count}` : '',
|
|
759
760
|
}
|
|
760
761
|
}
|
|
761
762
|
case 'abnormal': {
|
|
762
|
-
const
|
|
763
|
-
const
|
|
764
|
-
const total = normal + abnormal
|
|
763
|
+
const [n, a] = abnormalSegmentsFromData(data)
|
|
764
|
+
const total = n.count + a.count
|
|
765
765
|
const segments = total > 0
|
|
766
|
-
? [
|
|
767
|
-
{ ratio: normal / total, color: '#8BDA5B' },
|
|
768
|
-
{ ratio: abnormal / total, color: '#F4B4B4' },
|
|
769
|
-
]
|
|
766
|
+
? [n, a].map(s => ({ ratio: s.ratio, color: s.color }))
|
|
770
767
|
: [{ ratio: 1, color: '#dddddd' }]
|
|
771
768
|
return {
|
|
772
769
|
segments,
|
|
773
|
-
borderColor:
|
|
774
|
-
label: total > 0 ? `N${
|
|
770
|
+
borderColor: a.count > 0 ? ABNORMAL_COLORS.abnormal : ABNORMAL_COLORS.normal,
|
|
771
|
+
label: total > 0 ? `N${n.count} A${a.count}` : '',
|
|
775
772
|
}
|
|
776
773
|
}
|
|
777
774
|
default: { // capacity
|
|
@@ -25,6 +25,10 @@ declare module '@hatiolab/things-scene' {
|
|
|
25
25
|
equipmentLegendTheme?: any
|
|
26
26
|
style: any
|
|
27
27
|
vehicleAnimation?: boolean
|
|
28
|
+
// 스토커 게이지 표시 모드 — 런타임 전용 transient (모델 비저장, 항상 'capacity' 시작)
|
|
29
|
+
stockerGaugeMode: string
|
|
30
|
+
_stockerGaugeMode?: string
|
|
31
|
+
_refreshStockerGauges(): void
|
|
28
32
|
// 캐시 관련 속성 추가
|
|
29
33
|
_animationConfigCache?: Map<string, any>
|
|
30
34
|
_carrierManager?: CarrierManager
|
|
@@ -32,6 +36,17 @@ declare module '@hatiolab/things-scene' {
|
|
|
32
36
|
}
|
|
33
37
|
}
|
|
34
38
|
|
|
39
|
+
/**
|
|
40
|
+
* 스토커 적재율 게이지 표시 모드 — 런타임 선택지.
|
|
41
|
+
* value 는 엔진 canonical(2D StockerCapacityBar / 3D buildGauge 공통), label 은 표시명.
|
|
42
|
+
* 런타임 UI(뷰어 컨트롤)가 이 목록으로 selector 를 구성한다.
|
|
43
|
+
*/
|
|
44
|
+
export const STOCKER_GAUGE_MODES: { value: string; label: string }[] = [
|
|
45
|
+
{ value: 'capacity', label: 'Capacity' },
|
|
46
|
+
{ value: 'carrier', label: 'Carrier Status' },
|
|
47
|
+
{ value: 'abnormal', label: 'Abnormal Carrier' }
|
|
48
|
+
]
|
|
49
|
+
|
|
35
50
|
const ModelLayer = (Component as any).register('model-layer') as any
|
|
36
51
|
|
|
37
52
|
Object.defineProperty(ModelLayer.prototype, 'nature', {
|
|
@@ -318,3 +333,45 @@ Object.defineProperty(RootContainer.prototype, 'carrierManager', {
|
|
|
318
333
|
return this._carrierManager
|
|
319
334
|
}
|
|
320
335
|
})
|
|
336
|
+
|
|
337
|
+
// === 스토커 게이지 표시 모드 (런타임 전용 transient) ===
|
|
338
|
+
//
|
|
339
|
+
// 모델(state)에 저장하지 않는다 — nature 속성이 아니므로 보드 직렬화에 포함되지 않고,
|
|
340
|
+
// 보드를 다시 열면 항상 기본값 'capacity' 로 시작한다. 실행 중 뷰어 컨트롤 등이
|
|
341
|
+
// `scene.root.stockerGaugeMode = 'carrier'` 로 동적 변경한다.
|
|
342
|
+
//
|
|
343
|
+
// 2D(StockerCapacityBar.gaugeMode) 와 3D(machine-3d buildGauge) 가 모두 이 값을 읽어
|
|
344
|
+
// 게이지 표현이 항상 일치한다. 값이 바뀌면 setter 가 2D 리드로우 + 3D 게이지 재빌드를
|
|
345
|
+
// 즉시 트리거한다.
|
|
346
|
+
Object.defineProperty(RootContainer.prototype, 'stockerGaugeMode', {
|
|
347
|
+
get: function () {
|
|
348
|
+
return this._stockerGaugeMode || 'capacity'
|
|
349
|
+
},
|
|
350
|
+
set: function (mode: string) {
|
|
351
|
+
const next = mode || 'capacity'
|
|
352
|
+
if (this._stockerGaugeMode === next) return
|
|
353
|
+
this._stockerGaugeMode = next
|
|
354
|
+
this._refreshStockerGauges()
|
|
355
|
+
}
|
|
356
|
+
})
|
|
357
|
+
|
|
358
|
+
/** scene 하위 STOCKER(3D 게이지) + StockerCapacityBar(2D 게이지) 를 새 모드로 갱신. */
|
|
359
|
+
RootContainer.prototype._refreshStockerGauges = function () {
|
|
360
|
+
const walk = (container: any) => {
|
|
361
|
+
const kids = container?.components
|
|
362
|
+
if (!Array.isArray(kids)) return
|
|
363
|
+
for (const c of kids) {
|
|
364
|
+
const type = c?.state?.type
|
|
365
|
+
if (type === 'STOCKER') {
|
|
366
|
+
// 3D 게이지는 스토커 자신의 real-object 가 그린다 → 재빌드 (2D 모드면 _realObject 없음)
|
|
367
|
+
c._realObject?.update?.()
|
|
368
|
+
} else if (type === 'StockerCapacityBar' || type === 'MCSGaugeCapacityBar') {
|
|
369
|
+
// 2D 게이지는 자식 컴포넌트가 그린다 → 리드로우
|
|
370
|
+
c.invalidate?.()
|
|
371
|
+
}
|
|
372
|
+
walk(c)
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
walk(this)
|
|
376
|
+
this.invalidate?.()
|
|
377
|
+
}
|
|
@@ -13,6 +13,26 @@ import { LEGEND_CAPACITY, Legend } from './features/mcs-status-default'
|
|
|
13
13
|
import { safeRound } from './utils/safe-round'
|
|
14
14
|
import { getValueOnRanges } from './utils/get-value-on-ranges'
|
|
15
15
|
import { ParentObjectMixin, ParentObjectMixinProperties } from './features/parent-object-mixin'
|
|
16
|
+
import { carrierSegments, abnormalSegments, GaugeSegment } from './utils/stocker-gauge'
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* 스토커 게이지 (통합).
|
|
20
|
+
*
|
|
21
|
+
* 하나의 컴포넌트가 mode 에 따라 3가지 게이지를 표현한다:
|
|
22
|
+
* - 'capacity' : 용량 사용률(usage%) 채움 게이지 + high/low watermark (기본)
|
|
23
|
+
* - 'carrier' : 캐리어 상태비율 세그먼트 (Full / Empty / EmptyEmpty)
|
|
24
|
+
* - 'abnormal' : 캐리어 정상/비정상 비율 세그먼트 (Normal / Abnormal)
|
|
25
|
+
*
|
|
26
|
+
* mode 소스: root.stockerGaugeMode (기본 'capacity').
|
|
27
|
+
* RootContainer 의 **런타임 전용 transient 속성** — 모델에 저장하지 않고 항상 'capacity'
|
|
28
|
+
* 로 시작하며 실행 중 동적으로만 변경된다(root-container-override.ts 정의). 3D 표현
|
|
29
|
+
* (machine-3d.ts buildGauge)과 동일 소스라 2D/3D 게이지가 항상 일치한다.
|
|
30
|
+
*
|
|
31
|
+
* 데이터 필드(2D 기준, canonical):
|
|
32
|
+
* - capacity : CURRENTCAPACITY / MAXCAPACITY / HIGHWATERMARK / LOWWATERMARK / FULLUP
|
|
33
|
+
* - carrier : CARRIER_FULL / CARRIER_EMPTY / CARRIER_EMPTY_EMPTY
|
|
34
|
+
* - abnormal : CARRIER_NORMAL / CARRIER_ABNORMAL
|
|
35
|
+
*/
|
|
16
36
|
|
|
17
37
|
const NATURE: ComponentNature = {
|
|
18
38
|
mutable: false,
|
|
@@ -53,6 +73,31 @@ const NATURE: ComponentNature = {
|
|
|
53
73
|
name: 'fullup',
|
|
54
74
|
label: 'fullup'
|
|
55
75
|
},
|
|
76
|
+
{
|
|
77
|
+
type: 'number',
|
|
78
|
+
name: 'carrierFull',
|
|
79
|
+
label: 'carrier-full'
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
type: 'number',
|
|
83
|
+
name: 'carrierEmpty',
|
|
84
|
+
label: 'carrier-empty'
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
type: 'number',
|
|
88
|
+
name: 'carrierEmptyEmpty',
|
|
89
|
+
label: 'carrier-empty-empty'
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
type: 'number',
|
|
93
|
+
name: 'carrierNormal',
|
|
94
|
+
label: 'carrier-normal'
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
type: 'number',
|
|
98
|
+
name: 'carrierAbnormal',
|
|
99
|
+
label: 'carrier-abnormal'
|
|
100
|
+
},
|
|
56
101
|
{
|
|
57
102
|
type: 'number',
|
|
58
103
|
label: 'round',
|
|
@@ -97,6 +142,11 @@ export default class StockerCapacityBar extends ParentObjectMixin(MCSStatusMixin
|
|
|
97
142
|
return ['STOCKER']
|
|
98
143
|
}
|
|
99
144
|
|
|
145
|
+
/** 게이지 모드: RootContainer 런타임 transient 속성. 3D(machine-3d) 와 동일 소스. */
|
|
146
|
+
get gaugeMode(): string {
|
|
147
|
+
return (this.root as any)?.stockerGaugeMode || 'capacity'
|
|
148
|
+
}
|
|
149
|
+
|
|
100
150
|
get controls() {
|
|
101
151
|
var { left, top, width, round, height } = this.state
|
|
102
152
|
round = round == undefined ? 0 : safeRound(round, width, height)
|
|
@@ -140,8 +190,15 @@ export default class StockerCapacityBar extends ParentObjectMixin(MCSStatusMixin
|
|
|
140
190
|
}
|
|
141
191
|
|
|
142
192
|
render(context: CanvasRenderingContext2D) {
|
|
143
|
-
const
|
|
193
|
+
const mode = this.gaugeMode
|
|
144
194
|
|
|
195
|
+
if (mode === 'carrier' || mode === 'abnormal') {
|
|
196
|
+
this.renderSegmentGauge(context, this.segments(mode))
|
|
197
|
+
return
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// capacity (기본)
|
|
201
|
+
const { width, height } = this.bounds
|
|
145
202
|
if (width >= height) {
|
|
146
203
|
this.renderLandscape(context)
|
|
147
204
|
} else {
|
|
@@ -149,6 +206,107 @@ export default class StockerCapacityBar extends ParentObjectMixin(MCSStatusMixin
|
|
|
149
206
|
}
|
|
150
207
|
}
|
|
151
208
|
|
|
209
|
+
/**
|
|
210
|
+
* carrier/abnormal mode 의 세그먼트 목록 (공유 유틸 = 3D 와 동일 계산).
|
|
211
|
+
* 배열 순서 = portrait 아래→위 / landscape 왼→오.
|
|
212
|
+
*/
|
|
213
|
+
segments(mode: string): GaugeSegment[] {
|
|
214
|
+
if (mode === 'abnormal') {
|
|
215
|
+
const { carrierNormal = 0, carrierAbnormal = 0 } = this.state
|
|
216
|
+
return abnormalSegments(carrierNormal, carrierAbnormal)
|
|
217
|
+
}
|
|
218
|
+
const { carrierFull = 0, carrierEmpty = 0, carrierEmptyEmpty = 0 } = this.state
|
|
219
|
+
return carrierSegments(carrierFull, carrierEmpty, carrierEmptyEmpty)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** 세그먼트 게이지 렌더 (carrier/abnormal 공통). bounds 방향으로 landscape/portrait 자동. */
|
|
223
|
+
renderSegmentGauge(context: CanvasRenderingContext2D, segments: GaugeSegment[]) {
|
|
224
|
+
const { left, top, width, height } = this.bounds
|
|
225
|
+
const { lineWidth, strokeStyle, round, fillStyle = 'white' } = this.state
|
|
226
|
+
const landscape = width >= height
|
|
227
|
+
|
|
228
|
+
context.save()
|
|
229
|
+
context.translate(left, top)
|
|
230
|
+
|
|
231
|
+
// background
|
|
232
|
+
context.beginPath()
|
|
233
|
+
context.fillStyle = fillStyle
|
|
234
|
+
context.roundRect(0, 0, width, height, round)
|
|
235
|
+
context.fill()
|
|
236
|
+
context.clip()
|
|
237
|
+
|
|
238
|
+
if (landscape) {
|
|
239
|
+
// 왼→오
|
|
240
|
+
let x = 0
|
|
241
|
+
for (const seg of segments) {
|
|
242
|
+
const segWidth = width * seg.ratio
|
|
243
|
+
if (segWidth <= 0) continue
|
|
244
|
+
|
|
245
|
+
context.beginPath()
|
|
246
|
+
context.fillStyle = seg.color
|
|
247
|
+
context.rect(x, 0, segWidth, height)
|
|
248
|
+
context.fill()
|
|
249
|
+
|
|
250
|
+
this._drawSegmentLabel(context, seg, x, 0, segWidth, height)
|
|
251
|
+
x += segWidth
|
|
252
|
+
}
|
|
253
|
+
} else {
|
|
254
|
+
// 아래→위 (배열 첫 세그먼트가 바닥)
|
|
255
|
+
let y = height
|
|
256
|
+
for (const seg of segments) {
|
|
257
|
+
const segHeight = height * seg.ratio
|
|
258
|
+
if (segHeight <= 0) continue
|
|
259
|
+
|
|
260
|
+
y -= segHeight
|
|
261
|
+
|
|
262
|
+
context.beginPath()
|
|
263
|
+
context.fillStyle = seg.color
|
|
264
|
+
context.rect(0, y, width, segHeight)
|
|
265
|
+
context.fill()
|
|
266
|
+
|
|
267
|
+
this._drawSegmentLabel(context, seg, 0, y, width, segHeight)
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// outline
|
|
272
|
+
context.beginPath()
|
|
273
|
+
context.setLineDash([])
|
|
274
|
+
context.strokeStyle = strokeStyle
|
|
275
|
+
context.lineWidth = lineWidth
|
|
276
|
+
context.roundRect(0, 0, width, height, round)
|
|
277
|
+
context.stroke()
|
|
278
|
+
|
|
279
|
+
context.translate(-left, -top)
|
|
280
|
+
context.restore()
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
private _drawSegmentLabel(
|
|
284
|
+
context: CanvasRenderingContext2D,
|
|
285
|
+
seg: GaugeSegment,
|
|
286
|
+
x: number,
|
|
287
|
+
y: number,
|
|
288
|
+
w: number,
|
|
289
|
+
h: number
|
|
290
|
+
) {
|
|
291
|
+
const percent = Math.round(seg.ratio * 100)
|
|
292
|
+
const lines = seg.label.split('\n')
|
|
293
|
+
lines.push(`${percent}%`)
|
|
294
|
+
|
|
295
|
+
const fontSize = Math.max(9, Math.min(14, Math.min(w, h) * 0.22))
|
|
296
|
+
context.font = `bold ${fontSize}px sans-serif`
|
|
297
|
+
context.fillStyle = '#333'
|
|
298
|
+
context.textAlign = 'center'
|
|
299
|
+
context.textBaseline = 'middle'
|
|
300
|
+
|
|
301
|
+
const lineHeight = fontSize * 1.2
|
|
302
|
+
const totalHeight = lines.length * lineHeight
|
|
303
|
+
const startY = y + (h - totalHeight) / 2 + lineHeight / 2
|
|
304
|
+
|
|
305
|
+
for (let i = 0; i < lines.length; i++) {
|
|
306
|
+
context.fillText(lines[i], x + w / 2, startY + i * lineHeight)
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
152
310
|
renderLandscape(context: CanvasRenderingContext2D) {
|
|
153
311
|
const { left, top, width, height } = this.bounds
|
|
154
312
|
const { highWatermark, lowWatermark, lineWidth, strokeStyle, round, fillStyle = 'white' } = this.state
|
|
@@ -303,13 +461,19 @@ export default class StockerCapacityBar extends ParentObjectMixin(MCSStatusMixin
|
|
|
303
461
|
}
|
|
304
462
|
|
|
305
463
|
onchangeData(after: Record<string, any>, before: Record<string, any>): void {
|
|
464
|
+
const data = this.data && typeof this.data == 'object' ? this.data : {}
|
|
306
465
|
const {
|
|
307
466
|
CURRENTCAPACITY = 0,
|
|
308
467
|
MAXCAPACITY = 100,
|
|
309
468
|
HIGHWATERMARK = 100,
|
|
310
469
|
LOWWATERMARK = 0,
|
|
311
|
-
FULLUP = 'F'
|
|
312
|
-
|
|
470
|
+
FULLUP = 'F',
|
|
471
|
+
CARRIER_FULL = 0,
|
|
472
|
+
CARRIER_EMPTY = 0,
|
|
473
|
+
CARRIER_EMPTY_EMPTY = 0,
|
|
474
|
+
CARRIER_NORMAL = 0,
|
|
475
|
+
CARRIER_ABNORMAL = 0
|
|
476
|
+
} = data
|
|
313
477
|
|
|
314
478
|
const currentUsage = Number(CURRENTCAPACITY) || 0
|
|
315
479
|
const maxCapacity = Number(MAXCAPACITY) || 100
|
|
@@ -322,7 +486,12 @@ export default class StockerCapacityBar extends ParentObjectMixin(MCSStatusMixin
|
|
|
322
486
|
highWatermark,
|
|
323
487
|
lowWatermark,
|
|
324
488
|
fullup: FULLUP == 'T',
|
|
325
|
-
usage: Math.round((currentUsage / maxCapacity) * 100)
|
|
489
|
+
usage: Math.round((currentUsage / maxCapacity) * 100),
|
|
490
|
+
carrierFull: Number(CARRIER_FULL) || 0,
|
|
491
|
+
carrierEmpty: Number(CARRIER_EMPTY) || 0,
|
|
492
|
+
carrierEmptyEmpty: Number(CARRIER_EMPTY_EMPTY) || 0,
|
|
493
|
+
carrierNormal: Number(CARRIER_NORMAL) || 0,
|
|
494
|
+
carrierAbnormal: Number(CARRIER_ABNORMAL) || 0
|
|
326
495
|
})
|
|
327
496
|
}
|
|
328
497
|
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* 스토커 게이지 세그먼트 계산 (2D/3D 공통 단일 소스).
|
|
3
|
+
*
|
|
4
|
+
* 2D(StockerCapacityBar) 와 3D(machine-3d buildGauge) 가 각자 세그먼트 비율/색상을
|
|
5
|
+
* 중복 계산하면 필드/색상 drift 가 발생한다(실제로 carrier 필드명이 어긋난 전례).
|
|
6
|
+
* 이 모듈이 carrier/abnormal 세그먼트 계산의 단일 소스이며 순수 함수라 단위 테스트가
|
|
7
|
+
* 용이하다. 표현(라벨 포맷·watermark·테두리색)은 렌더러별로 다르므로 여기서 다루지 않는다.
|
|
8
|
+
*
|
|
9
|
+
* canonical 데이터 필드(2D 기준):
|
|
10
|
+
* - carrier : CARRIER_FULL / CARRIER_EMPTY / CARRIER_EMPTY_EMPTY
|
|
11
|
+
* - abnormal : CARRIER_NORMAL / CARRIER_ABNORMAL
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export type StockerGaugeMode = 'capacity' | 'carrier' | 'abnormal'
|
|
15
|
+
|
|
16
|
+
export interface GaugeSegment {
|
|
17
|
+
/** 세그먼트 라벨(2D per-segment 표시용, '\n' 은 줄바꿈). */
|
|
18
|
+
label: string
|
|
19
|
+
/** 카운트 원본값. */
|
|
20
|
+
count: number
|
|
21
|
+
/** 전체 대비 비율 (0~1). total 이 0 이면 0. */
|
|
22
|
+
ratio: number
|
|
23
|
+
/** 채움 색상. */
|
|
24
|
+
color: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** carrier 세그먼트 색상 (2D/3D 공통). */
|
|
28
|
+
export const CARRIER_COLORS = {
|
|
29
|
+
full: '#4AA7FE',
|
|
30
|
+
empty: '#8BDA5B',
|
|
31
|
+
emptyEmpty: '#019D59'
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** abnormal 세그먼트 색상 (2D/3D 공통). */
|
|
35
|
+
export const ABNORMAL_COLORS = {
|
|
36
|
+
normal: '#8BDA5B',
|
|
37
|
+
abnormal: '#F4B4B4'
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function num(v: unknown): number {
|
|
41
|
+
const n = Number(v)
|
|
42
|
+
return Number.isFinite(n) && n > 0 ? n : 0
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** 캐리어 상태 세그먼트 (Full / Empty / EmptyEmpty). total 0 이면 모든 ratio 0. */
|
|
46
|
+
export function carrierSegments(full: unknown, empty: unknown, emptyEmpty: unknown): GaugeSegment[] {
|
|
47
|
+
const f = num(full)
|
|
48
|
+
const e = num(empty)
|
|
49
|
+
const ee = num(emptyEmpty)
|
|
50
|
+
const total = f + e + ee
|
|
51
|
+
const r = (c: number) => (total > 0 ? c / total : 0)
|
|
52
|
+
return [
|
|
53
|
+
{ label: 'Full', count: f, ratio: r(f), color: CARRIER_COLORS.full },
|
|
54
|
+
{ label: 'Empty', count: e, ratio: r(e), color: CARRIER_COLORS.empty },
|
|
55
|
+
{ label: 'Empty\nEmpty', count: ee, ratio: r(ee), color: CARRIER_COLORS.emptyEmpty }
|
|
56
|
+
]
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** 캐리어 정상/비정상 세그먼트 (Normal / Abnormal). total 0 이면 모든 ratio 0. */
|
|
60
|
+
export function abnormalSegments(normal: unknown, abnormal: unknown): GaugeSegment[] {
|
|
61
|
+
const n = num(normal)
|
|
62
|
+
const a = num(abnormal)
|
|
63
|
+
const total = n + a
|
|
64
|
+
const r = (c: number) => (total > 0 ? c / total : 0)
|
|
65
|
+
return [
|
|
66
|
+
{ label: 'Normal', count: n, ratio: r(n), color: ABNORMAL_COLORS.normal },
|
|
67
|
+
{ label: 'Abnormal', count: a, ratio: r(a), color: ABNORMAL_COLORS.abnormal }
|
|
68
|
+
]
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** 캐리어 세그먼트 — canonical raw data(CARRIER_*) 에서 직접. 3D 처럼 raw data 소스용. */
|
|
72
|
+
export function carrierSegmentsFromData(data: any): GaugeSegment[] {
|
|
73
|
+
const d = data && typeof data === 'object' ? data : {}
|
|
74
|
+
return carrierSegments(d.CARRIER_FULL, d.CARRIER_EMPTY, d.CARRIER_EMPTY_EMPTY)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** 비정상 세그먼트 — canonical raw data(CARRIER_*) 에서 직접. */
|
|
78
|
+
export function abnormalSegmentsFromData(data: any): GaugeSegment[] {
|
|
79
|
+
const d = data && typeof data === 'object' ? data : {}
|
|
80
|
+
return abnormalSegments(d.CARRIER_NORMAL, d.CARRIER_ABNORMAL)
|
|
81
|
+
}
|