@fmsim/machine 2.0.0-beta.86 → 2.0.0-beta.88

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.
Files changed (50) hide show
  1. package/dist/agv.js +4 -0
  2. package/dist/agv.js.map +1 -1
  3. package/dist/crane.js +142 -1
  4. package/dist/crane.js.map +1 -1
  5. package/dist/factories/crane-3d.js +45 -7
  6. package/dist/factories/crane-3d.js.map +1 -1
  7. package/dist/factories/machine-3d.js +12 -5
  8. package/dist/factories/machine-3d.js.map +1 -1
  9. package/dist/features/parent-object-mixin.js +15 -7
  10. package/dist/features/parent-object-mixin.js.map +1 -1
  11. package/dist/mcs-machine.js +11 -0
  12. package/dist/mcs-machine.js.map +1 -1
  13. package/dist/mcs-unit.js +10 -0
  14. package/dist/mcs-unit.js.map +1 -1
  15. package/dist/oht.js +4 -0
  16. package/dist/oht.js.map +1 -1
  17. package/dist/port-flow.js +4 -0
  18. package/dist/port-flow.js.map +1 -1
  19. package/dist/port.js +4 -0
  20. package/dist/port.js.map +1 -1
  21. package/dist/shelf.js +4 -0
  22. package/dist/shelf.js.map +1 -1
  23. package/dist/stocker-abnormal-bar.js +4 -0
  24. package/dist/stocker-abnormal-bar.js.map +1 -1
  25. package/dist/stocker-capacity-bar.js +4 -0
  26. package/dist/stocker-capacity-bar.js.map +1 -1
  27. package/dist/stocker-carrier-bar.js +4 -0
  28. package/dist/stocker-carrier-bar.js.map +1 -1
  29. package/dist/tsconfig.tsbuildinfo +1 -1
  30. package/dist/utils/crane-position.js +96 -0
  31. package/dist/utils/crane-position.js.map +1 -0
  32. package/dist/utils/fit-gauge-scale.js +28 -0
  33. package/dist/utils/fit-gauge-scale.js.map +1 -0
  34. package/package.json +3 -3
  35. package/src/agv.ts +5 -0
  36. package/src/crane.ts +181 -1
  37. package/src/factories/crane-3d.ts +50 -7
  38. package/src/factories/machine-3d.ts +14 -5
  39. package/src/features/parent-object-mixin.ts +16 -7
  40. package/src/mcs-machine.ts +13 -0
  41. package/src/mcs-unit.ts +12 -0
  42. package/src/oht.ts +5 -0
  43. package/src/port-flow.ts +5 -0
  44. package/src/port.ts +5 -0
  45. package/src/shelf.ts +5 -0
  46. package/src/stocker-abnormal-bar.ts +5 -0
  47. package/src/stocker-capacity-bar.ts +5 -0
  48. package/src/stocker-carrier-bar.ts +5 -0
  49. package/src/utils/crane-position.ts +115 -0
  50. package/src/utils/fit-gauge-scale.ts +35 -0
