@fmsim/machine 2.0.0-beta.111 → 2.0.0-beta.114

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.
@@ -10,7 +10,8 @@
10
10
  * }
11
11
  *
12
12
  * current 를 limit 범위 안의 0~1 비율로 환산한 뒤 host(부모) footprint 에 매핑한다.
13
- * x → host 긴쪽, y → 짧은쪽, z → 포크 높이(forkZ 0~100).
13
+ * z → 포크 높이(forkZ 0~100). x/y 의 축 배정과 원점은 host 좌표계 설정(CoordMapping)을
14
+ * 따르고, 미설정 시 x → host 긴쪽, y → 짧은쪽, 원점 = 좌상단.
14
15
  */
15
16
  import { parseIfJsonString } from './data-value';
16
17
  /**
@@ -33,23 +34,31 @@ export function axisRatio(value, lim) {
33
34
  * craneLen/2 만큼 inset 된 중심선 위를 움직임). 크레인이 host 보다 크면 span 0
34
35
  * 으로 떨어져 host 모서리에 정렬.
35
36
  *
36
- * x(rx) 는 host 의 긴쪽, y(ry) 는 짧은쪽에 매핑 (가상 rail 은 host 긴쪽 방향).
37
+ * 축 배정과 원점은 opts(CoordMapping = host 의 내부 좌표계 설정)로 정한다. 기본(미설정)은
38
+ * x(rx) → host 의 긴쪽, y(ry) → 짧은쪽, 원점 = left/top.
37
39
  */
