@fmsim/machine 2.0.0-beta.92 → 2.0.0-beta.93

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.
@@ -12,6 +12,7 @@
12
12
  * current 를 limit 범위 안의 0~1 비율로 환산한 뒤 host(부모) footprint 에 매핑한다.
13
13
  * x → host 긴쪽, y → 짧은쪽, z → 포크 높이(forkZ 0~100).
14
14
  */
15
+ import { parseIfJsonString } from './data-value';
15
16
  /**
16
17
  * current 값을 limit 범위 안에서 0~1 비율로 환산.
17
18
  * limit 누락 / 0 span / 비정상 입력 시 0 반환 (안전 fallback).
@@ -69,12 +70,14 @@ export function lowerKeysDeep(value) {
69
70
  return value;
70
71
  }
71
72
  /**
72
- * raw cranePosition (키 대소문자 무관) → 캐노니컬 소문자 키 구조(CranePositionData).
73
- * 이후 current.x / limit.x.min 등 소문자 키로 안전하게 접근 가능. 비객체면 undefined.
73
+ * raw CRANEPOSITION → 내부 표준형(소문자 키) 구조(CranePositionData).
74
+ * 값이 JSON 문자열로 올 수 있어 먼저 파싱하고, 중첩 키(CURRENT/LIMIT/X/Y/Z/MIN/MAX)를
75
+ * 소문자(current.x / limit.x.min)로 변환. 파싱 실패/비객체면 undefined.
74
76
  */
75
77
  export function normalizeCranePosition(raw) {
76
- if (!raw || typeof raw !== 'object')
78
+ const obj = parseIfJsonString(raw);
79
+ if (!obj || typeof obj !== 'object')
77
80
  return undefined;
78
- return lowerKeysDeep(raw);
81
+ return lowerKeysDeep(obj);
79
82
  }
80
83
  //# sourceMappingURL=crane-position.js.map
@@ -1 +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 * 객체 키를 재귀적으로 소문자화한 사본 반환 (배열/원시값은 그대로, 값은 변형 안 함).\n * 데이터 키는 모두 대문자 전제 — CRANEPOSITION 의 중첩 키(CURRENT/LIMIT/X/Y/Z/MIN/MAX)를\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"]}
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\nimport { parseIfJsonString } from './data-value'\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 * 객체 키를 재귀적으로 소문자화한 사본 반환 (배열/원시값은 그대로, 값은 변형 안 함).\n * 데이터 키는 모두 대문자 전제 — CRANEPOSITION 의 중첩 키(CURRENT/LIMIT/X/Y/Z/MIN/MAX)를\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 * 값이 JSON 문자열로 올 수 있어 먼저 파싱하고, 중첩 키(CURRENT/LIMIT/X/Y/Z/MIN/MAX)를\n * 소문자(current.x / limit.x.min)로 변환. 파싱 실패/비객체면 undefined.\n */\nexport function normalizeCranePosition(raw: any): CranePositionData | undefined {\n const obj = parseIfJsonString(raw)\n if (!obj || typeof obj !== 'object') return undefined\n return lowerKeysDeep<CranePositionData>(obj)\n}\n"]}
@@ -0,0 +1,21 @@
1
+ /*
2
+ * Copyright © HatioLab Inc. All rights reserved.
3
+ *
4
+ * 데이터 필드값이 JSON 문자열로 전달될 수 있어(예: CRANEPOSITION, MULTICARRIERS)
5
+ * 객체/배열로 변환하기 위한 헬퍼.
6
+ */
7
+ /**
8
+ * 값이 JSON 문자열이면 파싱해서 반환. 객체/배열 등 비문자열은 그대로 반환.
9
+ * 파싱 실패 시 undefined.
10
+ */
11
+ export function parseIfJsonString(value) {
12
+ if (typeof value !== 'string')
13
+ return value;
14
+ try {
15
+ return JSON.parse(value);
16
+ }
17
+ catch (_a) {
18
+ return undefined;
19
+ }
20
+ }
21
+ //# sourceMappingURL=data-value.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"data-value.js","sourceRoot":"","sources":["../../src/utils/data-value.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH;;;GAGG;AACH,MAAM,UAAU,iBAAiB,CAAC,KAAU;IAC1C,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAA;IAC3C,IAAI,CAAC;QACH,OAAO,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAA;IAC1B,CAAC;IAAC,WAAM,CAAC;QACP,OAAO,SAAS,CAAA;IAClB,CAAC;AACH,CAAC","sourcesContent":["/*\n * Copyright © HatioLab Inc. All rights reserved.\n *\n * 데이터 필드값이 JSON 문자열로 전달될 수 있어(예: CRANEPOSITION, MULTICARRIERS)\n * 객체/배열로 변환하기 위한 헬퍼.\n */\n\n/**\n * 값이 JSON 문자열이면 파싱해서 반환. 객체/배열 등 비문자열은 그대로 반환.\n * 파싱 실패 시 undefined.\n */\nexport function parseIfJsonString(value: any): any {\n if (typeof value !== 'string') return value\n try {\n return JSON.parse(value)\n } catch {\n return undefined\n }\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.92",
5
+ "version": "2.0.0-beta.93",
6
6
  "main": "dist/index.js",
7
7
  "module": "dist/index.js",
8
8
  "things-scene": true,
@@ -55,5 +55,5 @@
55
55
  "prettier --write"
56
56
  ]
57
57
  },
58
- "gitHead": "82db9032ea3045b862bc22f901bc5dba3c98f34b"
58
+ "gitHead": "05f75ee9679e0122f38320c1c7fb19de62ff857d"
59
59
  }