@@ -0,0 +1,96 @@
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
+ * current 값을 limit 범위 안에서 0~1 비율로 환산.
17
+ * limit 누락 / 0 span / 비정상 입력 시 0 반환 (안전 fallback).
18
+ */
19
+ export function axisRatio(value, lim) {
20
+ const v = Number(value);
21
+ const min = Number(lim === null || lim === void 0 ? void 0 : lim.min) || 0;
22
+ const max = Number(lim === null || lim === void 0 ? void 0 : lim.max);
23
+ if (!Number.isFinite(v) || !Number.isFinite(max) || max === min)
24
+ return 0;
25
+ return Math.max(0, Math.min(1, (v - min) / (max - min)));
26
+ }
27
+ /**
28
+ * host(부모) footprint 안에서 0~1 비율 (rx,ry) → 크레인 top-left 로컬 좌표.
29
+ *
30
+ * **스토커 안쪽 오프셋**: 크레인의 폭/두께를 감안해, 크레인 *전체* 가 host 안에
31
+ * 머무르도록 top-left 좌표를 [0, hostLen - craneLen] 범위로 매핑한다 (= 양끝에서
32
+ * craneLen/2 만큼 inset 된 중심선 위를 움직임). 크레인이 host 보다 크면 span 0
33
+ * 으로 떨어져 host 모서리에 정렬.
34
+ *
35
+ * x(rx) 는 host 의 긴쪽, y(ry) 는 짧은쪽에 매핑 (가상 rail 은 host 긴쪽 방향).
36
+ */
37
+ export function mapCraneLocalPosition(hostW, hostH, craneW, craneH, rx, ry) {
38
+ const longIsWidth = hostW >= hostH;
39
+ const longLen = longIsWidth ? hostW : hostH;
40
+ const shortLen = longIsWidth ? hostH : hostW;
41
+ const craneLongSize = longIsWidth ? craneW : craneH;
42
+ const craneShortSize = longIsWidth ? craneH : craneW;
43
+ // 크레인 크기를 뺀 inset 가용 span. top-left = ratio * span 이므로 크레인
44
+ // 전체가 host [0, hostLen] 안에 들어온다.
45
+ const longSpan = Math.max(0, longLen - craneLongSize);
46
+ const shortSpan = Math.max(0, shortLen - craneShortSize);
47
+ const longPos = rx * longSpan;
48
+ const shortPos = ry * shortSpan;
49
+ return {
50
+ left: longIsWidth ? longPos : shortPos,
51
+ top: longIsWidth ? shortPos : longPos
52
+ };
53
+ }
54
+ /**
55
+ * 객체에서 key 를 **대소문자 무관**으로 찾아 값 반환 (정확 매치 우선, 없으면 소문자 비교).
56
+ * 데이터 명세의 키 대소문자가 흔들려도(cranePosition / CRANEPOSITION / DURATION_MS …)
57
+ * 동일하게 읽기 위함. 매치 없으면 undefined.
58
+ */
59
+ export function ciGet(obj, key) {
60
+ if (!obj || typeof obj !== 'object')
61
+ return undefined;
62
+ if (key in obj)
63
+ return obj[key];
64
+ const lower = key.toLowerCase();
65
+ for (const k of Object.keys(obj)) {
66
+ if (k.toLowerCase() === lower)
67
+ return obj[k];
68
+ }
69
+ return undefined;
70
+ }
71
+ /**
72
+ * 객체 키를 재귀적으로 소문자화한 사본 반환 (배열/원시값은 그대로, 값은 변형 안 함).
73
+ * 중첩 키(current/limit/x/y/z/min/max)를 대소문자 무관으로 접근하기 위한 정규화.
74
+ */
75
+ export function lowerKeysDeep(value) {
76
+ if (Array.isArray(value))
77
+ return value.map(v => lowerKeysDeep(v));
78
+ if (value && typeof value === 'object') {
79
+ const out = {};
80
+ for (const k of Object.keys(value)) {
81
+ out[k.toLowerCase()] = lowerKeysDeep(value[k]);
82
+ }
83
+ return out;
84
+ }
85
+ return value;
86
+ }
87
+ /**
88
+ * raw cranePosition (키 대소문자 무관) → 캐노니컬 소문자 키 구조(CranePositionData).
89
+ * 이후 current.x / limit.x.min 등 소문자 키로 안전하게 접근 가능. 비객체면 undefined.
90
+ */
91
+ export function normalizeCranePosition(raw) {
92
+ if (!raw || typeof raw !== 'object')
93
+ return undefined;
94
+ return lowerKeysDeep(raw);
95
+ }
96
+ //# sourceMappingURL=crane-position.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"crane-position.js","sourceRoot":"","sources":["../../src/utils/crane-position.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * Crane 위치 데이터 (data.cranePosition) 변환 유틸.\n *\n * 명세:\n * {\n * cranePosition: {\n * current: { x, y, z }, // 실 좌표값\n * limit: { x:{min,max}, y:{min,max}, z:{min,max} } // 각 축 한계\n * }\n * }\n *\n * current 를 limit 범위 안의 0~1 비율로 환산한 뒤 host(부모) footprint 에 매핑한다.\n * x → host 긴쪽, y → 짧은쪽, z → 포크 높이(forkZ 0~100).\n */\n\n/** 축별 한계 (cranePosition.limit.{x|y|z}). */\nexport interface AxisLimit {\n min?: number\n max?: number\n}\n\n/** 새 데이터 명세 — data.cranePosition. */\nexport interface CranePositionData {\n current?: { x?: number; y?: number; z?: number }\n limit?: { x?: AxisLimit; y?: AxisLimit; z?: AxisLimit }\n}\n\n/**\n * current 값을 limit 범위 안에서 0~1 비율로 환산.\n * limit 누락 / 0 span / 비정상 입력 시 0 반환 (안전 fallback).\n */\nexport function axisRatio(value: unknown, lim?: AxisLimit): number {\n const v = Number(value)\n const min = Number(lim?.min) || 0\n const max = Number(lim?.max)\n if (!Number.isFinite(v) || !Number.isFinite(max) || max === min) return 0\n return Math.max(0, Math.min(1, (v - min) / (max - min)))\n}\n\n/**\n * host(부모) footprint 안에서 0~1 비율 (rx,ry) → 크레인 top-left 로컬 좌표.\n *\n * **스토커 안쪽 오프셋**: 크레인의 폭/두께를 감안해, 크레인 *전체* 가 host 안에\n * 머무르도록 top-left 좌표를 [0, hostLen - craneLen] 범위로 매핑한다 (= 양끝에서\n * craneLen/2 만큼 inset 된 중심선 위를 움직임). 크레인이 host 보다 크면 span 0\n * 으로 떨어져 host 모서리에 정렬.\n *\n * x(rx) 는 host 의 긴쪽, y(ry) 는 짧은쪽에 매핑 (가상 rail 은 host 긴쪽 방향).\n */\nexport function mapCraneLocalPosition(\n hostW: number,\n hostH: number,\n craneW: number,\n craneH: number,\n rx: number,\n ry: number\n): { left: number; top: number } {\n const longIsWidth = hostW >= hostH\n const longLen = longIsWidth ? hostW : hostH\n const shortLen = longIsWidth ? hostH : hostW\n const craneLongSize = longIsWidth ? craneW : craneH\n const craneShortSize = longIsWidth ? craneH : craneW\n\n // 크레인 크기를 뺀 inset 가용 span. top-left = ratio * span 이므로 크레인\n // 전체가 host [0, hostLen] 안에 들어온다.\n const longSpan = Math.max(0, longLen - craneLongSize)\n const shortSpan = Math.max(0, shortLen - craneShortSize)\n const longPos = rx * longSpan\n const shortPos = ry * shortSpan\n\n return {\n left: longIsWidth ? longPos : shortPos,\n top: longIsWidth ? shortPos : longPos\n }\n}\n\n/**\n * 객체에서 key 를 **대소문자 무관**으로 찾아 값 반환 (정확 매치 우선, 없으면 소문자 비교).\n * 데이터 명세의 키 대소문자가 흔들려도(cranePosition / CRANEPOSITION / DURATION_MS …)\n * 동일하게 읽기 위함. 매치 없으면 undefined.\n */\nexport function ciGet(obj: any, key: string): any {\n if (!obj || typeof obj !== 'object') return undefined\n if (key in obj) return obj[key]\n const lower = key.toLowerCase()\n for (const k of Object.keys(obj)) {\n if (k.toLowerCase() === lower) return obj[k]\n }\n return undefined\n}\n\n/**\n * 객체 키를 재귀적으로 소문자화한 사본 반환 (배열/원시값은 그대로, 값은 변형 안 함).\n * 중첩 키(current/limit/x/y/z/min/max)를 대소문자 무관으로 접근하기 위한 정규화.\n */\nexport function lowerKeysDeep<T = any>(value: any): T {\n if (Array.isArray(value)) return value.map(v => lowerKeysDeep(v)) as any\n if (value && typeof value === 'object') {\n const out: Record<string, any> = {}\n for (const k of Object.keys(value)) {\n out[k.toLowerCase()] = lowerKeysDeep(value[k])\n }\n return out as T\n }\n return value as T\n}\n\n/**\n * raw cranePosition (키 대소문자 무관) → 캐노니컬 소문자 키 구조(CranePositionData).\n * 이후 current.x / limit.x.min 등 소문자 키로 안전하게 접근 가능. 비객체면 undefined.\n */\nexport function normalizeCranePosition(raw: any): CranePositionData | undefined {\n if (!raw || typeof raw !== 'object') return undefined\n return lowerKeysDeep<CranePositionData>(raw)\n}\n"]}
@@ -0,0 +1,28 @@
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(w, h, maxW, maxH, margin = 0.85) {
20
+ const isWidthLonger = w >= h;
21
+ // 바 가로축(maxW)이 정렬되는 스토커 치수 = 장축, 세로축(maxH) = 단축
22
+ const availAlongW = isWidthLonger ? w : h;
23
+ const availAlongH = isWidthLonger ? h : w;
24
+ const scale = Math.min(1, (availAlongW * margin) / maxW, (availAlongH * margin) / maxH);
25
+ // 음수/NaN 방어 — 0 이하로 떨어지면 안 됨
26
+ return scale > 0 && Number.isFinite(scale) ? scale : 1;
27
+ }
28
+ //# sourceMappingURL=fit-gauge-scale.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fit-gauge-scale.js","sourceRoot":"","sources":["../../src/utils/fit-gauge-scale.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,UAAU,aAAa,CAC3B,CAAS,EACT,CAAS,EACT,IAAY,EACZ,IAAY,EACZ,SAAiB,IAAI;IAErB,MAAM,aAAa,GAAG,CAAC,IAAI,CAAC,CAAA;IAC5B,gDAAgD;IAChD,MAAM,WAAW,GAAG,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;IACzC,MAAM,WAAW,GAAG,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;IAEzC,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,WAAW,GAAG,MAAM,CAAC,GAAG,IAAI,EAAE,CAAC,WAAW,GAAG,MAAM,CAAC,GAAG,IAAI,CAAC,CAAA;IAEvF,6BAA6B;IAC7B,OAAO,KAAK,GAAG,CAAC,IAAI,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAA;AACxD,CAAC","sourcesContent":["/**\n * 스토커 지붕 위 용량 게이지 바의 비례 축소 계수를 계산한다.\n *\n * 정책:\n * - 게이지 바의 *최대* 크기는 maxW × maxH. 스토커가 충분히 크면 이 크기 그대로 (계수 1).\n * - 스토커가 그보다 작으면 지붕 footprint 안에 들어오도록 *비례* 축소 (uniform).\n * - *확대는 하지 않는다* (계수 상한 1) — 현재 크기가 상한이라는 요구사항.\n *\n * 방향 매핑: 바의 가로축(maxW)은 스토커의 *장축*에 정렬된다 (buildGauge 의\n * `isWidthLonger ? geo.rotateY` 처리와 일치). 따라서 가용 치수도 그 기준으로 매칭한다.\n *\n * @param w 스토커 footprint 가로 (월드 단위)\n * @param h 스토커 footprint 세로 (월드 단위)\n * @param maxW 게이지 바 최대 가로\n * @param maxH 게이지 바 최대 세로\n * @param margin 지붕 가장자리 여백 비율 (0~1, 기본 0.85)\n * @returns 0 < scale <= 1\n */\nexport function fitGaugeScale(\n w: number,\n h: number,\n maxW: number,\n maxH: number,\n margin: number = 0.85\n): number {\n const isWidthLonger = w >= h\n // 바 가로축(maxW)이 정렬되는 스토커 치수 = 장축, 세로축(maxH) = 단축\n const availAlongW = isWidthLonger ? w : h\n const availAlongH = isWidthLonger ? h : w\n\n const scale = Math.min(1, (availAlongW * margin) / maxW, (availAlongH * margin) / maxH)\n\n // 음수/NaN 방어 — 0 이하로 떨어지면 안 됨\n return scale > 0 && Number.isFinite(scale) ? scale : 1\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.86",
5
+ "version": "2.0.0-beta.88",
6
6
  "main": "dist/index.js",