38
- export function mapCraneLocalPosition(hostW, hostH, craneW, craneH, rx, ry) {
39
- const longIsWidth = hostW >= hostH;
40
- const longLen = longIsWidth ? hostW : hostH;
41
- const shortLen = longIsWidth ? hostH : hostW;
42
- const craneLongSize = longIsWidth ? craneW : craneH;
43
- const craneShortSize = longIsWidth ? craneH : craneW;
40
+ export function mapCraneLocalPosition(hostW, hostH, craneW, craneH, rx, ry, opts = {}) {
41
+ // 내부 X 가 달리는 축 배정: 명시 설정 우선, 'auto'/미설정은 긴 변(종전 폴백).
42
+ // 정사각형에 가까운 host 는 auto 가 리사이즈로 뒤집힐 수 있어 명시 지정을 권한다.
43
+ const xIsWidth = opts.axisX === 'width' ? true : opts.axisX === 'height' ? false : hostW >= hostH;
44
+ // 반전은 *비율 단계*에서 흡수 — pos = (1-r)*span 이 위치 뒤집기와 동일하고 더 단순하다.
45
+ const fx = opts.invertX ? 1 - rx : rx;
46
+ const fy = opts.invertY ? 1 - ry : ry;
47
+ // 데이터 X/Y 축 기준으로 길이를 잡는다. axisX 를 명시하면 X 가 짧은 변일 수도 있으므로
48
+ // 길이(long/short)가 아니라 축 이름으로 부른다.
49
+ const xLen = xIsWidth ? hostW : hostH;
50
+ const yLen = xIsWidth ? hostH : hostW;
51
+ const craneXSize = xIsWidth ? craneW : craneH;
52
+ const craneYSize = xIsWidth ? craneH : craneW;
44
53
  // 크레인 크기를 뺀 inset 가용 span. top-left = ratio * span 이므로 크레인
45
54
  // 전체가 host [0, hostLen] 안에 들어온다.
46
- const longSpan = Math.max(0, longLen - craneLongSize);
47
- const shortSpan = Math.max(0, shortLen - craneShortSize);
48
- const longPos = rx * longSpan;
49
- const shortPos = ry * shortSpan;
55
+ const xSpan = Math.max(0, xLen - craneXSize);
56
+ const ySpan = Math.max(0, yLen - craneYSize);
57
+ const xPos = fx * xSpan;
58
+ const yPos = fy * ySpan;
50
59
  return {
51
- left: longIsWidth ? longPos : shortPos,
52
- top: longIsWidth ? shortPos : longPos
60
+ left: xIsWidth ? xPos : yPos,
61
+ top: xIsWidth ? yPos : xPos
53
62
  };
54
63
  }
55
64
  /**
@@ -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\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"]}
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 * z → 포크 높이(forkZ 0~100). x/y 의 축 배정과 원점은 host 좌표계 설정(CoordMapping)을\n * 따르고, 미설정 시 x → host 긴쪽, y → 짧은쪽, 원점 = 좌상단.\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(보통 STOCKER) 의 **내부 좌표계** 설정 — host state 에서 읽는다.\n *\n * 외부 시스템이 보내는 좌표의 X/Y 를 host footprint 의 어느 축·어느 방향에 대응시킬지\n * 정한다. 크레인 전용이 아니라 host 자신의 좌표 규약이므로, 같은 좌표계를 쓰는 다른\n * 자식(Shelf 등)도 동일하게 참조할 수 있다. 모두 미설정이면 종전 동작(긴쪽=X,\n * 원점=left/top)과 같다.\n *\n * 주의: **로컬 프레임 기준**이다. host 회전(rotation)은 렌더 단계에서 적용되므로,\n * 이 설정은 회전 전 모델 좌표계로 이해해야 한다.\n */\nexport type CoordMapping = {\n /** 내부 X 가 달리는 축. 'auto'(기본) = 긴 변. */\n axisX?: 'auto' | 'width' | 'height'\n /** X=0 을 반대쪽 끝으로. */\n invertX?: boolean\n /** Y=0 을 반대쪽 끝으로. */\n invertY?: boolean\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 * 축 배정과 원점은 opts(CoordMapping = host 의 내부 좌표계 설정)로 정한다. 기본(미설정)은\n * x(rx) → host 의 긴쪽, y(ry) → 짧은쪽, 원점 = left/top.\n */\nexport function mapCraneLocalPosition(\n hostW: number,\n hostH: number,\n craneW: number,\n craneH: number,\n rx: number,\n ry: number,\n opts: CoordMapping = {}\n): { left: number; top: number } {\n // 내부 X 가 달리는 축 배정: 명시 설정 우선, 'auto'/미설정은 긴 변(종전 폴백).\n // 정사각형에 가까운 host 는 auto 가 리사이즈로 뒤집힐 수 있어 명시 지정을 권한다.\n const xIsWidth = opts.axisX === 'width' ? true : opts.axisX === 'height' ? false : hostW >= hostH\n\n // 반전은 *비율 단계*에서 흡수 — pos = (1-r)*span 이 위치 뒤집기와 동일하고 더 단순하다.\n const fx = opts.invertX ? 1 - rx : rx\n const fy = opts.invertY ? 1 - ry : ry\n\n // 데이터 X/Y 축 기준으로 길이를 잡는다. axisX 를 명시하면 X 가 짧은 변일 수도 있으므로\n // 길이(long/short)가 아니라 축 이름으로 부른다.\n const xLen = xIsWidth ? hostW : hostH\n const yLen = xIsWidth ? hostH : hostW\n const craneXSize = xIsWidth ? craneW : craneH\n const craneYSize = xIsWidth ? craneH : craneW\n\n // 크레인 크기를 뺀 inset 가용 span. top-left = ratio * span 이므로 크레인\n // 전체가 host [0, hostLen] 안에 들어온다.\n const xSpan = Math.max(0, xLen - craneXSize)\n const ySpan = Math.max(0, yLen - craneYSize)\n const xPos = fx * xSpan\n const yPos = fy * ySpan\n\n return {\n left: xIsWidth ? xPos : yPos,\n top: xIsWidth ? yPos : xPos\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"]}
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.111",
5
+ "version": "2.0.0-beta.114",
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.110",
29
+ "@fmsim/api": "^2.0.0-beta.112",
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": "4ad2b756c574fae9c629720b8b94d2ccefd604b9"
58
+ "gitHead": "750d1fd5969ad889065876838889806f1586a52a"
59
59
  }
package/src/crane.ts CHANGED
@@ -165,10 +165,12 @@ export default class Crane extends MCSVehicle {
165
165
  * cranePosition (current + limit) → state.left/top + forkZ.
166
166
  *
167
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 비율로 매핑)
168
+ * 만 사용해 좌표 매핑 — 타입 무관 일반화. 축 배정과 원점은 host 의 내부 좌표계 설정을
169
+ * 따른다 (coordAxisX / coordInvertX / coordInvertY, 미설정 시 아래 폴백):
170
+ * - current.x → X축 (기본: host 의 긴쪽, 자동 판별 max(width, height))
171
+ * - current.y → Y축 (기본: 짧은쪽)
172
+ * - current.z → forkZ (state 에 0~100 으로 저장, 3D 측에서 mast 비율로 매핑).
173
+ * Z 는 바닥 기준이 물리적으로 고정이라 반전 설정을 두지 않는다.
172
174
  *
173
175
  * 각 축 비율 = (current - limit.min) / (limit.max - limit.min) ∈ [0,1].
174
176
  *
@@ -196,7 +198,14 @@ export default class Crane extends MCSVehicle {
196
198
  const ry = axisRatio(cp.current?.y, cp.limit?.y)
197
199
  const rz = axisRatio(cp.current?.z, cp.limit?.z)
198
200
 
199
- const { left, top } = mapCraneLocalPosition(hw, hh, cw, ch, rx, ry)
201
+ // 축 배정/원점은 host(보통 STOCKER) 의 내부 좌표계 설정을 따른다. 타입을 묻지 않고
202
+ // state 만 읽어 어떤 컨테이너가 host 여도 동일하게 동작한다 — 미설정이면 종전
203
+ // 폴백(긴쪽=X, 원점 left/top).
204
+ const { left, top } = mapCraneLocalPosition(hw, hh, cw, ch, rx, ry, {
205
+ axisX: host.state?.coordAxisX,
206
+ invertX: !!host.state?.coordInvertX,
207
+ invertY: !!host.state?.coordInvertY
208
+ })
200
209
 
201
210
  return {
202
211
  left,
package/src/stocker.ts CHANGED
@@ -5,7 +5,35 @@ const NATURE: ComponentNature = {
5
5
  mutable: false,
6
6
  resizable: true,
7
7
  rotatable: true,
8
- properties: [...MCSTransport.properties]
8
+ properties: [
9
+ ...MCSTransport.properties,
10
+ /* 이 스토커의 *내부 좌표계* 규약 — 외부 시스템이 보내는 X/Y 를 footprint 의 어느
11
+ * 축·어느 방향에 대응시킬지 정한다(크레인 위치 데이터 CRANEPOSITION 등이 이를 따른다).
12
+ * 미설정이면 종전 폴백(긴 변 = X축, 원점 = 좌상단). 모두 *로컬 프레임* 기준이라
13
+ * 스토커 회전(rotation)과는 무관하다. Z(높이)는 바닥 기준이 물리적으로 고정이라 설정 없음. */
14
+ {
15
+ type: 'select',
16
+ label: 'coord-axis-x',
17
+ name: 'coordAxisX',
18
+ property: {
19
+ options: [
20
+ { display: 'auto (longer side)', value: '' },
21
+ { display: 'width', value: 'width' },
22
+ { display: 'height', value: 'height' }
23
+ ]
24
+ }
25
+ },
26
+ {
27
+ type: 'checkbox',
28
+ label: 'coord-invert-x',
29
+ name: 'coordInvertX'
30
+ },
31
+ {
32
+ type: 'checkbox',
33
+ label: 'coord-invert-y',
34
+ name: 'coordInvertY'
35
+ }
36
+ ]
9
37
  }
10
38
 
11
39
  // const capacity = { ko: 'Capacity', en: 'Capacity', zh_CN: '容量' }
@@ -10,7 +10,8 @@
10
10
  * }
11
11
  *
12
12
  * current 를 limit 범위 안의 0~1 비율로 환산한 뒤 host(부모) footprint 에 매핑한다.
13
- * x → host 긴쪽, y → 짧은쪽, z → 포크 높이(forkZ 0~100).
13
+ * z → 포크 높이(forkZ 0~100). x/y 의 축 배정과 원점은 host 좌표계 설정(CoordMapping)을
14
+ * 따르고, 미설정 시 x → host 긴쪽, y → 짧은쪽, 원점 = 좌상단.
14
15
  */
15
16
 
16
17
  import { parseIfJsonString } from './data-value'
@@ -39,6 +40,26 @@ export function axisRatio(value: unknown, lim?: AxisLimit): number {
39
40
  return Math.max(0, Math.min(1, (v - min) / (max - min)))
40
41
  }
41
42
 
43
+ /**
44
+ * host(보통 STOCKER) 의 **내부 좌표계** 설정 — host state 에서 읽는다.
45
+ *
46
+ * 외부 시스템이 보내는 좌표의 X/Y 를 host footprint 의 어느 축·어느 방향에 대응시킬지
47
+ * 정한다. 크레인 전용이 아니라 host 자신의 좌표 규약이므로, 같은 좌표계를 쓰는 다른
48
+ * 자식(Shelf 등)도 동일하게 참조할 수 있다. 모두 미설정이면 종전 동작(긴쪽=X,
49
+ * 원점=left/top)과 같다.
50
+ *
51
+ * 주의: **로컬 프레임 기준**이다. host 회전(rotation)은 렌더 단계에서 적용되므로,
52
+ * 이 설정은 회전 전 모델 좌표계로 이해해야 한다.
53
+ */
54
+ export type CoordMapping = {
55
+ /** 내부 X 가 달리는 축. 'auto'(기본) = 긴 변. */
56
+ axisX?: 'auto' | 'width' | 'height'
57
+ /** X=0 을 반대쪽 끝으로. */
58
+ invertX?: boolean
59
+ /** Y=0 을 반대쪽 끝으로. */
60
+ invertY?: boolean
61
+ }
62
+
42
63
  /**
43
64
  * host(부모) footprint 안에서 0~1 비율 (rx,ry) → 크레인 top-left 로컬 좌표.
44
65
  *
@@ -47,7 +68,8 @@ export function axisRatio(value: unknown, lim?: AxisLimit): number {
47
68
  * craneLen/2 만큼 inset 된 중심선 위를 움직임). 크레인이 host 보다 크면 span 0
48
69
  * 으로 떨어져 host 모서리에 정렬.
49
70
  *
50
- * x(rx) 는 host 의 긴쪽, y(ry) 는 짧은쪽에 매핑 (가상 rail 은 host 긴쪽 방향).
71
+ * 축 배정과 원점은 opts(CoordMapping = host 의 내부 좌표계 설정)로 정한다. 기본(미설정)은
72
+ * x(rx) → host 의 긴쪽, y(ry) → 짧은쪽, 원점 = left/top.
51
73
  */
52
74
  export function mapCraneLocalPosition(
53
75
  hostW: number,
@@ -55,24 +77,34 @@ export function mapCraneLocalPosition(
55
77
  craneW: number,
56
78
  craneH: number,
57
79
  rx: number,
58
- ry: number
80
+ ry: number,
81
+ opts: CoordMapping = {}
59
82
  ): { 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
83
+ // 내부 X 가 달리는 축 배정: 명시 설정 우선, 'auto'/미설정은 긴 변(종전 폴백).
84
+ // 정사각형에 가까운 host 는 auto 가 리사이즈로 뒤집힐 수 있어 명시 지정을 권한다.
85
+ const xIsWidth = opts.axisX === 'width' ? true : opts.axisX === 'height' ? false : hostW >= hostH
86
+
87
+ // 반전은 *비율 단계*에서 흡수 — pos = (1-r)*span 이 위치 뒤집기와 동일하고 더 단순하다.
88
+ const fx = opts.invertX ? 1 - rx : rx
89
+ const fy = opts.invertY ? 1 - ry : ry
90
+
91
+ // 데이터 X/Y 축 기준으로 길이를 잡는다. axisX 를 명시하면 X 가 짧은 변일 수도 있으므로
92
+ // 길이(long/short)가 아니라 축 이름으로 부른다.
93
+ const xLen = xIsWidth ? hostW : hostH
94
+ const yLen = xIsWidth ? hostH : hostW
95
+ const craneXSize = xIsWidth ? craneW : craneH
96
+ const craneYSize = xIsWidth ? craneH : craneW
65
97
 
66
98
  // 크레인 크기를 뺀 inset 가용 span. top-left = ratio * span 이므로 크레인
67
99
  // 전체가 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
100
+ const xSpan = Math.max(0, xLen - craneXSize)
101
+ const ySpan = Math.max(0, yLen - craneYSize)
102
+ const xPos = fx * xSpan
103
+ const yPos = fy * ySpan
72
104
 
73
105
  return {
74
- left: longIsWidth ? longPos : shortPos,
75
- top: longIsWidth ? shortPos : longPos
106
+ left: xIsWidth ? xPos : yPos,
107
+ top: xIsWidth ? yPos : xPos
76
108
  }
77
109
  }
78
110
 
@@ -71,5 +71,8 @@
71
71
  "label.light-preset": "light preset",
72
72
  "label.dark": "dark",
73
73
  "label.medium": "medium",
74
- "label.bright": "bright"
74
+ "label.bright": "bright",
75
+ "label.coord-axis-x": "internal x axis",
76
+ "label.coord-invert-x": "invert x origin",
77
+ "label.coord-invert-y": "invert y origin"
75
78
  }
@@ -73,5 +73,8 @@
73
73
  "label.light-preset": "照明プリセット",
74
74
  "label.dark": "暗",
75
75
  "label.medium": "中",
76
- "label.bright": "明"
76
+ "label.bright": "明",
77
+ "label.coord-axis-x": "内部X軸",
78
+ "label.coord-invert-x": "X原点反転",
79
+ "label.coord-invert-y": "Y原点反転"
77
80
  }
@@ -74,5 +74,8 @@
74
74
  "label.light-preset": "조명 프리셋",
75
75
  "label.dark": "어두움",
76
76
  "label.medium": "중간",
77
- "label.bright": "밝음"
77
+ "label.bright": "밝음",
78
+ "label.coord-axis-x": "내부 X축",
79
+ "label.coord-invert-x": "X 원점 반전",
80
+ "label.coord-invert-y": "Y 원점 반전"
78
81
  }
@@ -73,5 +73,8 @@
73
73
  "label.light-preset": "pratetapan cahaya",
74
74
  "label.dark": "gelap",
75
75
  "label.medium": "sederhana",
76
- "label.bright": "cerah"
76
+ "label.bright": "cerah",
77
+ "label.coord-axis-x": "internal x axis",
78
+ "label.coord-invert-x": "invert x origin",
79
+ "label.coord-invert-y": "invert y origin"
77
80
  }
@@ -73,5 +73,8 @@
73
73
  "label.light-preset": "灯光预设",
74
74
  "label.dark": "昏暗",
75
75
  "label.medium": "适中",
76
- "label.bright": "明亮"
76
+ "label.bright": "明亮",
77
+ "label.coord-axis-x": "内部X轴",
78
+ "label.coord-invert-x": "X原点反转",
79
+ "label.coord-invert-y": "Y原点反转"
77
80
  }