@@ -1,6 +1,7 @@
1
1
  import { Model } from '@hatiolab/things-scene'
2
2
  import type Carrier from './carrier'
3
3
  import type MCSCarrierHolder from './mcs-carrier-holder'
4
+ import { parseIfJsonString } from './utils/data-value'
4
5
 
5
6
  // 사라진 carrier 의 dispose 지연. 같은 macrotask 안에서 holder A 의 사라짐
6
7
  // 보고와 holder B 의 등장 보고가 순차로 들어오는 reparent 시나리오 (= 같은
@@ -136,8 +137,10 @@ export default class CarrierManager {
136
137
 
137
138
  const stack: CarrierStackItem[] = []
138
139
 
139
- if (Array.isArray(data.MULTICARRIERS)) {
140
- for (const a of data.MULTICARRIERS) {
140
+ // MULTICARRIERS 값이 JSON 문자열로 올 수 있어 파싱 후 처리.
141
+ const list = parseIfJsonString(data.MULTICARRIERS)
142
+ if (Array.isArray(list)) {
143
+ for (const a of list) {
141
144
  if (a && typeof a === 'object' && a.CARRIERNAME) {
142
145
  // 항목 *원본 그대로* — 외부 시스템이 임의 키를 추가로 보낼 수 있고,
143
146
  // 누락된 4 키는 default 없이 그대로 비워둔다 (carrier.data 에 흘리는
@@ -1,5 +1,6 @@
1
1
  import { themesColorMap, themesAnimationMap } from '@fmsim/api'
2
2
  import MCSUnit from './mcs-unit.js'
3
+ import { parseIfJsonString } from './utils/data-value'
3
4
 
4
5
  /**
5
6
  * Holder.data 변경 감지 시 carrierManager 에 전달할 4 키 묶음 + multiCarriers 배열.
@@ -9,9 +10,12 @@ import MCSUnit from './mcs-unit.js'
9
10
  const CARRIER_DATA_KEYS = ['CARRIERNAME', 'CARRIERTYPE', 'EMPTYTYPE', 'CARRIERSTATUS'] as const
10
11
 
11
12
  function multiCarriersChanged(a: any, b: any): boolean {
12
- // shallow JSON 비교로 충분 — 각 항목이 4 키 primitive 만 가짐. 깊은 ref 비교
13
- // 보다 빠르고 정확. 빈/누락은 같은 '[]' 직렬화로 동치 취급.
14
- return JSON.stringify(a?.MULTICARRIERS ?? []) !== JSON.stringify(b?.MULTICARRIERS ?? [])
13
+ // MULTICARRIERS 값이 JSON 문자열로 올 수 있어 파싱 후 비교. shallow JSON 비교로 충분 —
14
+ // 각 항목이 4 키 primitive 만 가짐. 빈/누락은 같은 '[]' 직렬화로 동치 취급.
15
+ return (
16
+ JSON.stringify(parseIfJsonString(a?.MULTICARRIERS) ?? []) !==
17
+ JSON.stringify(parseIfJsonString(b?.MULTICARRIERS) ?? [])
18
+ )
15
19
  }
16
20
 
17
21
  export default class MCSCarrierHolder extends MCSUnit {
@@ -94,7 +98,8 @@ export default class MCSCarrierHolder extends MCSUnit {
94
98
  private _hasCarrierData(data: any): boolean {
95
99
  if (!data) return false
96
100
  if (data.CARRIERNAME) return true
97
- if (Array.isArray(data.MULTICARRIERS) && data.MULTICARRIERS.some((a: any) => a?.CARRIERNAME)) return true
101
+ const list = parseIfJsonString(data.MULTICARRIERS)
102
+ if (Array.isArray(list) && list.some((a: any) => a?.CARRIERNAME)) return true
98
103
  return false
99
104
  }
100
105
  }
@@ -13,6 +13,8 @@
13
13
  * x → host 긴쪽, y → 짧은쪽, z → 포크 높이(forkZ 0~100).
14
14
  */
15
15
 
16
+ import { parseIfJsonString } from './data-value'
17
+
16
18
  /** 축별 한계 (cranePosition.limit.{x|y|z}). */
17
19
  export interface AxisLimit {
18
20
  min?: number
@@ -92,10 +94,12 @@ export function lowerKeysDeep<T = any>(value: any): T {
92
94
  }
93
95
 
94
96
  /**
95
- * raw cranePosition (키 대소문자 무관) → 캐노니컬 소문자 키 구조(CranePositionData).
96
- * 이후 current.x / limit.x.min 등 소문자 키로 안전하게 접근 가능. 비객체면 undefined.
97
+ * raw CRANEPOSITION → 내부 표준형(소문자 키) 구조(CranePositionData).
98
+ * 값이 JSON 문자열로 올 수 있어 먼저 파싱하고, 중첩 키(CURRENT/LIMIT/X/Y/Z/MIN/MAX)를
99
+ * 소문자(current.x / limit.x.min)로 변환. 파싱 실패/비객체면 undefined.
97
100
  */
98
101
  export function normalizeCranePosition(raw: any): CranePositionData | undefined {
99
- if (!raw || typeof raw !== 'object') return undefined
100
- return lowerKeysDeep<CranePositionData>(raw)
102
+ const obj = parseIfJsonString(raw)
103
+ if (!obj || typeof obj !== 'object') return undefined
104
+ return lowerKeysDeep<CranePositionData>(obj)
101
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
+ }