7
7
  "module": "dist/index.js",
8
8
  "things-scene": true,
@@ -26,7 +26,7 @@
26
26
  "migration": "things-factory-migration"
27
27
  },
28
28
  "dependencies": {
29
- "@fmsim/api": "^2.0.0-beta.84",
29
+ "@fmsim/api": "^2.0.0-beta.88",
30
30
  "@hatiolab/things-scene": "^10.0.0-beta.1"
31
31
  },
32
32
  "devDependencies": {
@@ -55,5 +55,5 @@
55
55
  "prettier --write"
56
56
  ]
57
57
  },
58
- "gitHead": "2e7fe85f5e286c28c9fb3b5c095a23c8c150b41d"
58
+ "gitHead": "a85bfc6794b783dd619448e2cc07cca8eafbb467"
59
59
  }
package/src/agv.ts CHANGED
@@ -33,6 +33,11 @@ export default class AGV extends MCSVehicle {
33
33
  return 'black'
34
34
  }
35
35
 
36
+ /** AGV 의 parentObject 는 AGVLine. */
37
+ get parentObjectTypes(): string[] {
38
+ return ['AGVLine']
39
+ }
40
+
36
41
  getLegendFallback() {
37
42
  return (this.root as any).agvLegendTheme || super.getLegendFallback()
38
43
  }
