@fmsim/machine 2.0.0-beta.10 → 2.0.0-beta.101
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/agv.js +6 -2
- package/dist/agv.js.map +1 -1
- package/dist/carrier-manager.js +183 -0
- package/dist/carrier-manager.js.map +1 -0
- package/dist/carrier.js +99 -9
- package/dist/carrier.js.map +1 -1
- package/dist/conveyor-join.js +8 -2
- package/dist/conveyor-join.js.map +1 -1
- package/dist/conveyor.js +8 -0
- package/dist/conveyor.js.map +1 -1
- package/dist/crane.js +144 -1
- package/dist/crane.js.map +1 -1
- package/dist/factories/agv-3d.js +17 -36
- package/dist/factories/agv-3d.js.map +1 -1
- package/dist/factories/conveyor-3d.js +179 -100
- package/dist/factories/conveyor-3d.js.map +1 -1
- package/dist/factories/crane-3d.js +56 -29
- package/dist/factories/crane-3d.js.map +1 -1
- package/dist/factories/crane-rail-3d.js +14 -18
- package/dist/factories/crane-rail-3d.js.map +1 -1
- package/dist/factories/machine-3d.js +533 -197
- package/dist/factories/machine-3d.js.map +1 -1
- package/dist/factories/oht-3d.js +29 -47
- package/dist/factories/oht-3d.js.map +1 -1
- package/dist/factories/shuttle-3d.js +13 -32
- package/dist/factories/shuttle-3d.js.map +1 -1
- package/dist/factories/z-priority-defaults.js +33 -0
- package/dist/factories/z-priority-defaults.js.map +1 -0
- package/dist/factories/z-priority.js +54 -0
- package/dist/factories/z-priority.js.map +1 -0
- package/dist/features/conveyor-mixin.js.map +1 -1
- package/dist/features/mcs-status-default.js +15 -1
- package/dist/features/mcs-status-default.js.map +1 -1
- package/dist/features/mcs-status-mixin.js +22 -0
- package/dist/features/mcs-status-mixin.js.map +1 -1
- package/dist/features/mcs-zone-mixin.js.map +1 -1
- package/dist/features/parent-object-mixin.js +15 -7
- package/dist/features/parent-object-mixin.js.map +1 -1
- package/dist/floating-container.js +5 -0
- package/dist/floating-container.js.map +1 -1
- package/dist/index.js +52 -34
- package/dist/index.js.map +1 -1
- package/dist/mcs-carrier-holder.js +56 -22
- package/dist/mcs-carrier-holder.js.map +1 -1
- package/dist/mcs-machine.js +20 -0
- package/dist/mcs-machine.js.map +1 -1
- package/dist/mcs-unit.js +17 -0
- package/dist/mcs-unit.js.map +1 -1
- package/dist/mcs-vehicle.js +8 -3
- package/dist/mcs-vehicle.js.map +1 -1
- package/dist/node.js +10 -3
- package/dist/node.js.map +1 -1
- package/dist/oht.js +4 -0
- package/dist/oht.js.map +1 -1
- package/dist/port-flow.js +4 -0
- package/dist/port-flow.js.map +1 -1
- package/dist/port.js +4 -0
- package/dist/port.js.map +1 -1
- package/dist/scanner.js +30 -0
- package/dist/scanner.js.map +1 -0
- package/dist/scene/root-container-override.js +160 -96
- package/dist/scene/root-container-override.js.map +1 -1
- package/dist/shelf.js +4 -0
- package/dist/shelf.js.map +1 -1
- package/dist/static-container.js +5 -0
- package/dist/static-container.js.map +1 -1
- package/dist/stocker-abnormal-bar.js +5 -1
- package/dist/stocker-abnormal-bar.js.map +1 -1
- package/dist/stocker-capacity-bar.js +253 -6
- package/dist/stocker-capacity-bar.js.map +1 -1
- package/dist/stocker-carrier-bar.js +5 -1
- package/dist/stocker-carrier-bar.js.map +1 -1
- package/dist/stocker.js +6 -0
- package/dist/stocker.js.map +1 -1
- package/dist/templates/backdrop.js +30 -0
- package/dist/templates/backdrop.js.map +1 -0
- package/dist/templates/decal.js +21 -0
- package/dist/templates/decal.js.map +1 -0
- package/dist/templates/index.js +7 -1
- package/dist/templates/index.js.map +1 -1
- package/dist/templates/scanner.js +20 -0
- package/dist/templates/scanner.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/utils/crane-position.js +83 -0
- package/dist/utils/crane-position.js.map +1 -0
- package/dist/utils/data-value.js +21 -0
- package/dist/utils/data-value.js.map +1 -0
- package/dist/utils/fit-gauge-scale.js +28 -0
- package/dist/utils/fit-gauge-scale.js.map +1 -0
- package/dist/utils/get-value-on-ranges.js +2 -4
- package/dist/utils/get-value-on-ranges.js.map +1 -1
- package/dist/utils/stocker-gauge.js +78 -0
- package/dist/utils/stocker-gauge.js.map +1 -0
- package/dist/zone-capacity-bar.js +3 -3
- package/dist/zone-capacity-bar.js.map +1 -1
- package/icons/backdrop.png +0 -0
- package/icons/decal.png +0 -0
- package/jest.config.js +10 -0
- package/package.json +4 -3
- package/src/agv.ts +7 -2
- package/src/carrier-manager.ts +223 -0
- package/src/carrier.ts +103 -10
- package/src/conveyor-join.ts +5 -2
- package/src/conveyor.ts +8 -0
- package/src/crane.ts +181 -1
- package/src/factories/agv-3d.ts +19 -36
- package/src/factories/conveyor-3d.ts +183 -103
- package/src/factories/crane-3d.ts +62 -29
- package/src/factories/crane-rail-3d.ts +17 -20
- package/src/factories/machine-3d.ts +631 -205
- package/src/factories/oht-3d.ts +30 -46
- package/src/factories/shuttle-3d.ts +14 -32
- package/src/factories/z-priority-defaults.ts +38 -0
- package/src/factories/z-priority.ts +60 -0
- package/src/features/conveyor-mixin.ts +1 -1
- package/src/features/mcs-status-default.ts +15 -1
- package/src/features/mcs-status-mixin.ts +26 -4
- package/src/features/mcs-zone-mixin.ts +1 -1
- package/src/features/parent-object-mixin.ts +17 -8
- package/src/floating-container.ts +5 -0
- package/src/index.ts +58 -34
- package/src/mcs-carrier-holder.ts +57 -34
- package/src/mcs-machine.ts +23 -0
- package/src/mcs-unit.ts +21 -1
- package/src/mcs-vehicle.ts +9 -6
- package/src/node.ts +9 -3
- package/src/oht.ts +6 -1
- package/src/port-flow.ts +5 -0
- package/src/port.ts +5 -0
- package/src/scanner.ts +29 -0
- package/src/scene/root-container-override.ts +154 -103
- package/src/shelf.ts +5 -0
- package/src/static-container.ts +5 -0
- package/src/stocker-abnormal-bar.ts +6 -1
- package/src/stocker-capacity-bar.ts +298 -7
- package/src/stocker-carrier-bar.ts +6 -1
- package/src/stocker.ts +7 -0
- package/src/templates/backdrop.ts +30 -0
- package/src/templates/decal.ts +21 -0
- package/src/templates/index.ts +7 -1
- package/src/templates/scanner.ts +20 -0
- package/src/utils/crane-position.ts +105 -0
- package/src/utils/data-value.ts +19 -0
- package/src/utils/fit-gauge-scale.ts +35 -0
- package/src/utils/get-value-on-ranges.ts +2 -4
- package/src/utils/stocker-gauge.ts +111 -0
- package/src/zone-capacity-bar.ts +3 -3
- package/translations/en.json +26 -1
- package/translations/ja.json +26 -1
- package/translations/ko.json +26 -1
- package/translations/ms.json +26 -1
- package/translations/zh.json +26 -1
|
@@ -72,6 +72,11 @@ export default class StockerAbnormalBar extends ParentObjectMixin(RectPath(Shape
|
|
|
72
72
|
return NATURE
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
/** StockerAbnormalBar 의 parentObject 는 STOCKER. */
|
|
76
|
+
get parentObjectTypes(): string[] {
|
|
77
|
+
return ['STOCKER']
|
|
78
|
+
}
|
|
79
|
+
|
|
75
80
|
get controls() {
|
|
76
81
|
var { left, top, width, round, height } = this.state
|
|
77
82
|
round = round == undefined ? 0 : safeRound(round, width, height)
|
|
@@ -232,7 +237,7 @@ export default class StockerAbnormalBar extends ParentObjectMixin(RectPath(Shape
|
|
|
232
237
|
const {
|
|
233
238
|
CARRIER_NORMAL = 0,
|
|
234
239
|
CARRIER_ABNORMAL = 0
|
|
235
|
-
} = typeof this.data == 'object' ? this.data : {}
|
|
240
|
+
} = this.data && typeof this.data == 'object' ? this.data : {}
|
|
236
241
|
|
|
237
242
|
this.setState({
|
|
238
243
|
carrierNormal: Number(CARRIER_NORMAL) || 0,
|
|
@@ -9,10 +9,30 @@ import {
|
|
|
9
9
|
} from '@hatiolab/things-scene'
|
|
10
10
|
import { themesColorRange } from '@fmsim/api'
|
|
11
11
|
import { MCSStatusMixin } from './features/mcs-status-mixin'
|
|
12
|
-
import { LEGEND_CAPACITY, Legend } from './features/mcs-status-default'
|
|
12
|
+
import { LEGEND_CAPACITY, Legend, resolveCarrierLegend } from './features/mcs-status-default'
|
|
13
13
|
import { safeRound } from './utils/safe-round'
|
|
14
|
-
import {
|
|
14
|
+
import { getValueOnRanges } from './utils/get-value-on-ranges'
|
|
15
15
|
import { ParentObjectMixin, ParentObjectMixinProperties } from './features/parent-object-mixin'
|
|
16
|
+
import { carrierSegments, abnormalSegments, carrierColorsFromLegend, 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',
|
|
@@ -92,6 +137,16 @@ export default class StockerCapacityBar extends ParentObjectMixin(MCSStatusMixin
|
|
|
92
137
|
return NATURE
|
|
93
138
|
}
|
|
94
139
|
|
|
140
|
+
/** StockerCapacityBar / MCSGaugeCapacityBar 의 parentObject 는 STOCKER. */
|
|
141
|
+
get parentObjectTypes(): string[] {
|
|
142
|
+
return ['STOCKER']
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** 게이지 모드: RootContainer 런타임 transient 속성. 3D(machine-3d) 와 동일 소스. */
|
|
146
|
+
get gaugeMode(): string {
|
|
147
|
+
return (this.root as any)?.stockerGaugeMode || 'capacity'
|
|
148
|
+
}
|
|
149
|
+
|
|
95
150
|
get controls() {
|
|
96
151
|
var { left, top, width, round, height } = this.state
|
|
97
152
|
round = round == undefined ? 0 : safeRound(round, width, height)
|
|
@@ -127,16 +182,28 @@ export default class StockerCapacityBar extends ParentObjectMixin(MCSStatusMixin
|
|
|
127
182
|
|
|
128
183
|
get statusColor() {
|
|
129
184
|
const { fullup } = this.state
|
|
130
|
-
return fullup ? 'red' :
|
|
185
|
+
return fullup ? 'red' : getValueOnRanges(this.usage, this.legend)
|
|
131
186
|
}
|
|
132
187
|
|
|
133
188
|
get text() {
|
|
189
|
+
// usage% 값은 capacity 모드에서만 표시. carrier/abnormal 은 세그먼트별 자체 라벨을 그리므로
|
|
190
|
+
// base 의 value 텍스트(value-property=usage)를 억제한다.
|
|
191
|
+
if (this.gaugeMode !== 'capacity') {
|
|
192
|
+
return ''
|
|
193
|
+
}
|
|
134
194
|
return this.usage + '%'
|
|
135
195
|
}
|
|
136
196
|
|
|
137
197
|
render(context: CanvasRenderingContext2D) {
|
|
138
|
-
const
|
|
198
|
+
const mode = this.gaugeMode
|
|
139
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
|
|
140
207
|
if (width >= height) {
|
|
141
208
|
this.renderLandscape(context)
|
|
142
209
|
} else {
|
|
@@ -144,6 +211,125 @@ export default class StockerCapacityBar extends ParentObjectMixin(MCSStatusMixin
|
|
|
144
211
|
}
|
|
145
212
|
}
|
|
146
213
|
|
|
214
|
+
/**
|
|
215
|
+
* Shape.postrender 의 drawFill/drawStroke 를 건너뛴다.
|
|
216
|
+
* 각 모드 render 가 자체 배경(fillStyle)·외곽선(strokeStyle)을 이미 그리므로, base 가
|
|
217
|
+
* fillStyle 로 게이지 전체를 다시 덮어(각 모드 표현이 사라지는) 것을 방지한다.
|
|
218
|
+
* 값 텍스트(drawText)는 Component.postrender 와 동일하게 유지한다.
|
|
219
|
+
*/
|
|
220
|
+
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
|
+
|
|
147
333
|
renderLandscape(context: CanvasRenderingContext2D) {
|
|
148
334
|
const { left, top, width, height } = this.bounds
|
|
149
335
|
const { highWatermark, lowWatermark, lineWidth, strokeStyle, round, fillStyle = 'white' } = this.state
|
|
@@ -298,13 +484,19 @@ export default class StockerCapacityBar extends ParentObjectMixin(MCSStatusMixin
|
|
|
298
484
|
}
|
|
299
485
|
|
|
300
486
|
onchangeData(after: Record<string, any>, before: Record<string, any>): void {
|
|
487
|
+
const data = this.data && typeof this.data == 'object' ? this.data : {}
|
|
301
488
|
const {
|
|
302
489
|
CURRENTCAPACITY = 0,
|
|
303
490
|
MAXCAPACITY = 100,
|
|
304
491
|
HIGHWATERMARK = 100,
|
|
305
492
|
LOWWATERMARK = 0,
|
|
306
|
-
FULLUP = 'F'
|
|
307
|
-
|
|
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
|
|
308
500
|
|
|
309
501
|
const currentUsage = Number(CURRENTCAPACITY) || 0
|
|
310
502
|
const maxCapacity = Number(MAXCAPACITY) || 100
|
|
@@ -317,7 +509,106 @@ export default class StockerCapacityBar extends ParentObjectMixin(MCSStatusMixin
|
|
|
317
509
|
highWatermark,
|
|
318
510
|
lowWatermark,
|
|
319
511
|
fullup: FULLUP == 'T',
|
|
320
|
-
usage: Math.round((currentUsage / maxCapacity) * 100)
|
|
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
|
|
321
518
|
})
|
|
322
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
|
+
}
|
|
613
|
+
}
|
|
323
614
|
}
|
|
@@ -81,6 +81,11 @@ export default class StockerCarrierBar extends ParentObjectMixin(RectPath(Shape)
|
|
|
81
81
|
return NATURE
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
/** StockerCarrierBar 의 parentObject 는 STOCKER. */
|
|
85
|
+
get parentObjectTypes(): string[] {
|
|
86
|
+
return ['STOCKER']
|
|
87
|
+
}
|
|
88
|
+
|
|
84
89
|
get controls() {
|
|
85
90
|
var { left, top, width, round, height } = this.state
|
|
86
91
|
round = round == undefined ? 0 : safeRound(round, width, height)
|
|
@@ -246,7 +251,7 @@ export default class StockerCarrierBar extends ParentObjectMixin(RectPath(Shape)
|
|
|
246
251
|
CARRIER_FULL = 0,
|
|
247
252
|
CARRIER_EMPTY = 0,
|
|
248
253
|
CARRIER_EMPTY_EMPTY = 0
|
|
249
|
-
} = typeof this.data == 'object' ? this.data : {}
|
|
254
|
+
} = this.data && typeof this.data == 'object' ? this.data : {}
|
|
250
255
|
|
|
251
256
|
this.setState({
|
|
252
257
|
carrierFull: Number(CARRIER_FULL) || 0,
|
package/src/stocker.ts
CHANGED
|
@@ -47,6 +47,13 @@ export default class Stocker extends MCSTransport {
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
onchangeData(after: Record<string, any>, before: Record<string, any>): void {
|
|
50
|
+
/* MCSStatusMixin.onchangeData 의 statusColor → state.fillStyle 동기화 로직을
|
|
51
|
+
* 호출. 3D 경로에서는 mesh material 의 color 가 build 시점 한 번 결정되므로
|
|
52
|
+
* 데이터 변경 시 fillStyle 이 갱신되어야 MCSRealObject3D.onchange 의
|
|
53
|
+
* 'fillStyle in after' 분기를 타고 mesh rebuild 가 발생한다.
|
|
54
|
+
* (2D 는 render() 가 매 프레임 statusColor getter 를 직접 사용하므로 영향 없음.) */
|
|
55
|
+
super.onchangeData(after, before)
|
|
56
|
+
|
|
50
57
|
/* non-identifiable component 즉, StockerCapacityBar 에게 데이터를 전달함 */
|
|
51
58
|
this.components.filter(component => !component.isIdentifiable()).forEach(component => (component.data = this.data))
|
|
52
59
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
const icon = new URL('../../icons/backdrop.png', import.meta.url).href
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
type: 'backdrop',
|
|
5
|
+
description: '3D scene backdrop — 6-sided envelope (4 walls + floor + ceiling)',
|
|
6
|
+
group: ['etc'],
|
|
7
|
+
icon,
|
|
8
|
+
model: {
|
|
9
|
+
type: 'backdrop',
|
|
10
|
+
top: 0,
|
|
11
|
+
left: 0,
|
|
12
|
+
width: 3000,
|
|
13
|
+
height: 2400,
|
|
14
|
+
// depth seeded on creation as min(width, height) by Backdrop's
|
|
15
|
+
// `_syncFromRoot(seedDepth=true)`. Editable via property panel
|
|
16
|
+
// afterward — board-resize syncs won't override it.
|
|
17
|
+
fitToBoard: true,
|
|
18
|
+
alpha: 1,
|
|
19
|
+
// 6면 합성 상태. 비어있으면 backdrop 컴포넌트의 default 가 적용됨
|
|
20
|
+
// (벽 4면 = #dde2e8/0.35, 바닥 = #9c9c9c/1.0, 천장 = #e8e8ec/1.0).
|
|
21
|
+
faces: {
|
|
22
|
+
east: { fillStyle: '#dde2e8', visible: true, alpha: 0.35 },
|
|
23
|
+
west: { fillStyle: '#dde2e8', visible: true, alpha: 0.35 },
|
|
24
|
+
north: { fillStyle: '#dde2e8', visible: true, alpha: 0.35 },
|
|
25
|
+
south: { fillStyle: '#dde2e8', visible: true, alpha: 0.35 },
|
|
26
|
+
floor: { fillStyle: '#9c9c9c', visible: true, alpha: 1.0 },
|
|
27
|
+
ceiling: { fillStyle: '#e8e8ec', visible: true, alpha: 1.0 }
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const icon = new URL('../../icons/decal.png', import.meta.url).href
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
type: 'decal',
|
|
5
|
+
description: 'flat surface decal (3D-only) — floor zone paint, path lanes, signage',
|
|
6
|
+
group: ['etc'],
|
|
7
|
+
icon,
|
|
8
|
+
model: {
|
|
9
|
+
type: 'decal',
|
|
10
|
+
top: 100,
|
|
11
|
+
left: 100,
|
|
12
|
+
width: 200,
|
|
13
|
+
height: 200,
|
|
14
|
+
zPos: 0,
|
|
15
|
+
depth: 0,
|
|
16
|
+
fillStyle: '#cccccc',
|
|
17
|
+
strokeStyle: '#999999',
|
|
18
|
+
lineWidth: 1,
|
|
19
|
+
alpha: 1
|
|
20
|
+
}
|
|
21
|
+
}
|
package/src/templates/index.ts
CHANGED
|
@@ -17,9 +17,12 @@ import agvLine from './agv-line'
|
|
|
17
17
|
import ohtLine from './oht-line'
|
|
18
18
|
import conveyor from './conveyor'
|
|
19
19
|
|
|
20
|
+
import Scanner from './scanner'
|
|
20
21
|
import Node from './node'
|
|
21
22
|
import DataSubscription from './data-subscription'
|
|
22
23
|
import FloatingContainer from './floating-container'
|
|
24
|
+
import Decal from './decal'
|
|
25
|
+
import Backdrop from './backdrop'
|
|
23
26
|
|
|
24
27
|
export default [
|
|
25
28
|
StockerCapacityBar,
|
|
@@ -41,7 +44,10 @@ export default [
|
|
|
41
44
|
Shelf,
|
|
42
45
|
Port,
|
|
43
46
|
|
|
47
|
+
Scanner,
|
|
44
48
|
Node,
|
|
45
49
|
FloatingContainer,
|
|
46
|
-
DataSubscription
|
|
50
|
+
DataSubscription,
|
|
51
|
+
Decal,
|
|
52
|
+
Backdrop
|
|
47
53
|
]
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
const icon = new URL('../../icons/equipment.png', import.meta.url).href
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
type: 'Scanner',
|
|
5
|
+
description: 'Fixed mount barcode/QR scanner',
|
|
6
|
+
group: ['etc'],
|
|
7
|
+
icon,
|
|
8
|
+
model: {
|
|
9
|
+
type: 'Scanner',
|
|
10
|
+
x1: 100,
|
|
11
|
+
y1: 100,
|
|
12
|
+
x2: 130,
|
|
13
|
+
y2: 110,
|
|
14
|
+
x3: 100,
|
|
15
|
+
y3: 120,
|
|
16
|
+
strokeStyle: '#333',
|
|
17
|
+
fillStyle: 'red',
|
|
18
|
+
lineWidth: 1
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Crane 위치 데이터 (data.cranePosition) 변환 유틸.
|
|
3
|
+
*
|
|
4
|
+
* 명세:
|
|
5
|
+
* {
|
|
6
|
+
* cranePosition: {
|
|
7
|
+
* current: { x, y, z }, // 실 좌표값
|
|
8
|
+
* limit: { x:{min,max}, y:{min,max}, z:{min,max} } // 각 축 한계
|
|
9
|
+
* }
|
|
10
|
+
* }
|
|
11
|
+
*
|
|
12
|
+
* current 를 limit 범위 안의 0~1 비율로 환산한 뒤 host(부모) footprint 에 매핑한다.
|
|
13
|
+
* x → host 긴쪽, y → 짧은쪽, z → 포크 높이(forkZ 0~100).
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { parseIfJsonString } from './data-value'
|
|
17
|
+
|
|
18
|
+
/** 축별 한계 (cranePosition.limit.{x|y|z}). */
|
|
19
|
+
export interface AxisLimit {
|
|
20
|
+
min?: number
|
|
21
|
+
max?: number
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** 새 데이터 명세 — data.cranePosition. */
|
|
25
|
+
export interface CranePositionData {
|
|
26
|
+
current?: { x?: number; y?: number; z?: number }
|
|
27
|
+
limit?: { x?: AxisLimit; y?: AxisLimit; z?: AxisLimit }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* current 값을 limit 범위 안에서 0~1 비율로 환산.
|
|
32
|
+
* limit 누락 / 0 span / 비정상 입력 시 0 반환 (안전 fallback).
|
|
33
|
+
*/
|
|
34
|
+
export function axisRatio(value: unknown, lim?: AxisLimit): number {
|
|
35
|
+
const v = Number(value)
|
|
36
|
+
const min = Number(lim?.min) || 0
|
|
37
|
+
const max = Number(lim?.max)
|
|
38
|
+
if (!Number.isFinite(v) || !Number.isFinite(max) || max === min) return 0
|
|
39
|
+
return Math.max(0, Math.min(1, (v - min) / (max - min)))
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* host(부모) footprint 안에서 0~1 비율 (rx,ry) → 크레인 top-left 로컬 좌표.
|
|
44
|
+
*
|
|
45
|
+
* **스토커 안쪽 오프셋**: 크레인의 폭/두께를 감안해, 크레인 *전체* 가 host 안에
|
|
46
|
+
* 머무르도록 top-left 좌표를 [0, hostLen - craneLen] 범위로 매핑한다 (= 양끝에서
|
|
47
|
+
* craneLen/2 만큼 inset 된 중심선 위를 움직임). 크레인이 host 보다 크면 span 0
|
|
48
|
+
* 으로 떨어져 host 모서리에 정렬.
|
|
49
|
+
*
|
|
50
|
+
* x(rx) 는 host 의 긴쪽, y(ry) 는 짧은쪽에 매핑 (가상 rail 은 host 긴쪽 방향).
|
|
51
|
+
*/
|
|
52
|
+
export function mapCraneLocalPosition(
|
|
53
|
+
hostW: number,
|
|
54
|
+
hostH: number,
|
|
55
|
+
craneW: number,
|
|
56
|
+
craneH: number,
|
|
57
|
+
rx: number,
|
|
58
|
+
ry: number
|
|
59
|
+
): { left: number; top: number } {
|
|
60
|
+
const longIsWidth = hostW >= hostH
|
|
61
|
+
const longLen = longIsWidth ? hostW : hostH
|
|
62
|
+
const shortLen = longIsWidth ? hostH : hostW
|
|
63
|
+
const craneLongSize = longIsWidth ? craneW : craneH
|
|
64
|
+
const craneShortSize = longIsWidth ? craneH : craneW
|
|
65
|
+
|
|
66
|
+
// 크레인 크기를 뺀 inset 가용 span. top-left = ratio * span 이므로 크레인
|
|
67
|
+
// 전체가 host [0, hostLen] 안에 들어온다.
|
|
68
|
+
const longSpan = Math.max(0, longLen - craneLongSize)
|
|
69
|
+
const shortSpan = Math.max(0, shortLen - craneShortSize)
|
|
70
|
+
const longPos = rx * longSpan
|
|
71
|
+
const shortPos = ry * shortSpan
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
left: longIsWidth ? longPos : shortPos,
|
|
75
|
+
top: longIsWidth ? shortPos : longPos
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* 객체 키를 재귀적으로 소문자화한 사본 반환 (배열/원시값은 그대로, 값은 변형 안 함).
|
|
81
|
+
* 데이터 키는 모두 대문자 전제 — CRANEPOSITION 의 중첩 키(CURRENT/LIMIT/X/Y/Z/MIN/MAX)를
|
|
82
|
+
* 내부 표준형(소문자 current/limit/x/y/z/min/max)으로 변환하기 위한 정규화.
|
|
83
|
+
*/
|
|
84
|
+
export function lowerKeysDeep<T = any>(value: any): T {
|
|
85
|
+
if (Array.isArray(value)) return value.map(v => lowerKeysDeep(v)) as any
|
|
86
|
+
if (value && typeof value === 'object') {
|
|
87
|
+
const out: Record<string, any> = {}
|
|
88
|
+
for (const k of Object.keys(value)) {
|
|
89
|
+
out[k.toLowerCase()] = lowerKeysDeep(value[k])
|
|
90
|
+
}
|
|
91
|
+
return out as T
|
|
92
|
+
}
|
|
93
|
+
return value as T
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* raw CRANEPOSITION → 내부 표준형(소문자 키) 구조(CranePositionData).
|
|
98
|
+
* 값이 JSON 문자열로 올 수 있어 먼저 파싱하고, 중첩 키(CURRENT/LIMIT/X/Y/Z/MIN/MAX)를
|
|
99
|
+
* 소문자(current.x / limit.x.min)로 변환. 파싱 실패/비객체면 undefined.
|
|
100
|
+
*/
|
|
101
|
+
export function normalizeCranePosition(raw: any): CranePositionData | undefined {
|
|
102
|
+
const obj = parseIfJsonString(raw)
|
|
103
|
+
if (!obj || typeof obj !== 'object') return undefined
|
|
104
|
+
return lowerKeysDeep<CranePositionData>(obj)
|
|
105
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright © HatioLab Inc. All rights reserved.
|
|
3
|
+
*
|
|
4
|
+
* 데이터 필드값이 JSON 문자열로 전달될 수 있어(예: CRANEPOSITION, MULTICARRIERS)
|
|
5
|
+
* 객체/배열로 변환하기 위한 헬퍼.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 값이 JSON 문자열이면 파싱해서 반환. 객체/배열 등 비문자열은 그대로 반환.
|
|
10
|
+
* 파싱 실패 시 undefined.
|
|
11
|
+
*/
|
|
12
|
+
export function parseIfJsonString(value: any): any {
|
|
13
|
+
if (typeof value !== 'string') return value
|
|
14
|
+
try {
|
|
15
|
+
return JSON.parse(value)
|
|
16
|
+
} catch {
|
|
17
|
+
return undefined
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 스토커 지붕 위 용량 게이지 바의 비례 축소 계수를 계산한다.
|
|
3
|
+
*
|
|
4
|
+
* 정책:
|
|
5
|
+
* - 게이지 바의 *최대* 크기는 maxW × maxH. 스토커가 충분히 크면 이 크기 그대로 (계수 1).
|
|
6
|
+
* - 스토커가 그보다 작으면 지붕 footprint 안에 들어오도록 *비례* 축소 (uniform).
|
|
7
|
+
* - *확대는 하지 않는다* (계수 상한 1) — 현재 크기가 상한이라는 요구사항.
|
|
8
|
+
*
|
|
9
|
+
* 방향 매핑: 바의 가로축(maxW)은 스토커의 *장축*에 정렬된다 (buildGauge 의
|
|
10
|
+
* `isWidthLonger ? geo.rotateY` 처리와 일치). 따라서 가용 치수도 그 기준으로 매칭한다.
|
|
11
|
+
*
|
|
12
|
+
* @param w 스토커 footprint 가로 (월드 단위)
|
|
13
|
+
* @param h 스토커 footprint 세로 (월드 단위)
|
|
14
|
+
* @param maxW 게이지 바 최대 가로
|
|
15
|
+
* @param maxH 게이지 바 최대 세로
|
|
16
|
+
* @param margin 지붕 가장자리 여백 비율 (0~1, 기본 0.85)
|
|
17
|
+
* @returns 0 < scale <= 1
|
|
18
|
+
*/
|
|
19
|
+
export function fitGaugeScale(
|
|
20
|
+
w: number,
|
|
21
|
+
h: number,
|
|
22
|
+
maxW: number,
|
|
23
|
+
maxH: number,
|
|
24
|
+
margin: number = 0.85
|
|
25
|
+
): number {
|
|
26
|
+
const isWidthLonger = w >= h
|
|
27
|
+
// 바 가로축(maxW)이 정렬되는 스토커 치수 = 장축, 세로축(maxH) = 단축
|
|
28
|
+
const availAlongW = isWidthLonger ? w : h
|
|
29
|
+
const availAlongH = isWidthLonger ? h : w
|
|
30
|
+
|
|
31
|
+
const scale = Math.min(1, (availAlongW * margin) / maxW, (availAlongH * margin) / maxH)
|
|
32
|
+
|
|
33
|
+
// 음수/NaN 방어 — 0 이하로 떨어지면 안 됨
|
|
34
|
+
return scale > 0 && Number.isFinite(scale) ? scale : 1
|
|
35
|
+
}
|