@fmsim/machine 2.0.0-beta.3 → 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.3",
5
+ "version": "2.0.0-beta.5",
6
6
  "main": "dist/index.js",
7
7
  "module": "dist/index.js",
8
8
  "things-scene": true,
@@ -54,5 +54,5 @@
54
54
  "prettier --write"
55
55
  ]
56
56
  },
57
- "gitHead": "29cf1d4f43eb48dc030aaeb188f2dce65ecff95f"
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
- private get absSize(): { w: number; h: number } {
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.5,
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.5,
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
- register('STOCKER', {
303
+ // STOCKER: 벽면에 용량 게이지 바를 포함하는 커스텀 3D 오브젝트
304
+ const STOCKER_CONFIG: MCS3DConfig = {
305
+ ...DEFAULTS,
302
306
  defaultDepth: 5 * TRANSPORT_SURFACE_HEIGHT,
303
- opacity: 0.35,
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
- register('ZoneCapacityBar', {
343
- depthRatio: 0.15,
344
- metalness: 0.2,
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
- register('MCSGaugeCapacityBar', {
357
- depthRatio: 0.15,
358
- metalness: 0.2,
359
- roughness: 0.8,
360
- defaultColor: 0x44aa44,
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)