@fmsim/machine 2.0.0-beta.104 → 2.0.0-beta.108
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/features/capacity-gauge-mixin.js +514 -0
- package/dist/features/capacity-gauge-mixin.js.map +1 -0
- package/dist/scene/root-container-override.js +3 -2
- package/dist/scene/root-container-override.js.map +1 -1
- package/dist/stocker-capacity-bar.js +12 -453
- package/dist/stocker-capacity-bar.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/utils/stocker-gauge.js +27 -5
- package/dist/utils/stocker-gauge.js.map +1 -1
- package/dist/zone-capacity-bar.js +13 -213
- package/dist/zone-capacity-bar.js.map +1 -1
- package/package.json +2 -2
- package/src/features/capacity-gauge-mixin.ts +628 -0
- package/src/scene/root-container-override.ts +3 -2
- package/src/stocker-capacity-bar.ts +16 -573
- package/src/utils/stocker-gauge.ts +28 -5
- package/src/zone-capacity-bar.ts +17 -282
|
@@ -1,138 +1,32 @@
|
|
|
1
|
-
import {
|
|
2
|
-
Component,
|
|
3
|
-
ComponentNature,
|
|
4
|
-
POSITION,
|
|
5
|
-
Properties,
|
|
6
|
-
RectPath,
|
|
7
|
-
Shape,
|
|
8
|
-
sceneComponent
|
|
9
|
-
} from '@hatiolab/things-scene'
|
|
10
|
-
import { themesColorRange } from '@fmsim/api'
|
|
1
|
+
import { ComponentNature, RectPath, Shape, sceneComponent } from '@hatiolab/things-scene'
|
|
11
2
|
import { MCSStatusMixin } from './features/mcs-status-mixin'
|
|
12
|
-
import { LEGEND_CAPACITY, Legend
|
|
13
|
-
import { safeRound } from './utils/safe-round'
|
|
14
|
-
import { getValueOnRanges } from './utils/get-value-on-ranges'
|
|
3
|
+
import { LEGEND_CAPACITY, Legend } from './features/mcs-status-default'
|
|
15
4
|
import { ParentObjectMixin, ParentObjectMixinProperties } from './features/parent-object-mixin'
|
|
16
|
-
import {
|
|
5
|
+
import { CapacityGaugeMixin, CapacityGaugeMixinProperties } from './features/capacity-gauge-mixin'
|
|
17
6
|
|
|
18
7
|
/**
|
|
19
8
|
* 스토커 게이지 (통합).
|
|
20
9
|
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* - 'abnormal' : 캐리어 정상/비정상 비율 세그먼트 (Normal / Abnormal)
|
|
10
|
+
* mode 에 따라 capacity / carrier / abnormal 3가지 게이지를 표현한다. 그리는 규칙은
|
|
11
|
+
* CapacityGaugeMixin 에 있으며 ZoneCapacityBar 와 완전히 공유한다(단일 소스). 스토커는
|
|
12
|
+
* STOCKER 내부에 놓이는 Shape 로, 부모 STOCKER 가 데이터를 push 한다(isIdentifiable=false).
|
|
25
13
|
*
|
|
26
|
-
* mode 소스: root.stockerGaugeMode (
|
|
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
|
|
14
|
+
* mode 소스: root.stockerGaugeMode. 3D(machine-3d buildGauge)와 동일 소스라 2D/3D 가 일치한다.
|
|
35
15
|
*/
|
|
36
16
|
|
|
37
17
|
const NATURE: ComponentNature = {
|
|
38
18
|
mutable: false,
|
|
39
19
|
resizable: true,
|
|
40
20
|
rotatable: true,
|
|
41
|
-
properties: [
|
|
42
|
-
...ParentObjectMixinProperties,
|
|
43
|
-
{
|
|
44
|
-
type: 'select',
|
|
45
|
-
label: 'legend-name',
|
|
46
|
-
name: 'legendName',
|
|
47
|
-
property: {
|
|
48
|
-
options: themesColorRange
|
|
49
|
-
}
|
|
50
|
-
},
|
|
51
|
-
{
|
|
52
|
-
type: 'number',
|
|
53
|
-
name: 'currentUsage',
|
|
54
|
-
label: 'current-usage'
|
|
55
|
-
},
|
|
56
|
-
{
|
|
57
|
-
type: 'number',
|
|
58
|
-
name: 'highWatermark',
|
|
59
|
-
label: 'high-watermark'
|
|
60
|
-
},
|
|
61
|
-
{
|
|
62
|
-
type: 'number',
|
|
63
|
-
name: 'lowWatermark',
|
|
64
|
-
label: 'low-watermark'
|
|
65
|
-
},
|
|
66
|
-
{
|
|
67
|
-
type: 'number',
|
|
68
|
-
name: 'maxCapacity',
|
|
69
|
-
label: 'max-capacity'
|
|
70
|
-
},
|
|
71
|
-
{
|
|
72
|
-
type: 'boolean',
|
|
73
|
-
name: 'fullup',
|
|
74
|
-
label: 'fullup'
|
|
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
|
-
},
|
|
101
|
-
{
|
|
102
|
-
type: 'number',
|
|
103
|
-
label: 'round',
|
|
104
|
-
name: 'round',
|
|
105
|
-
property: {
|
|
106
|
-
min: 0
|
|
107
|
-
}
|
|
108
|
-
},
|
|
109
|
-
{
|
|
110
|
-
type: 'hidden',
|
|
111
|
-
name: 'usage',
|
|
112
|
-
label: 'usage'
|
|
113
|
-
}
|
|
114
|
-
],
|
|
21
|
+
properties: [...ParentObjectMixinProperties, ...CapacityGaugeMixinProperties],
|
|
115
22
|
'value-property': 'usage'
|
|
116
23
|
}
|
|
117
24
|
|
|
118
|
-
var controlHandler = {
|
|
119
|
-
ondragmove: function (point: POSITION, index: number, component: Component) {
|
|
120
|
-
var { left, top, width, height } = component.model
|
|
121
|
-
|
|
122
|
-
var transcoorded = component.transcoordP2S(point.x, point.y)
|
|
123
|
-
var round = ((transcoorded.x - left) / (width / 2)) * 100
|
|
124
|
-
|
|
125
|
-
round = safeRound(round, width, height)
|
|
126
|
-
|
|
127
|
-
component.set({
|
|
128
|
-
round
|
|
129
|
-
})
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
25
|
@sceneComponent('StockerCapacityBar')
|
|
134
26
|
@sceneComponent('MCSGaugeCapacityBar')
|
|
135
|
-
export default class StockerCapacityBar extends
|
|
27
|
+
export default class StockerCapacityBar extends CapacityGaugeMixin(
|
|
28
|
+
ParentObjectMixin(MCSStatusMixin(RectPath(Shape)))
|
|
29
|
+
) {
|
|
136
30
|
static get nature() {
|
|
137
31
|
return NATURE
|
|
138
32
|
}
|
|
@@ -142,473 +36,22 @@ export default class StockerCapacityBar extends ParentObjectMixin(MCSStatusMixin
|
|
|
142
36
|
return ['STOCKER']
|
|
143
37
|
}
|
|
144
38
|
|
|
145
|
-
/**
|
|
146
|
-
get gaugeMode(): string {
|
|
147
|
-
return (this.root as any)?.stockerGaugeMode || 'capacity'
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
get controls() {
|
|
151
|
-
var { left, top, width, round, height } = this.state
|
|
152
|
-
round = round == undefined ? 0 : safeRound(round, width, height)
|
|
153
|
-
|
|
154
|
-
return [
|
|
155
|
-
{
|
|
156
|
-
x: left + (width / 2) * (round / 100),
|
|
157
|
-
y: top,
|
|
158
|
-
handler: controlHandler
|
|
159
|
-
}
|
|
160
|
-
]
|
|
161
|
-
}
|
|
162
|
-
|
|
39
|
+
/** 게이지는 자체 id 로 매핑하지 않는다 — 부모 STOCKER 가 데이터를 push. */
|
|
163
40
|
isIdentifiable() {
|
|
164
41
|
return false
|
|
165
42
|
}
|
|
166
43
|
|
|
167
|
-
|
|
168
|
-
const { legendName } = this.state
|
|
169
|
-
|
|
170
|
-
if (legendName) {
|
|
171
|
-
return (this.root as any)?.style?.[legendName]
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
get legend(): Legend {
|
|
176
|
-
return this.getTheme() || this.getLegendFallback()
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
getLegendFallback() {
|
|
44
|
+
getLegendFallback(): Legend {
|
|
180
45
|
return (this.root as any).stockerCapacityLegendTheme || LEGEND_CAPACITY
|
|
181
46
|
}
|
|
182
47
|
|
|
183
|
-
get statusColor() {
|
|
184
|
-
const { fullup } = this.state
|
|
185
|
-
return fullup ? 'red' : getValueOnRanges(this.usage, this.legend)
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
get text() {
|
|
189
|
-
// usage% 값은 capacity 모드에서만 표시. carrier/abnormal 은 세그먼트별 자체 라벨을 그리므로
|
|
190
|
-
// base 의 value 텍스트(value-property=usage)를 억제한다.
|
|
191
|
-
if (this.gaugeMode !== 'capacity') {
|
|
192
|
-
return ''
|
|
193
|
-
}
|
|
194
|
-
return this.usage + '%'
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
render(context: CanvasRenderingContext2D) {
|
|
198
|
-
const mode = this.gaugeMode
|
|
199
|
-
|
|
200
|
-
if (mode === 'carrier' || mode === 'abnormal') {
|
|
201
|
-
this.renderSegmentGauge(context, this.segments(mode))
|
|
202
|
-
return
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
// capacity (기본)
|
|
206
|
-
const { width, height } = this.bounds
|
|
207
|
-
if (width >= height) {
|
|
208
|
-
this.renderLandscape(context)
|
|
209
|
-
} else {
|
|
210
|
-
this.renderPortrait(context)
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
|
|
214
48
|
/**
|
|
215
49
|
* Shape.postrender 의 drawFill/drawStroke 를 건너뛴다.
|
|
216
|
-
*
|
|
217
|
-
* fillStyle 로 게이지 전체를 다시
|
|
50
|
+
* render 가 자체 배경(fillStyle)·외곽선(strokeStyle)을 이미 그리므로, base 가
|
|
51
|
+
* fillStyle 로 게이지 전체를 다시 덮는(각 모드 표현이 사라지는) 것을 방지한다.
|
|
218
52
|
* 값 텍스트(drawText)는 Component.postrender 와 동일하게 유지한다.
|
|
219
53
|
*/
|
|
220
54
|
postrender(context: CanvasRenderingContext2D) {
|
|
221
|
-
this.drawText(context)
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
/**
|
|
225
|
-
* carrier/abnormal mode 의 세그먼트 목록 (공유 유틸 = 3D 와 동일 계산).
|
|
226
|
-
* 배열 순서 = portrait 아래→위 / landscape 왼→오.
|
|
227
|
-
*/
|
|
228
|
-
segments(mode: string): GaugeSegment[] {
|
|
229
|
-
if (mode === 'abnormal') {
|
|
230
|
-
const { carrierNormal = 0, carrierAbnormal = 0 } = this.state
|
|
231
|
-
return abnormalSegments(carrierNormal, carrierAbnormal)
|
|
232
|
-
}
|
|
233
|
-
const { carrierFull = 0, carrierEmpty = 0, carrierEmptyEmpty = 0 } = this.state
|
|
234
|
-
// 색상은 carrier 컴포넌트와 동일한 legend(테마)에서 취득 (carrierColorsFromLegend).
|
|
235
|
-
const colors = carrierColorsFromLegend(resolveCarrierLegend(this))
|
|
236
|
-
return carrierSegments(carrierFull, carrierEmpty, carrierEmptyEmpty, colors)
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
/** 세그먼트 게이지 렌더 (carrier/abnormal 공통). bounds 방향으로 landscape/portrait 자동. */
|
|
240
|
-
renderSegmentGauge(context: CanvasRenderingContext2D, segments: GaugeSegment[]) {
|
|
241
|
-
const { left, top, width, height } = this.bounds
|
|
242
|
-
const { lineWidth, strokeStyle, round, fillStyle = 'white' } = this.state
|
|
243
|
-
const landscape = width >= height
|
|
244
|
-
|
|
245
|
-
context.save()
|
|
246
|
-
context.translate(left, top)
|
|
247
|
-
|
|
248
|
-
// background
|
|
249
|
-
context.beginPath()
|
|
250
|
-
context.fillStyle = fillStyle
|
|
251
|
-
context.roundRect(0, 0, width, height, round)
|
|
252
|
-
context.fill()
|
|
253
|
-
context.clip()
|
|
254
|
-
|
|
255
|
-
if (landscape) {
|
|
256
|
-
// 왼→오
|
|
257
|
-
let x = 0
|
|
258
|
-
for (const seg of segments) {
|
|
259
|
-
const segWidth = width * seg.ratio
|
|
260
|
-
if (segWidth <= 0) continue
|
|
261
|
-
|
|
262
|
-
context.beginPath()
|
|
263
|
-
context.fillStyle = seg.color
|
|
264
|
-
context.rect(x, 0, segWidth, height)
|
|
265
|
-
context.fill()
|
|
266
|
-
|
|
267
|
-
this._drawSegmentLabel(context, seg, x, 0, segWidth, height)
|
|
268
|
-
x += segWidth
|
|
269
|
-
}
|
|
270
|
-
} else {
|
|
271
|
-
// 아래→위 (배열 첫 세그먼트가 바닥)
|
|
272
|
-
let y = height
|
|
273
|
-
for (const seg of segments) {
|
|
274
|
-
const segHeight = height * seg.ratio
|
|
275
|
-
if (segHeight <= 0) continue
|
|
276
|
-
|
|
277
|
-
y -= segHeight
|
|
278
|
-
|
|
279
|
-
context.beginPath()
|
|
280
|
-
context.fillStyle = seg.color
|
|
281
|
-
context.rect(0, y, width, segHeight)
|
|
282
|
-
context.fill()
|
|
283
|
-
|
|
284
|
-
this._drawSegmentLabel(context, seg, 0, y, width, segHeight)
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
// outline
|
|
289
|
-
context.beginPath()
|
|
290
|
-
context.setLineDash([])
|
|
291
|
-
context.strokeStyle = strokeStyle
|
|
292
|
-
context.lineWidth = lineWidth
|
|
293
|
-
context.roundRect(0, 0, width, height, round)
|
|
294
|
-
context.stroke()
|
|
295
|
-
|
|
296
|
-
context.translate(-left, -top)
|
|
297
|
-
context.restore()
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
private _drawSegmentLabel(
|
|
301
|
-
context: CanvasRenderingContext2D,
|
|
302
|
-
seg: GaugeSegment,
|
|
303
|
-
x: number,
|
|
304
|
-
y: number,
|
|
305
|
-
w: number,
|
|
306
|
-
h: number
|
|
307
|
-
) {
|
|
308
|
-
// 이름 라벨(Full/Empty/Abnormal…)은 그리지 않는다. 각 세그먼트 영역 중심에 숫자(%)만 표시.
|
|
309
|
-
// 컴포넌트의 폰트 설정(fontFamily/fontSize/bold/italic)을 그대로 적용 — this.font.
|
|
310
|
-
const fontSize = this.state.fontSize || 15
|
|
311
|
-
context.font = this.font
|
|
312
|
-
|
|
313
|
-
// 공간에 따라 단계적 축소: 넉넉하면 '값%', 좁으면 '%' 떼고 값만, 더 좁으면 생략.
|
|
314
|
-
const value = `${Math.round(seg.ratio * 100)}`
|
|
315
|
-
const fits = (t: string) => fontSize <= h && context.measureText(t).width <= w
|
|
316
|
-
|
|
317
|
-
let text: string | null = null
|
|
318
|
-
if (fits(`${value}%`)) {
|
|
319
|
-
text = `${value}%`
|
|
320
|
-
} else if (fits(value)) {
|
|
321
|
-
text = value
|
|
322
|
-
}
|
|
323
|
-
if (text === null) {
|
|
324
|
-
return
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
context.fillStyle = '#333'
|
|
328
|
-
context.textAlign = 'center'
|
|
329
|
-
context.textBaseline = 'middle'
|
|
330
|
-
context.fillText(text, x + w / 2, y + h / 2)
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
renderLandscape(context: CanvasRenderingContext2D) {
|
|
334
|
-
const { left, top, width, height } = this.bounds
|
|
335
|
-
const { highWatermark, lowWatermark, lineWidth, strokeStyle, round, fillStyle = 'white' } = this.state
|
|
336
|
-
|
|
337
|
-
context.save()
|
|
338
|
-
context.translate(left, top)
|
|
339
|
-
|
|
340
|
-
// gauge background
|
|
341
|
-
context.beginPath()
|
|
342
|
-
|
|
343
|
-
context.fillStyle = fillStyle
|
|
344
|
-
context.roundRect(0, 0, width, height, round)
|
|
345
|
-
|
|
346
|
-
context.fill()
|
|
347
|
-
|
|
348
|
-
context.clip()
|
|
349
|
-
|
|
350
|
-
// gauge foreground
|
|
351
|
-
context.beginPath()
|
|
352
|
-
|
|
353
|
-
context.fillStyle = this.statusColor!
|
|
354
|
-
|
|
355
|
-
context.rect(0, 0, (width / 100) * this.usage, height)
|
|
356
|
-
context.fill()
|
|
357
|
-
|
|
358
|
-
// high-watermark level
|
|
359
|
-
|
|
360
|
-
if (highWatermark && highWatermark > 0 && highWatermark < 100) {
|
|
361
|
-
context.beginPath()
|
|
362
|
-
|
|
363
|
-
context.strokeStyle = 'black'
|
|
364
|
-
context.lineWidth = 1
|
|
365
|
-
context.setLineDash([3])
|
|
366
|
-
|
|
367
|
-
const x = (width / 100) * highWatermark
|
|
368
|
-
context.moveTo(x, 0)
|
|
369
|
-
context.lineTo(x, height)
|
|
370
|
-
|
|
371
|
-
context.stroke()
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
// low-watermark level
|
|
375
|
-
|
|
376
|
-
if (lowWatermark && lowWatermark > 0 && lowWatermark < 100) {
|
|
377
|
-
context.beginPath()
|
|
378
|
-
|
|
379
|
-
context.strokeStyle = 'black'
|
|
380
|
-
context.lineWidth = 1
|
|
381
|
-
context.setLineDash([3])
|
|
382
|
-
|
|
383
|
-
const x = (width / 100) * lowWatermark
|
|
384
|
-
context.moveTo(x, 0)
|
|
385
|
-
context.lineTo(x, height)
|
|
386
|
-
|
|
387
|
-
context.stroke()
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
// gauge outline
|
|
391
|
-
context.beginPath()
|
|
392
|
-
|
|
393
|
-
context.setLineDash([])
|
|
394
|
-
context.strokeStyle = strokeStyle
|
|
395
|
-
context.lineWidth = lineWidth
|
|
396
|
-
context.roundRect(0, 0, width, height, round)
|
|
397
|
-
context.stroke()
|
|
398
|
-
|
|
399
|
-
context.beginPath()
|
|
400
|
-
|
|
401
|
-
context.translate(-left, -top)
|
|
402
|
-
context.restore()
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
renderPortrait(context: CanvasRenderingContext2D) {
|
|
406
|
-
const { left, top, width, height } = this.bounds
|
|
407
|
-
const { highWatermark, lowWatermark, lineWidth, strokeStyle, round, fillStyle = 'white' } = this.state
|
|
408
|
-
|
|
409
|
-
context.save()
|
|
410
|
-
context.translate(left, top)
|
|
411
|
-
|
|
412
|
-
// gauge background
|
|
413
|
-
context.beginPath()
|
|
414
|
-
|
|
415
|
-
context.fillStyle = fillStyle
|
|
416
|
-
context.roundRect(0, 0, width, height, round)
|
|
417
|
-
|
|
418
|
-
context.fill()
|
|
419
|
-
|
|
420
|
-
context.clip()
|
|
421
|
-
|
|
422
|
-
// gauge foreground
|
|
423
|
-
context.beginPath()
|
|
424
|
-
|
|
425
|
-
context.fillStyle = this.statusColor!
|
|
426
|
-
|
|
427
|
-
context.rect(0, height - (height / 100) * this.usage, width, (height / 100) * this.usage)
|
|
428
|
-
context.fill()
|
|
429
|
-
|
|
430
|
-
// high-watermark level
|
|
431
|
-
|
|
432
|
-
if (highWatermark && highWatermark > 0 && highWatermark < 100) {
|
|
433
|
-
context.beginPath()
|
|
434
|
-
|
|
435
|
-
context.strokeStyle = 'black'
|
|
436
|
-
context.lineWidth = 1
|
|
437
|
-
context.setLineDash([3])
|
|
438
|
-
|
|
439
|
-
const y = height - (height / 100) * highWatermark
|
|
440
|
-
context.moveTo(0, y)
|
|
441
|
-
context.lineTo(width, y)
|
|
442
|
-
|
|
443
|
-
context.stroke()
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
// low-watermark level
|
|
447
|
-
|
|
448
|
-
if (lowWatermark && lowWatermark > 0 && lowWatermark < 100) {
|
|
449
|
-
context.beginPath()
|
|
450
|
-
|
|
451
|
-
context.strokeStyle = 'black'
|
|
452
|
-
context.lineWidth = 1
|
|
453
|
-
context.setLineDash([3])
|
|
454
|
-
|
|
455
|
-
const y = height - (height / 100) * lowWatermark
|
|
456
|
-
context.moveTo(0, y)
|
|
457
|
-
context.lineTo(width, y)
|
|
458
|
-
|
|
459
|
-
context.stroke()
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
// gauge outline
|
|
463
|
-
context.beginPath()
|
|
464
|
-
|
|
465
|
-
context.setLineDash([])
|
|
466
|
-
context.strokeStyle = strokeStyle
|
|
467
|
-
context.lineWidth = lineWidth
|
|
468
|
-
context.roundRect(0, 0, width, height, round)
|
|
469
|
-
context.stroke()
|
|
470
|
-
|
|
471
|
-
context.beginPath()
|
|
472
|
-
|
|
473
|
-
context.translate(-left, -top)
|
|
474
|
-
context.restore()
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
get usage() {
|
|
478
|
-
const { currentUsage = 0, maxCapacity = 100 } = this.state
|
|
479
|
-
return Math.round((currentUsage / maxCapacity) * 100)
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
set usage(usage: number) {
|
|
483
|
-
// intentionally have done nothing
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
onchangeData(after: Record<string, any>, before: Record<string, any>): void {
|
|
487
|
-
const data = this.data && typeof this.data == 'object' ? this.data : {}
|
|
488
|
-
const {
|
|
489
|
-
CURRENTCAPACITY = 0,
|
|
490
|
-
MAXCAPACITY = 100,
|
|
491
|
-
HIGHWATERMARK = 100,
|
|
492
|
-
LOWWATERMARK = 0,
|
|
493
|
-
FULLUP = 'F',
|
|
494
|
-
CARRIER_FULL = 0,
|
|
495
|
-
CARRIER_EMPTY = 0,
|
|
496
|
-
CARRIER_EMPTY_EMPTY = 0,
|
|
497
|
-
CARRIER_NORMAL = 0,
|
|
498
|
-
CARRIER_ABNORMAL = 0
|
|
499
|
-
} = data
|
|
500
|
-
|
|
501
|
-
const currentUsage = Number(CURRENTCAPACITY) || 0
|
|
502
|
-
const maxCapacity = Number(MAXCAPACITY) || 100
|
|
503
|
-
const highWatermark = Number(HIGHWATERMARK) || 100
|
|
504
|
-
const lowWatermark = Number(LOWWATERMARK) || 0
|
|
505
|
-
|
|
506
|
-
this.setState({
|
|
507
|
-
currentUsage,
|
|
508
|
-
maxCapacity,
|
|
509
|
-
highWatermark,
|
|
510
|
-
lowWatermark,
|
|
511
|
-
fullup: FULLUP == 'T',
|
|
512
|
-
usage: Math.round((currentUsage / maxCapacity) * 100),
|
|
513
|
-
carrierFull: Number(CARRIER_FULL) || 0,
|
|
514
|
-
carrierEmpty: Number(CARRIER_EMPTY) || 0,
|
|
515
|
-
carrierEmptyEmpty: Number(CARRIER_EMPTY_EMPTY) || 0,
|
|
516
|
-
carrierNormal: Number(CARRIER_NORMAL) || 0,
|
|
517
|
-
carrierAbnormal: Number(CARRIER_ABNORMAL) || 0
|
|
518
|
-
})
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
// ── hover 툴팁 (carrier/abnormal 모드 세그먼트별 수치) ───────────────────────────
|
|
522
|
-
// 색상만으로 타입 구분이 어려울 때, 마우스 hover 로 정확한 수치 확인. 뷰어 모드 전용
|
|
523
|
-
// (model-layer 가 편집 모드에선 hover dispatch 안 함).
|
|
524
|
-
|
|
525
|
-
async ready(): Promise<void> {
|
|
526
|
-
await super.ready()
|
|
527
|
-
// hover 이벤트 수신 opt-in: model-layer _onmouseenter_all guard(hasLegacyHover) 통과.
|
|
528
|
-
const ev = this.model.event
|
|
529
|
-
if (!ev || !ev.hover) {
|
|
530
|
-
this.model.event = { ...(ev || {}), hover: true }
|
|
531
|
-
}
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
onMouseEnter(): void {
|
|
535
|
-
// capacity 는 값이 한 개(usage%)라 툴팁 불필요. carrier/abnormal 만.
|
|
536
|
-
if (this.gaugeMode === 'capacity') return
|
|
537
|
-
this._showGaugeTooltip()
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
onMouseLeave(): void {
|
|
541
|
-
this._hideGaugeTooltip()
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
dispose(): void {
|
|
545
|
-
this._hideGaugeTooltip()
|
|
546
|
-
super.dispose()
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
private _tooltipEl?: HTMLDivElement
|
|
550
|
-
private _tooltipMove?: (e: MouseEvent) => void
|
|
551
|
-
|
|
552
|
-
private _showGaugeTooltip(): void {
|
|
553
|
-
if (typeof document === 'undefined') return
|
|
554
|
-
const segments = this.segments(this.gaugeMode).filter(s => s.count > 0)
|
|
555
|
-
if (!segments.length) return
|
|
556
|
-
|
|
557
|
-
this._hideGaugeTooltip()
|
|
558
|
-
|
|
559
|
-
const el = document.createElement('div')
|
|
560
|
-
el.style.cssText = [
|
|
561
|
-
'position:fixed',
|
|
562
|
-
'left:-9999px',
|
|
563
|
-
'top:0',
|
|
564
|
-
'z-index:2147483647',
|
|
565
|
-
'pointer-events:none',
|
|
566
|
-
'background:rgba(30,30,30,0.92)',
|
|
567
|
-
'color:#fff',
|
|
568
|
-
'padding:6px 9px',
|
|
569
|
-
'border-radius:6px',
|
|
570
|
-
'font:12px/1.5 sans-serif',
|
|
571
|
-
'box-shadow:0 2px 10px rgba(0,0,0,0.35)',
|
|
572
|
-
'white-space:nowrap'
|
|
573
|
-
].join(';')
|
|
574
|
-
|
|
575
|
-
for (const seg of segments) {
|
|
576
|
-
const row = document.createElement('div')
|
|
577
|
-
row.style.cssText = 'display:flex;align-items:center;gap:6px'
|
|
578
|
-
|
|
579
|
-
const swatch = document.createElement('span')
|
|
580
|
-
swatch.style.cssText = `display:inline-block;width:10px;height:10px;border-radius:2px;background:${seg.color}`
|
|
581
|
-
|
|
582
|
-
const label = document.createElement('span')
|
|
583
|
-
label.textContent = seg.label.replace(/\n/g, ' ')
|
|
584
|
-
label.style.cssText = 'flex:1'
|
|
585
|
-
|
|
586
|
-
const value = document.createElement('span')
|
|
587
|
-
value.textContent = String(seg.count)
|
|
588
|
-
value.style.cssText = 'font-weight:700;margin-left:12px'
|
|
589
|
-
|
|
590
|
-
row.append(swatch, label, value)
|
|
591
|
-
el.appendChild(row)
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
document.body.appendChild(el)
|
|
595
|
-
this._tooltipEl = el
|
|
596
|
-
|
|
597
|
-
this._tooltipMove = (e: MouseEvent) => {
|
|
598
|
-
el.style.left = `${e.clientX + 14}px`
|
|
599
|
-
el.style.top = `${e.clientY + 14}px`
|
|
600
|
-
}
|
|
601
|
-
document.addEventListener('mousemove', this._tooltipMove, true)
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
private _hideGaugeTooltip(): void {
|
|
605
|
-
if (this._tooltipMove) {
|
|
606
|
-
document.removeEventListener('mousemove', this._tooltipMove, true)
|
|
607
|
-
this._tooltipMove = undefined
|
|
608
|
-
}
|
|
609
|
-
if (this._tooltipEl) {
|
|
610
|
-
this._tooltipEl.remove()
|
|
611
|
-
this._tooltipEl = undefined
|
|
612
|
-
}
|
|
55
|
+
;(this as any).drawText(context)
|
|
613
56
|
}
|
|
614
57
|
}
|
|
@@ -16,6 +16,8 @@ import { Legend } from '../features/mcs-status-default'
|
|
|
16
16
|
export type StockerGaugeMode = 'capacity' | 'carrier' | 'abnormal'
|
|
17
17
|
|
|
18
18
|
export interface GaugeSegment {
|
|
19
|
+
/** 안정적 식별 토큰 (대문자, 클릭 이벤트 payload·데이터 필드 관례와 일치). */
|
|
20
|
+
key: 'FULL' | 'EMPTY' | 'EMPTYEMPTY' | 'NORMAL' | 'ABNORMAL'
|
|
19
21
|
/** 세그먼트 라벨(2D per-segment 표시용, '\n' 은 줄바꿈). */
|
|
20
22
|
label: string
|
|
21
23
|
/** 카운트 원본값. */
|
|
@@ -80,9 +82,9 @@ export function carrierSegments(
|
|
|
80
82
|
const total = f + e + ee
|
|
81
83
|
const r = (c: number) => (total > 0 ? c / total : 0)
|
|
82
84
|
return [
|
|
83
|
-
{ label: 'Full', count: f, ratio: r(f), color: colors.full },
|
|
84
|
-
{ label: 'Empty', count: e, ratio: r(e), color: colors.empty },
|
|
85
|
-
{ label: 'Empty\nEmpty', count: ee, ratio: r(ee), color: colors.emptyEmpty }
|
|
85
|
+
{ key: 'FULL', label: 'Full', count: f, ratio: r(f), color: colors.full },
|
|
86
|
+
{ key: 'EMPTY', label: 'Empty', count: e, ratio: r(e), color: colors.empty },
|
|
87
|
+
{ key: 'EMPTYEMPTY', label: 'Empty\nEmpty', count: ee, ratio: r(ee), color: colors.emptyEmpty }
|
|
86
88
|
]
|
|
87
89
|
}
|
|
88
90
|
|
|
@@ -93,11 +95,32 @@ export function abnormalSegments(normal: unknown, abnormal: unknown): GaugeSegme
|
|
|
93
95
|
const total = n + a
|
|
94
96
|
const r = (c: number) => (total > 0 ? c / total : 0)
|
|
95
97
|
return [
|
|
96
|
-
{ label: 'Normal', count: n, ratio: r(n), color: ABNORMAL_COLORS.normal },
|
|
97
|
-
{ label: 'Abnormal', count: a, ratio: r(a), color: ABNORMAL_COLORS.abnormal }
|
|
98
|
+
{ key: 'NORMAL', label: 'Normal', count: n, ratio: r(n), color: ABNORMAL_COLORS.normal },
|
|
99
|
+
{ key: 'ABNORMAL', label: 'Abnormal', count: a, ratio: r(a), color: ABNORMAL_COLORS.abnormal }
|
|
98
100
|
]
|
|
99
101
|
}
|
|
100
102
|
|
|
103
|
+
/**
|
|
104
|
+
* 누적 비율 위치(pos, 0..span 범위)가 속한 세그먼트를 반환한다. ratio 0 세그먼트는
|
|
105
|
+
* 화면에 없으므로 건너뛴다. 경계(pos == span)·초과 클릭은 마지막 비영 세그먼트로 귀속하고,
|
|
106
|
+
* 데이터가 전혀 없으면(모든 ratio 0) null.
|
|
107
|
+
*
|
|
108
|
+
* renderSegmentGauge 의 배치와 동일 규칙(누적 채움)이며, 2D 렌더와 클릭 판정이 이 함수를
|
|
109
|
+
* 공유해 drift 를 막는다. 방향(landscape 왼→오 / portrait 아래→위)은 호출부가 pos 로 흡수한다.
|
|
110
|
+
*/
|
|
111
|
+
export function pickSegmentAt(segments: GaugeSegment[], pos: number, span: number): GaugeSegment | null {
|
|
112
|
+
let acc = 0
|
|
113
|
+
let lastNonZero: GaugeSegment | null = null
|
|
114
|
+
for (const seg of segments) {
|
|
115
|
+
const segLen = span * seg.ratio
|
|
116
|
+
if (segLen <= 0) continue
|
|
117
|
+
lastNonZero = seg
|
|
118
|
+
if (pos < acc + segLen) return seg
|
|
119
|
+
acc += segLen
|
|
120
|
+
}
|
|
121
|
+
return lastNonZero
|
|
122
|
+
}
|
|
123
|
+
|
|
101
124
|
/** 캐리어 세그먼트 — canonical raw data(CARRIER_*) 에서 직접. 3D 처럼 raw data 소스용. */
|
|
102
125
|
export function carrierSegmentsFromData(data: any, colors?: CarrierColors): GaugeSegment[] {
|
|
103
126
|
const d = data && typeof data === 'object' ? data : {}
|