/* * Crane 위치 데이터 (data.cranePosition) 변환 유틸. * * 명세: * { * cranePosition: { * current: { x, y, z }, // 실 좌표값 * limit: { x:{min,max}, y:{min,max}, z:{min,max} } // 각 축 한계 * } * } * * current 를 limit 범위 안의 0~1 비율로 환산한 뒤 host(부모) footprint 에 매핑한다. * x → host 긴쪽, y → 짧은쪽, z → 포크 높이(forkZ 0~100). */ import { parseIfJsonString } from './data-value' /** 축별 한계 (cranePosition.limit.{x|y|z}). */ export interface AxisLimit { min?: number max?: number } /** 새 데이터 명세 — data.cranePosition. */ export interface CranePositionData { current?: { x?: number; y?: number; z?: number } limit?: { x?: AxisLimit; y?: AxisLimit; z?: AxisLimit } } /** * current 값을 limit 범위 안에서 0~1 비율로 환산. * limit 누락 / 0 span / 비정상 입력 시 0 반환 (안전 fallback). */ export function axisRatio(value: unknown, lim?: AxisLimit): number { const v = Number(value) const min = Number(lim?.min) || 0 const max = Number(lim?.max) if (!Number.isFinite(v) || !Number.isFinite(max) || max === min) return 0 return Math.max(0, Math.min(1, (v - min) / (max - min))) } /** * host(부모) footprint 안에서 0~1 비율 (rx,ry) → 크레인 top-left 로컬 좌표. * * **스토커 안쪽 오프셋**: 크레인의 폭/두께를 감안해, 크레인 *전체* 가 host 안에 * 머무르도록 top-left 좌표를 [0, hostLen - craneLen] 범위로 매핑한다 (= 양끝에서 * craneLen/2 만큼 inset 된 중심선 위를 움직임). 크레인이 host 보다 크면 span 0 * 으로 떨어져 host 모서리에 정렬. * * x(rx) 는 host 의 긴쪽, y(ry) 는 짧은쪽에 매핑 (가상 rail 은 host 긴쪽 방향). */ export function mapCraneLocalPosition( hostW: number, hostH: number, craneW: number, craneH: number, rx: number, ry: number ): { left: number; top: number } { const longIsWidth = hostW >= hostH const longLen = longIsWidth ? hostW : hostH const shortLen = longIsWidth ? hostH : hostW const craneLongSize = longIsWidth ? craneW : craneH const craneShortSize = longIsWidth ? craneH : craneW // 크레인 크기를 뺀 inset 가용 span. top-left = ratio * span 이므로 크레인 // 전체가 host [0, hostLen] 안에 들어온다. const longSpan = Math.max(0, longLen - craneLongSize) const shortSpan = Math.max(0, shortLen - craneShortSize) const longPos = rx * longSpan const shortPos = ry * shortSpan return { left: longIsWidth ? longPos : shortPos, top: longIsWidth ? shortPos : longPos } } /** * 객체 키를 재귀적으로 소문자화한 사본 반환 (배열/원시값은 그대로, 값은 변형 안 함). * 데이터 키는 모두 대문자 전제 — CRANEPOSITION 의 중첩 키(CURRENT/LIMIT/X/Y/Z/MIN/MAX)를 * 내부 표준형(소문자 current/limit/x/y/z/min/max)으로 변환하기 위한 정규화. */ export function lowerKeysDeep(value: any): T { if (Array.isArray(value)) return value.map(v => lowerKeysDeep(v)) as any if (value && typeof value === 'object') { const out: Record = {} for (const k of Object.keys(value)) { out[k.toLowerCase()] = lowerKeysDeep(value[k]) } return out as T } return value as T } /** * raw CRANEPOSITION → 내부 표준형(소문자 키) 구조(CranePositionData). * 값이 JSON 문자열로 올 수 있어 먼저 파싱하고, 중첩 키(CURRENT/LIMIT/X/Y/Z/MIN/MAX)를 * 소문자(current.x / limit.x.min)로 변환. 파싱 실패/비객체면 undefined. */ export function normalizeCranePosition(raw: any): CranePositionData | undefined { const obj = parseIfJsonString(raw) if (!obj || typeof obj !== 'object') return undefined return lowerKeysDeep(obj) }