package/src/crane.ts CHANGED
@@ -1,6 +1,13 @@
1
- import { Component, ComponentNature, sceneComponent } from '@hatiolab/things-scene'
1
+ import { Component, ComponentNature, sceneComponent, frameClock } from '@hatiolab/things-scene'
2
2
  import MCSVehicle from './mcs-vehicle'
3
3
  import { LEGEND_CRANE } from './features/mcs-status-default'
4
+ import {
5
+ axisRatio,
6
+ mapCraneLocalPosition,
7
+ ciGet,
8
+ normalizeCranePosition,
9
+ type CranePositionData
10
+ } from './utils/crane-position'
4
11
 
5
12
  const NATURE: ComponentNature = {
6
13
  mutable: false,
@@ -11,6 +18,29 @@ const NATURE: ComponentNature = {
11
18
 
12
19
  const GAP = 2
13
20
 
21
+ /** 좌표 보간 기본 duration (ms). data.DURATION_MS 가 없으면 이 값 사용. */
22
+ const DEFAULT_DURATION_MS = 1000
23
+
24
+ function lerp(a: number, b: number, t: number): number {
25
+ return a + (b - a) * t
26
+ }
27
+
28
+ function easeInOut(t: number): number {
29
+ return t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2
30
+ }
31
+
32
+ interface CranePositionAnimator {
33
+ fromLeft: number
34
+ fromTop: number
35
+ fromForkZ: number
36
+ toLeft: number
37
+ toTop: number
38
+ toForkZ: number
39
+ startTime: number
40
+ duration: number
41
+ unsubscribe: () => void
42
+ }
43
+
14
44
  @sceneComponent('Crane')
15
45
  export default class Crane extends MCSVehicle {
16
46
  static get nature() {
@@ -21,10 +51,160 @@ export default class Crane extends MCSVehicle {
21
51
  return 'black'
22
52
  }
23
53
 
54
+ /** Crane 의 parentObject 는 STOCKER. */
55
+ get parentObjectTypes(): string[] {
56
+ return ['STOCKER']
57
+ }
58
+
24
59
  getLegendFallback() {
25
60
  return (this.root as any).craneLegendTheme || super.getLegendFallback()
26
61
  }
27
62
 
63
+ private _posAnimator?: CranePositionAnimator
64
+
65
+ /**
66
+ * data.cranePosition = { current: {x,y,z}, limit: {x|y|z:{min,max}} } 분기.
67
+ * current 를 limit 범위 안의 0~1 비율로 환산한 뒤, 부모 (보통 Stocker) 기준으로
68
+ * 실 좌표 (state.left/top) 와 forkZ 로 변환해 lerp 시작. x→host 긴쪽, y→짧은쪽,
69
+ * z→포크 높이. 보간 중 새 값이 들어오면 *현재 보간 위치* 를 from 으로 잡고 새
70
+ * target 으로 새 lerp 시작.
71
+ * NODEID 경로는 super (MCSVehicle.onchangeData) 가 처리 — cranePosition 만 있는
72
+ * 데이터에선 NODEID 가 없어 자연스럽게 noop.
73
+ */
74
+ onchangeData(after: Record<string, any>, before: Record<string, any>): void {
75
+ const afterData = after?.data
76
+ // 데이터 명세 키 대소문자 무관: cranePosition / DURATION_MS 및 중첩 키(current/limit/x/y/z/min/max).
77
+ const afterCP = normalizeCranePosition(ciGet(afterData, 'cranePosition'))
78
+ if (afterCP) {
79
+ const beforeCP = normalizeCranePosition(ciGet(before?.data, 'cranePosition'))
80
+ if (this._cranePositionChanged(afterCP, beforeCP)) {
81
+ this._startPositionAnimation(afterCP, ciGet(afterData, 'DURATION_MS'))
82
+ }
83
+ }
84
+ super.onchangeData(after, before)
85
+ }
86
+
87
+ /** current 또는 limit 중 하나라도 바뀌면 재계산 필요 (limit 변화도 실 위치를 바꿈). */
88
+ private _cranePositionChanged(a: CranePositionData, b?: CranePositionData): boolean {
89
+ if (!b) return true
90
+ return (
91
+ JSON.stringify(a?.current ?? null) !== JSON.stringify(b?.current ?? null) ||
92
+ JSON.stringify(a?.limit ?? null) !== JSON.stringify(b?.limit ?? null)
93
+ )
94
+ }
95
+
96
+ dispose() {
97
+ if (this._posAnimator) {
98
+ this._posAnimator.unsubscribe()
99
+ this._posAnimator = undefined
100
+ }
101
+ super.dispose()
102
+ }
103
+
104
+ private _startPositionAnimation(cranePosition: CranePositionData, durationMs?: number): void {
105
+ const target = this._ratioToReal(cranePosition)
106
+ if (!target) return // 부모(host) 없거나 크기 0 이면 변환 skip
107
+
108
+ const now = frameClock.simTime
109
+ const duration = Math.max(1, Number(durationMs) || DEFAULT_DURATION_MS)
110
+
111
+ // 현재 위치 — 보간 중이면 그 시점 보간값을 from 으로
112
+ let fromLeft = Number(this.state.left) || 0
113
+ let fromTop = Number(this.state.top) || 0
114
+ let fromForkZ = Number((this.state as any).forkZ) || 0
115
+
116
+ if (this._posAnimator) {
117
+ const a = this._posAnimator
118
+ const t = Math.min(1, Math.max(0, (now - a.startTime) / a.duration))
119
+ const e = easeInOut(t)
120
+ fromLeft = lerp(a.fromLeft, a.toLeft, e)
121
+ fromTop = lerp(a.fromTop, a.toTop, e)
122
+ fromForkZ = lerp(a.fromForkZ, a.toForkZ, e)
123
+ a.unsubscribe()
124
+ this._posAnimator = undefined
125
+ }
126
+
127
+ const unsubscribe = frameClock.subscribe(
128
+ (_dt: number, simTime: number) => this._tickPositionAnimation(simTime),
129
+ { description: 'crane-position', component: this }
130
+ )
131
+
132
+ this._posAnimator = {
133
+ fromLeft,
134
+ fromTop,
135
+ fromForkZ,
136
+ toLeft: target.left,
137
+ toTop: target.top,
138
+ toForkZ: target.forkZ,
139
+ startTime: now,
140
+ duration,
141
+ unsubscribe
142
+ }
143
+ // 즉시 첫 frame 반영 (t=0) — set 한 번 호출. paused 환경에서도 from→target
144
+ // 진입 시점이 명확.
145
+ this._tickPositionAnimation(now)
146
+ }
147
+
148
+ private _tickPositionAnimation(simTime: number): void {
149
+ const a = this._posAnimator
150
+ if (!a) return
151
+ const t = Math.min(1, Math.max(0, (simTime - a.startTime) / a.duration))
152
+ const e = easeInOut(t)
153
+ this.set({
154
+ left: lerp(a.fromLeft, a.toLeft, e),
155
+ top: lerp(a.fromTop, a.toTop, e),
156
+ forkZ: lerp(a.fromForkZ, a.toForkZ, e)
157
+ })
158
+ if (t >= 1) {
159
+ a.unsubscribe()
160
+ this._posAnimator = undefined
161
+ }
162
+ }
163
+
164
+ /**
165
+ * cranePosition (current + limit) → state.left/top + forkZ.
166
+ *
167
+ * 부모 오브젝트 (보통은 Stocker 이지만 다른 컨테이너일 수도) 의 width/height
168
+ * 만 사용해 좌표 매핑 — 타입 무관 일반화. 가상 rail 은 host 의 긴쪽:
169
+ * - current.x → host 의 긴쪽 (자동 판별: max(width, height))
170
+ * - current.y → 짧은쪽
171
+ * - current.z → forkZ (state 에 0~100 으로 저장, 3D 측에서 mast 비율로 매핑)
172
+ *
173
+ * 각 축 비율 = (current - limit.min) / (limit.max - limit.min) ∈ [0,1].
174
+ *
175
+ * **스토커 안쪽 오프셋**: 크레인의 폭/두께를 감안해, 크레인 *전체* 가 host 안에
176
+ * 머무르도록 top-left 좌표를 [0, hostLen - craneLen] 범위로 매핑한다 (= 양끝에서
177
+ * craneLen/2 만큼 inset 된 중심선 위를 움직임). 크레인이 host 보다 크면 span 0
178
+ * 으로 떨어져 host 모서리에 정렬.
179
+ *
180
+ * 참조는 ParentObjectMixin 이 제공하는 `parentObject` 사용 —
181
+ * state.parentObjectId 명시 시 그것, 미명시 시 this.parent 자동 fallback.
182
+ */
183
+ private _ratioToReal(
184
+ cp: CranePositionData
185
+ ): { left: number; top: number; forkZ: number } | undefined {
186
+ const host: any = (this as any).parentObject
187
+ if (!host) return undefined
188
+
189
+ const hw = Number(host.state?.width) || 0
190
+ const hh = Number(host.state?.height) || 0
191
+ const cw = Number(this.state.width) || 0
192
+ const ch = Number(this.state.height) || 0
193
+ if (!hw || !hh) return undefined
194
+
195
+ const rx = axisRatio(cp.current?.x, cp.limit?.x)
196
+ const ry = axisRatio(cp.current?.y, cp.limit?.y)
197
+ const rz = axisRatio(cp.current?.z, cp.limit?.z)
198
+
199
+ const { left, top } = mapCraneLocalPosition(hw, hh, cw, ch, rx, ry)
200
+
201
+ return {
202
+ left,
203
+ top,
204
+ forkZ: rz * 100 // forkZ 0~100, 3D 측 (crane-3d) 에서 mast 비율로 매핑
205
+ }
206
+ }
207
+
28
208
  render(ctx: CanvasRenderingContext2D) {
29
209
  var { left, top, width, height } = this.bounds
30
210
  const { x: cx, y: cy } = this.center
@@ -22,11 +22,29 @@ const FORK_COLOR = 0x999999
22
22
  const DEFAULT_BODY_COLOR = 0xcc8844
23
23
 
24
24
  export class Crane3D extends RealObjectGroup {
25
+ /** 캐리지 포크 group — Crane.state.forkZ (0~100) 가 변하면 position.y 만 lerp.
26
+ * build() 시 inner 에 add 되고, *_mastBaseY 와 mastH 를 함께 보관* 해서
27
+ * setForkZ 가 비율 계산에 사용. */
28
+ private forkGroup?: THREE.Group
29
+ private forkMastBaseY = 0
30
+ private forkMastH = 0
31
+
25
32
  get effectiveDepth(): number {
26
33
  const { depth } = this.component.state
27
34
  return depth || 5 * TRANSPORT_SURFACE_HEIGHT - 2
28
35
  }
29
36
 
37
+ /**
38
+ * 0~100 정규화 z (= state.forkZ) 를 mast 높이 비율로 변환해서 포크 group 의
39
+ * three-up y 위치 갱신. build() 가 안 끝났으면 noop — build 가 forkZ 를 다시
40
+ * 반영하면서 끝난다.
41
+ */
42
+ applyForkZ(z: number): void {
43
+ if (!this.forkGroup) return
44
+ const clamped = Math.max(0, Math.min(100, Number(z) || 0))
45
+ this.forkGroup.position.y = this.forkMastBaseY + (clamped / 100) * this.forkMastH
46
+ }
47
+
30
48
  private resolveColor(): number {
31
49
  const comp = this.component as any
32
50
 
@@ -115,8 +133,10 @@ export class Crane3D extends RealObjectGroup {
115
133
  mastMesh.castShadow = true
116
134
  inner.add(mastMesh)
117
135
 
118
- // ── 3. 캐리지 포크 (마스트 40% 높이, 양쪽으로 뻗는 포크 암) ──
119
- const forkY = mastBaseY + mastH * 0.4
136
+ // ── 3. 캐리지 포크 (별도 group — state.forkZ 변경 시 group.position.y lerp) ──
137
+ // 포크 geometry 는 *_mastBaseY 기준 local 0 에 둠* — 즉 group.position.y = 0
138
+ // 일 때 mast 바닥 자리에. forkZ 0~100 에 따라 group.position.y 만 변경하면
139
+ // mast 따라 위/아래 이동. mesh 재구축 불필요.
120
140
  const forkMat = new THREE.MeshStandardMaterial({
121
141
  color: FORK_COLOR,
122
142
  roughness: 0.4,
@@ -125,15 +145,13 @@ export class Crane3D extends RealObjectGroup {
125
145
 
126
146
  const forkGeometries: THREE.BufferGeometry[] = []
127
147
 
128
- // 캐리지 중심 플레이트 (마스트에 부착, 양쪽 포크의 베이스)
129
148
  const plateW = w * 0.9
130
149
  const plateH = d * 0.08
131
150
  const plateD = pillarThick * 0.8
132
151
  const centerPlate = new THREE.BoxGeometry(plateW, plateH, plateD)
133
- centerPlate.translate(0, forkY + plateH / 2, 0)
152
+ centerPlate.translate(0, plateH / 2, 0) // local 0 = mast 바닥
134
153
  forkGeometries.push(centerPlate)
135
154
 
136
- // 양쪽 포크 암 (좌/우 선반 방향으로 뻗음)
137
155
  const armW = pillarThick * 0.6
138
156
  const armH = plateH * 0.4
139
157
  const armD = h * 0.6
@@ -142,14 +160,27 @@ export class Crane3D extends RealObjectGroup {
142
160
  for (const zSign of [-1, 1]) {
143
161
  for (const xSign of [-1, 1]) {
144
162
  const arm = new THREE.BoxGeometry(armW, armH, armD)
145
- arm.translate(xSign * (armSpacing / 2), forkY + armH / 2, zSign * (plateD / 2 + armD / 2))
163
+ arm.translate(xSign * (armSpacing / 2), armH / 2, zSign * (plateD / 2 + armD / 2))
146
164
  forkGeometries.push(arm)
147
165
  }
148
166
  }
149
167
 
150
168
  const forkMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(forkGeometries), forkMat)
151
169
  forkMesh.castShadow = true
152
- inner.add(forkMesh)
170
+
171
+ const forkGroup = new THREE.Group()
172
+ forkGroup.add(forkMesh)
173
+ inner.add(forkGroup)
174
+ this.forkGroup = forkGroup
175
+ this.forkMastBaseY = mastBaseY
176
+ // forkZ=100 시점에 mast 의 *_위쪽 한계*. 포크 자체 두께 (plateH) 만큼은 마스트
177
+ // 안에 머무르도록 limit 보정 — 그렇지 않으면 forkZ=100 일 때 포크가 상단
178
+ // 클램프와 충돌. mastH * 0.92 로 약간 보수적 (= 마스트 본체 92% 까지만 이동).
179
+ this.forkMastH = mastH * 0.92
180
+
181
+ // 초기 forkZ 반영 — state.forkZ 가 이미 set 되어 있으면 그 위치에 배치
182
+ const initialForkZ = Number((this.component.state as any).forkZ)
183
+ this.applyForkZ(Number.isFinite(initialForkZ) ? initialForkZ : 0)
153
184
 
154
185
  // ── 4. 상단 클램프 (천장 레일에 물리는 가이드부) ──
155
186
  const topClampGeo = new THREE.BoxGeometry(clampW, clampH, clampD)
@@ -162,11 +193,23 @@ export class Crane3D extends RealObjectGroup {
162
193
  updateDimension() {}
163
194
  updateAlpha() {}
164
195
 
196
+ /** build() 직전 호출. forkGroup 참조를 깨끗이 비워둔다 — 새 build 가 다시 set. */
197
+ clear() {
198
+ this.forkGroup = undefined
199
+ super.clear()
200
+ }
201
+
165
202
  onchange(after: Record<string, unknown>, before: Record<string, unknown>) {
166
203
  if ('width' in after || 'height' in after || 'depth' in after || 'fillStyle' in after || 'strokeStyle' in after) {
167
204
  this.update()
168
205
  return
169
206
  }
207
+ // forkZ 는 mesh 재구축 없이 group.position.y 만 갱신. 같은 onchange 에서
208
+ // left/top 도 함께 변경될 수 있으므로 super 도 반드시 호출해 본체 transform
209
+ // 갱신 path (state.left/top → obj3d.position.x/z) 가 동작하게 함.
210
+ if ('forkZ' in after) {
211
+ this.applyForkZ(Number(after.forkZ) || 0)
212
+ }
170
213
  super.onchange(after, before)
171
214
  }
172
215
  }
@@ -14,6 +14,7 @@ import { RealObjectGroup, registerRealObjectFactory, disposeObject3D } from '@ha
14
14
  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
+ import { fitGaugeScale } from '../utils/fit-gauge-scale'
17
18
  import { Z_FMSIM } from './z-priority'
18
19
 
19
20
  // ── 설정 타입 ────────────────────────────────────────────
@@ -667,12 +668,20 @@ class StockerRealObject3D extends MCSRealObject3D {
667
668
  const depth = this.effectiveDepth
668
669
  const isWidthLonger = w >= h
669
670
 
670
- // 게이지 캔버스 (고해상도)
671
- const barW = 130
672
- const barH = 26
671
+ // 게이지 최대 크기 (스토커가 충분히 크면 이 크기 그대로 사용).
672
+ const MAX_BAR_W = 130
673
+ const MAX_BAR_H = 26
674
+
675
+ // 스토커 지붕 footprint 에 맞춰 비례 축소 (확대는 안 함 — 현재 크기가 상한).
676
+ const fit = fitGaugeScale(w, h, MAX_BAR_W, MAX_BAR_H)
677
+
678
+ // PlaneGeometry 의 월드 크기는 clamp, 텍스처 캔버스 해상도는 최대 크기 기준 고정
679
+ // (축소되어도 선명도 유지).
680
+ const barW = MAX_BAR_W * fit
681
+ const barH = MAX_BAR_H * fit
673
682
  const scale = 4
674
- const canvasW = barW * scale
675
- const canvasH = barH * scale
683
+ const canvasW = MAX_BAR_W * scale
684
+ const canvasH = MAX_BAR_H * scale
676
685
  const canvas = document.createElement('canvas')
677
686
  canvas.width = canvasW
678
687
  canvas.height = canvasH
@@ -4,13 +4,12 @@ import { Component } from '@hatiolab/things-scene'
4
4
  import MCSMachine from '../mcs-machine'
5
5
  import MCSUnit from '../mcs-unit'
6
6
 
7
- export const ParentObjectMixinProperties = [
8
- {
9
- type: 'string',
10
- label: 'parent-object-id',
11
- name: 'parentObjectId'
12
- }
13
- ]
7
+ /**
8
+ * parentObjectId 는 generic property builder 가 아니라 property-sidebar 의 모델 참조
9
+ * 룩업(ox-input-entity-id, parentObjectTypes 기준 씬 머신/유닛 선택)으로 렌더한다.
10
+ * 따라서 NATURE 속성 목록에서는 제외 — 빈 배열.
11
+ */
12
+ export const ParentObjectMixinProperties: any[] = []
14
13
 
15
14
  export function ParentObjectMixin<TBase extends Constructor<Component>>(Base: TBase): any {
16
15
  return class extends Base {
@@ -32,5 +31,15 @@ export function ParentObjectMixin<TBase extends Constructor<Component>>(Base: TB
32
31
  return parent
33
32
  }
34
33
  }
34
+
35
+ /**
36
+ * parentObjectId 가 참조하는 모델 컴포넌트 타입들 — parentObject 후보를 모델(씬)에서
37
+ * 수집할 때 이 타입들로 거른다 (머신/유닛 무관, 복수 허용).
38
+ * 빈 배열(기본) 이면 모델의 모든 머신 (폴백). 참조 타입이 정해진 컴포넌트는 override:
39
+ * AGV→['AGVLine'], OHT→['OHTLine'], Crane/Shelf→['STOCKER'], PortFlow→['Port'].
40
+ */
41
+ get parentObjectTypes(): string[] {
42
+ return []
43
+ }
35
44
  }
36
45
  }
@@ -26,6 +26,19 @@ export default class MCSMachine extends MCSStatusMixin(ContainerAbstract) {
26
26
  return false
27
27
  }
28
28
 
29
+ /**
30
+ * id 후보를 조회할 때 백엔드에 보낼 타입명. 값이 있으면 검색(lookup) 대상.
31
+ * 기본은 자기 타입. 자기 타입과 다른 타입으로 조회해야 하는 특수 케이스는 override.
32
+ */
33
+ get idLookupType(): string | undefined {
34
+ return this.state.type
35
+ }
36
+
37
+ /** MCS 머신 — parentObject(부모 머신) 후보 수집 시 머신으로 식별. */
38
+ get isMcsMachine(): boolean {
39
+ return true
40
+ }
41
+
29
42
  containable(component: Component) {
30
43
  return ['rect', 'ellipse'].includes(component.state.type)
31
44
  }
package/src/mcs-unit.ts CHANGED
@@ -13,6 +13,18 @@ export default class MCSUnit extends MCSStatusMixin(ParentObjectMixin(ContainerA
13
13
  return [...MCSStatusMixinProperties, ...ParentObjectMixinProperties]
14
14
  }
15
15
 
16
+ get showMoveHandle() {
17
+ return false
18
+ }
19
+
20
+ /**
21
+ * id 후보를 조회할 때 백엔드에 보낼 타입명. 값이 있으면 검색(lookup) 대상.
22
+ * 기본은 자기 타입. 특수 케이스는 override.
23
+ */
24
+ get idLookupType(): string | undefined {
25
+ return this.state.type
26
+ }
27
+
16
28
  get hasTextProperty() {
17
29
  return true
18
30
  }
package/src/oht.ts CHANGED
@@ -20,6 +20,11 @@ export default class OHT extends MCSVehicle {
20
20
  return 'black'
21
21
  }
22
22
 
23
+ /** OHT 의 parentObject 는 OHTLine. */
24
+ get parentObjectTypes(): string[] {
25
+ return ['OHTLine']
26
+ }
27
+
23
28
  getLegendFallback() {
24
29
  return (this.root as any).ohtLegendTheme || super.getLegendFallback()
25
30
  }
package/src/port-flow.ts CHANGED
@@ -45,6 +45,11 @@ export default class PortFlow extends ParentObjectMixin(RectPath(Shape)) {
45
45
  return NATURE
46
46
  }
47
47
 
48
+ /** PortFlow 의 parentObject 는 Port (유닛). */
49
+ get parentObjectTypes(): string[] {
50
+ return ['Port']
51
+ }
52
+
48
53
  get hasTextProperty() {
49
54
  return this.getState('showText') || false
50
55
  }
package/src/port.ts CHANGED
@@ -23,6 +23,11 @@ export default class Port extends MCSZoneMixin(MCSCarrierHolder) {
23
23
  return (this.root as any).portLegendTheme || super.getLegendFallback()
24
24
  }
25
25
 
26
+ /** Port 는 후보가 매우 많아 부모 머신 없이는 id 리스트를 조회하지 않는다 (machine 필수). */
27
+ get idLookupRequiresMachine() {
28
+ return true
29
+ }
30
+
26
31
  render(ctx: CanvasRenderingContext2D) {
27
32
  var { left, top, width, height } = this.bounds
28
33
 
package/src/shelf.ts CHANGED
@@ -19,6 +19,11 @@ export default class Shelf extends MCSZoneMixin(MCSCarrierHolder) {
19
19
  return 'black'
20
20
  }
21
21
 
22
+ /** Shelf 의 parentObject 는 STOCKER. */
23
+ get parentObjectTypes(): string[] {
24
+ return ['STOCKER']
25
+ }
26
+
22
27
  getLegendFallback() {
23
28
  return (this.root as any).shelfLegendTheme || super.getLegendFallback()
24
29
  }