@fmsim/machine 2.0.0-beta.87 → 2.0.0-beta.89
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 +4 -0
- package/dist/agv.js.map +1 -1
- package/dist/carrier-manager.js +10 -4
- package/dist/carrier-manager.js.map +1 -1
- package/dist/crane.js +49 -41
- package/dist/crane.js.map +1 -1
- package/dist/factories/machine-3d.js +12 -5
- package/dist/factories/machine-3d.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/mcs-carrier-holder.js +13 -10
- package/dist/mcs-carrier-holder.js.map +1 -1
- package/dist/mcs-machine.js +11 -0
- package/dist/mcs-machine.js.map +1 -1
- package/dist/mcs-unit.js +10 -0
- package/dist/mcs-unit.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/shelf.js +4 -0
- package/dist/shelf.js.map +1 -1
- package/dist/stocker-abnormal-bar.js +4 -0
- package/dist/stocker-abnormal-bar.js.map +1 -1
- package/dist/stocker-capacity-bar.js +4 -0
- package/dist/stocker-capacity-bar.js.map +1 -1
- package/dist/stocker-carrier-bar.js +4 -0
- package/dist/stocker-carrier-bar.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/utils/carrier-data.js +55 -0
- package/dist/utils/carrier-data.js.map +1 -0
- package/dist/utils/crane-position.js +96 -0
- package/dist/utils/crane-position.js.map +1 -0
- package/dist/utils/fit-gauge-scale.js +28 -0
- package/dist/utils/fit-gauge-scale.js.map +1 -0
- package/package.json +3 -3
- package/src/agv.ts +5 -0
- package/src/carrier-manager.ts +11 -4
- package/src/crane.ts +57 -47
- package/src/factories/machine-3d.ts +14 -5
- package/src/features/parent-object-mixin.ts +16 -7
- package/src/mcs-carrier-holder.ts +13 -10
- package/src/mcs-machine.ts +13 -0
- package/src/mcs-unit.ts +12 -0
- package/src/oht.ts +5 -0
- package/src/port-flow.ts +5 -0
- package/src/port.ts +5 -0
- package/src/shelf.ts +5 -0
- package/src/stocker-abnormal-bar.ts +5 -0
- package/src/stocker-capacity-bar.ts +5 -0
- package/src/stocker-carrier-bar.ts +5 -0
- package/src/utils/carrier-data.ts +57 -0
- package/src/utils/crane-position.ts +115 -0
- package/src/utils/fit-gauge-scale.ts +35 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright © HatioLab Inc. All rights reserved.
|
|
3
|
+
*
|
|
4
|
+
* Carrier 데이터 키 정규화 — 데이터 명세의 키 대소문자가 흔들려도(CARRIERNAME /
|
|
5
|
+
* carrierName / multiCarriers / MULTICARRIERS …) 동일하게 읽기 위함.
|
|
6
|
+
*
|
|
7
|
+
* 성능: 케이스 무관 처리를 "매 읽기"가 아니라 **데이터 진입 경계에서 1회**만 수행.
|
|
8
|
+
* 이후 다운스트림은 캐노니컬(대문자) 키를 평범한 O(1) 직접 접근으로 읽는다.
|
|
9
|
+
*/
|
|
10
|
+
import { ciGet } from './crane-position';
|
|
11
|
+
/** 다중 carrier 배열의 캐노니컬 키. (구 명세의 ADDITIONS 는 더 이상 지원하지 않음) */
|
|
12
|
+
export const MULTI_CARRIER_KEY = 'multiCarriers';
|
|
13
|
+
/** carrier 식별 4 키 — 캐노니컬(대문자) 표기. */
|
|
14
|
+
const CARRIER_FIELDS = ['CARRIERNAME', 'CARRIERTYPE', 'EMPTYTYPE', 'CARRIERSTATUS'];
|
|
15
|
+
const FIELD_BY_LOWER = new Map(CARRIER_FIELDS.map(f => [f.toLowerCase(), f]));
|
|
16
|
+
/**
|
|
17
|
+
* carrier 객체의 키를 캐노니컬(대문자)로 승격 — 식별 4 키만 매핑하고 값/기타 키는 보존.
|
|
18
|
+
* 대소문자 무관 (carrierName → CARRIERNAME). 비객체면 그대로 반환.
|
|
19
|
+
*/
|
|
20
|
+
export function normalizeCarrierItem(raw) {
|
|
21
|
+
var _a;
|
|
22
|
+
if (!raw || typeof raw !== 'object')
|
|
23
|
+
return raw;
|
|
24
|
+
const out = {};
|
|
25
|
+
for (const k of Object.keys(raw)) {
|
|
26
|
+
out[(_a = FIELD_BY_LOWER.get(k.toLowerCase())) !== null && _a !== void 0 ? _a : k] = raw[k];
|
|
27
|
+
}
|
|
28
|
+
return out;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* holder.data 정규화 (진입 경계 1회):
|
|
32
|
+
* - 대표 carrier 4 키를 캐노니컬로 승격
|
|
33
|
+
* - 다중 carrier 배열을 캐노니컬 키 `multiCarriers` 로 통일 (대소문자 무관), 각 항목도 정규화
|
|
34
|
+
*
|
|
35
|
+
* 비객체면 그대로 반환. 정규화 결과는 idempotent (이미 캐노니컬이면 동일 형태).
|
|
36
|
+
*/
|
|
37
|
+
export function normalizeCarrierData(raw) {
|
|
38
|
+
var _a;
|
|
39
|
+
if (!raw || typeof raw !== 'object')
|
|
40
|
+
return raw;
|
|
41
|
+
const multiLower = MULTI_CARRIER_KEY.toLowerCase();
|
|
42
|
+
const out = {};
|
|
43
|
+
for (const k of Object.keys(raw)) {
|
|
44
|
+
// 다중 carrier 배열 키는 아래에서 캐노니컬로 통일 처리하므로 여기선 건너뜀.
|
|
45
|
+
if (k.toLowerCase() === multiLower)
|
|
46
|
+
continue;
|
|
47
|
+
out[(_a = FIELD_BY_LOWER.get(k.toLowerCase())) !== null && _a !== void 0 ? _a : k] = raw[k];
|
|
48
|
+
}
|
|
49
|
+
const list = ciGet(raw, MULTI_CARRIER_KEY);
|
|
50
|
+
if (Array.isArray(list)) {
|
|
51
|
+
out[MULTI_CARRIER_KEY] = list.map(normalizeCarrierItem);
|
|
52
|
+
}
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=carrier-data.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"carrier-data.js","sourceRoot":"","sources":["../../src/utils/carrier-data.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAA;AAExC,8DAA8D;AAC9D,MAAM,CAAC,MAAM,iBAAiB,GAAG,eAAe,CAAA;AAEhD,qCAAqC;AACrC,MAAM,cAAc,GAAG,CAAC,aAAa,EAAE,aAAa,EAAE,WAAW,EAAE,eAAe,CAAU,CAAA;AAC5F,MAAM,cAAc,GAAG,IAAI,GAAG,CAAC,cAAc,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAA;AAE7E;;;GAGG;AACH,MAAM,UAAU,oBAAoB,CAAC,GAAQ;;IAC3C,IAAI,CAAC,GAAG,IAAI,OAAO,GAAG,KAAK,QAAQ;QAAE,OAAO,GAAG,CAAA;IAC/C,MAAM,GAAG,GAAQ,EAAE,CAAA;IACnB,KAAK,MAAM,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;QACjC,GAAG,CAAC,MAAA,cAAc,CAAC,GAAG,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,mCAAI,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,CAAA;IACxD,CAAC;IACD,OAAO,GAAG,CAAA;AACZ,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,oBAAoB,CAAC,GAAQ;;IAC3C,IAAI,CAAC,GAAG,IAAI,OAAO,GAAG,KAAK,QAAQ;QAAE,OAAO,GAAG,CAAA;IAE/C,MAAM,UAAU,GAAG,iBAAiB,CAAC,WAAW,EAAE,CAAA;IAClD,MAAM,GAAG,GAAQ,EAAE,CAAA;IACnB,KAAK,MAAM,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;QACjC,gDAAgD;QAChD,IAAI,CAAC,CAAC,WAAW,EAAE,KAAK,UAAU;YAAE,SAAQ;QAC5C,GAAG,CAAC,MAAA,cAAc,CAAC,GAAG,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,mCAAI,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,CAAA;IACxD,CAAC;IAED,MAAM,IAAI,GAAG,KAAK,CAAC,GAAG,EAAE,iBAAiB,CAAC,CAAA;IAC1C,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;QACxB,GAAG,CAAC,iBAAiB,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,oBAAoB,CAAC,CAAA;IACzD,CAAC;IAED,OAAO,GAAG,CAAA;AACZ,CAAC","sourcesContent":["/*\n * Copyright © HatioLab Inc. All rights reserved.\n *\n * Carrier 데이터 키 정규화 — 데이터 명세의 키 대소문자가 흔들려도(CARRIERNAME /\n * carrierName / multiCarriers / MULTICARRIERS …) 동일하게 읽기 위함.\n *\n * 성능: 케이스 무관 처리를 \"매 읽기\"가 아니라 **데이터 진입 경계에서 1회**만 수행.\n * 이후 다운스트림은 캐노니컬(대문자) 키를 평범한 O(1) 직접 접근으로 읽는다.\n */\n\nimport { ciGet } from './crane-position'\n\n/** 다중 carrier 배열의 캐노니컬 키. (구 명세의 ADDITIONS 는 더 이상 지원하지 않음) */\nexport const MULTI_CARRIER_KEY = 'multiCarriers'\n\n/** carrier 식별 4 키 — 캐노니컬(대문자) 표기. */\nconst CARRIER_FIELDS = ['CARRIERNAME', 'CARRIERTYPE', 'EMPTYTYPE', 'CARRIERSTATUS'] as const\nconst FIELD_BY_LOWER = new Map(CARRIER_FIELDS.map(f => [f.toLowerCase(), f]))\n\n/**\n * carrier 객체의 키를 캐노니컬(대문자)로 승격 — 식별 4 키만 매핑하고 값/기타 키는 보존.\n * 대소문자 무관 (carrierName → CARRIERNAME). 비객체면 그대로 반환.\n */\nexport function normalizeCarrierItem(raw: any): any {\n if (!raw || typeof raw !== 'object') return raw\n const out: any = {}\n for (const k of Object.keys(raw)) {\n out[FIELD_BY_LOWER.get(k.toLowerCase()) ?? k] = raw[k]\n }\n return out\n}\n\n/**\n * holder.data 정규화 (진입 경계 1회):\n * - 대표 carrier 4 키를 캐노니컬로 승격\n * - 다중 carrier 배열을 캐노니컬 키 `multiCarriers` 로 통일 (대소문자 무관), 각 항목도 정규화\n *\n * 비객체면 그대로 반환. 정규화 결과는 idempotent (이미 캐노니컬이면 동일 형태).\n */\nexport function normalizeCarrierData(raw: any): any {\n if (!raw || typeof raw !== 'object') return raw\n\n const multiLower = MULTI_CARRIER_KEY.toLowerCase()\n const out: any = {}\n for (const k of Object.keys(raw)) {\n // 다중 carrier 배열 키는 아래에서 캐노니컬로 통일 처리하므로 여기선 건너뜀.\n if (k.toLowerCase() === multiLower) continue\n out[FIELD_BY_LOWER.get(k.toLowerCase()) ?? k] = raw[k]\n }\n\n const list = ciGet(raw, MULTI_CARRIER_KEY)\n if (Array.isArray(list)) {\n out[MULTI_CARRIER_KEY] = list.map(normalizeCarrierItem)\n }\n\n return out\n}\n"]}
|
|
@@ -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.
|
|
5
|
+
"version": "2.0.0-beta.89",
|
|
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.
|
|
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": "
|
|
58
|
+
"gitHead": "e65994c03e8dc1c33b4466a61f12e9135f1bc59f"
|
|
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/carrier-manager.ts
CHANGED
|
@@ -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 { normalizeCarrierData, MULTI_CARRIER_KEY } from './utils/carrier-data'
|
|
4
5
|
|
|
5
6
|
// 사라진 carrier 의 dispose 지연. 같은 macrotask 안에서 holder A 의 사라짐
|
|
6
7
|
// 보고와 holder B 의 등장 보고가 순차로 들어오는 reparent 시나리오 (= 같은
|
|
@@ -57,6 +58,11 @@ export default class CarrierManager {
|
|
|
57
58
|
* holder 가 같은 이름을 보고하면 cancel 되어 reparent 로 전환.
|
|
58
59
|
*/
|
|
59
60
|
update(holder: MCSCarrierHolder, after: Record<string, any>, before: Record<string, any>): void {
|
|
61
|
+
// 진입 경계 1회 정규화 — 키 대소문자 무관 + multiCarriers 통일 (idempotent).
|
|
62
|
+
// 이후 _buildStack / diffing 은 캐노니컬 키를 O(1) 직접 접근으로 읽는다.
|
|
63
|
+
after = normalizeCarrierData(after)
|
|
64
|
+
before = normalizeCarrierData(before)
|
|
65
|
+
|
|
60
66
|
const afterStack = this._buildStack(after)
|
|
61
67
|
const beforeStack = this._buildStack(before)
|
|
62
68
|
|
|
@@ -127,16 +133,17 @@ export default class CarrierManager {
|
|
|
127
133
|
}
|
|
128
134
|
|
|
129
135
|
/**
|
|
130
|
-
* data.CARRIERNAME (대표) + data.
|
|
131
|
-
* 적층 순서:
|
|
136
|
+
* data.CARRIERNAME (대표) + data.multiCarriers[] 를 하나의 stack list 로 정규화.
|
|
137
|
+
* 적층 순서: multiCarriers[0..n-1] (bottom → up) + 대표 (top, 맨 마지막).
|
|
138
|
+
* (data 는 update() 에서 이미 정규화되어 캐노니컬 키 + multiCarriers 로 통일된 상태)
|
|
132
139
|
*/
|
|
133
140
|
private _buildStack(data: any): CarrierStackItem[] {
|
|
134
141
|
if (!data) return []
|
|
135
142
|
|
|
136
143
|
const stack: CarrierStackItem[] = []
|
|
137
144
|
|
|
138
|
-
if (Array.isArray(data
|
|
139
|
-
for (const a of data
|
|
145
|
+
if (Array.isArray(data[MULTI_CARRIER_KEY])) {
|
|
146
|
+
for (const a of data[MULTI_CARRIER_KEY]) {
|
|
140
147
|
if (a && typeof a === 'object' && a.CARRIERNAME) {
|
|
141
148
|
// 항목 *원본 그대로* — 외부 시스템이 임의 키를 추가로 보낼 수 있고,
|
|
142
149
|
// 누락된 4 키는 default 없이 그대로 비워둔다 (carrier.data 에 흘리는
|
package/src/crane.ts
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
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,
|
|
@@ -14,10 +21,6 @@ const GAP = 2
|
|
|
14
21
|
/** 좌표 보간 기본 duration (ms). data.DURATION_MS 가 없으면 이 값 사용. */
|
|
15
22
|
const DEFAULT_DURATION_MS = 1000
|
|
16
23
|
|
|
17
|
-
function clamp01(v: number): number {
|
|
18
|
-
return Math.max(0, Math.min(100, Number(v) || 0))
|
|
19
|
-
}
|
|
20
|
-
|
|
21
24
|
function lerp(a: number, b: number, t: number): number {
|
|
22
25
|
return a + (b - a) * t
|
|
23
26
|
}
|
|
@@ -48,6 +51,11 @@ export default class Crane extends MCSVehicle {
|
|
|
48
51
|
return 'black'
|
|
49
52
|
}
|
|
50
53
|
|
|
54
|
+
/** Crane 의 parentObject 는 STOCKER. */
|
|
55
|
+
get parentObjectTypes(): string[] {
|
|
56
|
+
return ['STOCKER']
|
|
57
|
+
}
|
|
58
|
+
|
|
51
59
|
getLegendFallback() {
|
|
52
60
|
return (this.root as any).craneLegendTheme || super.getLegendFallback()
|
|
53
61
|
}
|
|
@@ -55,29 +63,36 @@ export default class Crane extends MCSVehicle {
|
|
|
55
63
|
private _posAnimator?: CranePositionAnimator
|
|
56
64
|
|
|
57
65
|
/**
|
|
58
|
-
* data.
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
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 만 있는
|
|
62
72
|
* 데이터에선 NODEID 가 없어 자연스럽게 noop.
|
|
63
73
|
*/
|
|
64
74
|
onchangeData(after: Record<string, any>, before: Record<string, any>): void {
|
|
65
75
|
const afterData = after?.data
|
|
66
|
-
|
|
67
|
-
const
|
|
68
|
-
if (
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
afterPos.Y !== beforePos?.Y ||
|
|
73
|
-
afterPos.Z !== beforePos?.Z
|
|
74
|
-
if (changed) {
|
|
75
|
-
this._startPositionAnimation(afterPos, afterData?.DURATION_MS)
|
|
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'))
|
|
76
82
|
}
|
|
77
83
|
}
|
|
78
84
|
super.onchangeData(after, before)
|
|
79
85
|
}
|
|
80
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
|
+
|
|
81
96
|
dispose() {
|
|
82
97
|
if (this._posAnimator) {
|
|
83
98
|
this._posAnimator.unsubscribe()
|
|
@@ -86,12 +101,9 @@ export default class Crane extends MCSVehicle {
|
|
|
86
101
|
super.dispose()
|
|
87
102
|
}
|
|
88
103
|
|
|
89
|
-
private _startPositionAnimation(
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
): void {
|
|
93
|
-
const target = this._normalizedToReal(pos)
|
|
94
|
-
if (!target) return // 부모가 Stocker 아니면 변환 skip
|
|
104
|
+
private _startPositionAnimation(cranePosition: CranePositionData, durationMs?: number): void {
|
|
105
|
+
const target = this._ratioToReal(cranePosition)
|
|
106
|
+
if (!target) return // 부모(host) 없거나 크기 0 이면 변환 skip
|
|
95
107
|
|
|
96
108
|
const now = frameClock.simTime
|
|
97
109
|
const duration = Math.max(1, Number(durationMs) || DEFAULT_DURATION_MS)
|
|
@@ -150,20 +162,26 @@ export default class Crane extends MCSVehicle {
|
|
|
150
162
|
}
|
|
151
163
|
|
|
152
164
|
/**
|
|
153
|
-
*
|
|
165
|
+
* cranePosition (current + limit) → state.left/top + forkZ.
|
|
154
166
|
*
|
|
155
167
|
* 부모 오브젝트 (보통은 Stocker 이지만 다른 컨테이너일 수도) 의 width/height
|
|
156
|
-
* 만 사용해 좌표 매핑 — 타입 무관 일반화. 가상 rail 은 host 의
|
|
157
|
-
*
|
|
158
|
-
* -
|
|
159
|
-
* -
|
|
160
|
-
*
|
|
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 모서리에 정렬.
|
|
161
179
|
*
|
|
162
180
|
* 참조는 ParentObjectMixin 이 제공하는 `parentObject` 사용 —
|
|
163
181
|
* state.parentObjectId 명시 시 그것, 미명시 시 this.parent 자동 fallback.
|
|
164
182
|
*/
|
|
165
|
-
private
|
|
166
|
-
|
|
183
|
+
private _ratioToReal(
|
|
184
|
+
cp: CranePositionData
|
|
167
185
|
): { left: number; top: number; forkZ: number } | undefined {
|
|
168
186
|
const host: any = (this as any).parentObject
|
|
169
187
|
if (!host) return undefined
|
|
@@ -174,24 +192,16 @@ export default class Crane extends MCSVehicle {
|
|
|
174
192
|
const ch = Number(this.state.height) || 0
|
|
175
193
|
if (!hw || !hh) return undefined
|
|
176
194
|
|
|
177
|
-
const
|
|
178
|
-
const
|
|
179
|
-
const
|
|
180
|
-
const craneLongSize = longIsWidth ? cw : ch
|
|
181
|
-
const craneShortSize = longIsWidth ? ch : cw
|
|
182
|
-
|
|
183
|
-
const X = clamp01(pos.X)
|
|
184
|
-
const Y = clamp01(pos.Y)
|
|
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)
|
|
185
198
|
|
|
186
|
-
|
|
187
|
-
// state.left/top 은 top-left 기준이므로 size/2 차감.
|
|
188
|
-
const longPos = (X / 100) * longLen - craneLongSize / 2
|
|
189
|
-
const shortPos = (Y / 100) * shortLen - craneShortSize / 2
|
|
199
|
+
const { left, top } = mapCraneLocalPosition(hw, hh, cw, ch, rx, ry)
|
|
190
200
|
|
|
191
201
|
return {
|
|
192
|
-
left
|
|
193
|
-
top
|
|
194
|
-
forkZ:
|
|
202
|
+
left,
|
|
203
|
+
top,
|
|
204
|
+
forkZ: rz * 100 // forkZ 0~100, 3D 측 (crane-3d) 에서 mast 비율로 매핑
|
|
195
205
|
}
|
|
196
206
|
}
|
|
197
207
|
|
|
@@ -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
|
|
672
|
-
const
|
|
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 =
|
|
675
|
-
const canvasH =
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
}
|
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
import { themesColorMap, themesAnimationMap } from '@fmsim/api'
|
|
2
2
|
import MCSUnit from './mcs-unit.js'
|
|
3
|
+
import { normalizeCarrierData, MULTI_CARRIER_KEY } from './utils/carrier-data'
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
|
-
* Holder.data 변경 감지 시 carrierManager 에 전달할 4 키 묶음 +
|
|
6
|
-
* 비교 결과 단 하나라도 다르면 carrierManager.update 호출.
|
|
6
|
+
* Holder.data 변경 감지 시 carrierManager 에 전달할 4 키 묶음 + multiCarriers 배열.
|
|
7
|
+
* 비교 결과 단 하나라도 다르면 carrierManager.update 호출. (정규화된 캐노니컬 데이터로 비교)
|
|
7
8
|
*/
|
|
8
9
|
const CARRIER_DATA_KEYS = ['CARRIERNAME', 'CARRIERTYPE', 'EMPTYTYPE', 'CARRIERSTATUS'] as const
|
|
9
10
|
|
|
10
|
-
function
|
|
11
|
+
function multiCarriersChanged(a: any, b: any): boolean {
|
|
11
12
|
// shallow JSON 비교로 충분 — 각 항목이 4 키 primitive 만 가짐. 깊은 ref 비교
|
|
12
13
|
// 보다 빠르고 정확. 빈/누락은 같은 '[]' 직렬화로 동치 취급.
|
|
13
|
-
return JSON.stringify(a?.
|
|
14
|
+
return JSON.stringify(a?.[MULTI_CARRIER_KEY] ?? []) !== JSON.stringify(b?.[MULTI_CARRIER_KEY] ?? [])
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
export default class MCSCarrierHolder extends MCSUnit {
|
|
@@ -68,8 +69,9 @@ export default class MCSCarrierHolder extends MCSUnit {
|
|
|
68
69
|
}
|
|
69
70
|
|
|
70
71
|
onchangeData(after: Record<string, any>, before: Record<string, any>): void {
|
|
71
|
-
|
|
72
|
-
const
|
|
72
|
+
// 진입 경계 1회 정규화 — 키 대소문자 무관 + multiCarriers 통일. 이후 비교/update 는 캐노니컬 O(1).
|
|
73
|
+
const afterData = normalizeCarrierData(after.data || {})
|
|
74
|
+
const beforeData = normalizeCarrierData(before.data || {})
|
|
73
75
|
|
|
74
76
|
let changed = false
|
|
75
77
|
for (const k of CARRIER_DATA_KEYS) {
|
|
@@ -79,7 +81,7 @@ export default class MCSCarrierHolder extends MCSUnit {
|
|
|
79
81
|
}
|
|
80
82
|
}
|
|
81
83
|
if (!changed) {
|
|
82
|
-
changed =
|
|
84
|
+
changed = multiCarriersChanged(afterData, beforeData)
|
|
83
85
|
}
|
|
84
86
|
|
|
85
87
|
if (changed) {
|
|
@@ -89,11 +91,12 @@ export default class MCSCarrierHolder extends MCSUnit {
|
|
|
89
91
|
super.onchangeData(after, before)
|
|
90
92
|
}
|
|
91
93
|
|
|
92
|
-
/** data 안에 carrier 정보 (대표 또는
|
|
93
|
-
private _hasCarrierData(
|
|
94
|
+
/** data 안에 carrier 정보 (대표 또는 multiCarriers) 가 하나라도 있는지. (정규화 후 판정) */
|
|
95
|
+
private _hasCarrierData(raw: any): boolean {
|
|
96
|
+
const data = normalizeCarrierData(raw)
|
|
94
97
|
if (!data) return false
|
|
95
98
|
if (data.CARRIERNAME) return true
|
|
96
|
-
if (Array.isArray(data
|
|
99
|
+
if (Array.isArray(data[MULTI_CARRIER_KEY]) && data[MULTI_CARRIER_KEY].some((a: any) => a?.CARRIERNAME)) return true
|
|
97
100
|
return false
|
|
98
101
|
}
|
|
99
102
|
}
|
package/src/mcs-machine.ts
CHANGED
|
@@ -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
|
}
|