@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
package/dist/agv.js CHANGED
@@ -28,6 +28,10 @@ let AGV = class AGV extends MCSVehicle {
28
28
  get auxColor() {
29
29
  return 'black';
30
30
  }
31
+ /** AGV 의 parentObject 는 AGVLine. */
32
+ get parentObjectTypes() {
33
+ return ['AGVLine'];
34
+ }
31
35
  getLegendFallback() {
32
36
  return this.root.agvLegendTheme || super.getLegendFallback();
33
37
  }
package/dist/agv.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"agv.js","sourceRoot":"","sources":["../src/agv.ts"],"names":[],"mappings":"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","sourcesContent":["import { ComponentNature, sceneComponent } from '@hatiolab/things-scene'\nimport { themesColorRange } from '@fmsim/api'\nimport MCSVehicle from './mcs-vehicle'\nimport { getValueOnRanges } from './utils/get-value-on-ranges'\nimport { BATTERY_RATE_LEGEND } from './features/mcs-status-default'\n\nconst NATURE: ComponentNature = {\n mutable: false,\n resizable: true,\n rotatable: true,\n properties: [\n ...MCSVehicle.properties,\n {\n type: 'select',\n label: 'battery-rate-legend-name',\n name: 'batteryRateLegendName',\n property: {\n options: themesColorRange\n }\n }\n ]\n}\n\nconst GAP = 2\n\n@sceneComponent('AGV')\nexport default class AGV extends MCSVehicle {\n static get nature() {\n return NATURE\n }\n\n get auxColor() {\n return 'black'\n }\n\n getLegendFallback() {\n return (this.root as any).agvLegendTheme || super.getLegendFallback()\n }\n\n getBatteryLegendFallback() {\n return (this.root as any).batteryRateLegendTheme || BATTERY_RATE_LEGEND\n }\n\n getBatteryLegendTheme() {\n const { batteryRateLegendName } = this.state\n\n if (batteryRateLegendName) {\n return (this.root as any)?.style?.[batteryRateLegendName]\n }\n }\n\n getBatteryColor(rate: number) {\n return getValueOnRanges(rate, this.getBatteryLegendTheme() || this.getBatteryLegendFallback())\n }\n\n render(ctx: CanvasRenderingContext2D) {\n var { left, top, width, height } = this.bounds\n\n ctx.translate(left, top)\n ctx.beginPath()\n\n // 작은 크기에서도 보이도록 최소 선 두께 설정\n const minLineWidth = 0.5\n const scaleFactor = Math.min(width, height) / 100\n const lineWidth = Math.max(minLineWidth, scaleFactor)\n\n ctx.strokeStyle = this.auxColor\n ctx.fillStyle = this.statusColor!\n ctx.lineWidth = lineWidth\n ctx.lineCap = 'round'\n ctx.setLineDash([])\n\n // 기본 사각형 그리기\n ctx.rect(1, 0, width - 2, height)\n ctx.rect(GAP + 1, GAP, width - (GAP + 1) * 2, height - GAP * 2)\n\n ctx.fill()\n ctx.stroke()\n\n ctx.beginPath()\n\n // 바퀴 그리기\n ctx.strokeStyle = 'black'\n\n ctx.moveTo(0, (height * 1) / 3)\n ctx.lineTo(0, (height * 2) / 3)\n ctx.moveTo(width, (height * 1) / 3)\n ctx.lineTo(width, (height * 2) / 3)\n\n ctx.stroke()\n\n ctx.translate(-left, -top)\n }\n\n // 사선 패턴을 그리는 함수\n drawHatchPattern(ctx: CanvasRenderingContext2D, x: number, y: number, width: number, height: number) {\n // 6개의 빗금을 그리기 위한 간격 계산\n const spacing = (width + height) / 6\n\n // 선 두께 계산 (크기에 비례)\n const minLineWidth = 0.5\n const scaleFactor = Math.min(width, height) / 20 // 130에서 50으로 변경하여 더 두껍게\n const lineWidth = Math.max(minLineWidth, scaleFactor)\n\n ctx.beginPath()\n ctx.strokeStyle = 'black'\n ctx.lineWidth = lineWidth\n\n // 배터리 박스 내부로 클리핑 설정\n ctx.save()\n ctx.rect(x, y, width, height)\n ctx.clip()\n\n // 6개의 사선 패턴 그리기\n for (let i = 0; i < 6; i++) {\n const startX = x + i * spacing\n ctx.moveTo(startX, y)\n ctx.lineTo(startX - height, y + height)\n }\n\n ctx.stroke()\n // 클리핑 해제\n ctx.restore()\n }\n\n postrender(ctx: CanvasRenderingContext2D): void {\n super.postrender(ctx)\n\n const BATTERYRATE = this.data?.BATTERYRATE\n\n if (typeof BATTERYRATE === 'number') {\n const { left, top, width, height } = this.bounds\n ctx.translate(left, top)\n\n // 배터리 크기 및 위치 설정 (최소 크기 보장)\n const minBatteryHeight = 2\n const minBatteryWidth = 4\n const preferredHeight = height / 5\n const preferredWidth = (width * 3) / 4\n\n const batteryHeight = Math.max(minBatteryHeight, preferredHeight)\n const batteryWidth = Math.max(minBatteryWidth, preferredWidth)\n const batteryX = (width - batteryWidth) / 2\n const batteryY = (height - batteryHeight) * 1.1\n\n // 배터리 테두리 그리기 (크기에 비례한 선 두께)\n const minLineWidth = 0.5\n const scaleFactor = Math.min(width, height) / 100\n const lineWidth = Math.max(minLineWidth, scaleFactor)\n\n // 배터리 배경을 흰색으로 채우기\n ctx.fillStyle = 'white'\n ctx.fillRect(batteryX, batteryY, batteryWidth, batteryHeight)\n\n // 배터리 전체 영역에 사선 패턴 그리기\n this.drawHatchPattern(ctx, batteryX, batteryY, batteryWidth, batteryHeight)\n\n // 배터리 잔량 표시\n const fillWidth = (batteryWidth * BATTERYRATE) / 100\n\n // 배터리 잔량에 따른 색상 설정 - 테마 적용\n ctx.fillStyle = this.getBatteryColor(BATTERYRATE)\n\n // 배터리 잔량 영역 채우기\n ctx.fillRect(batteryX, batteryY, fillWidth, batteryHeight)\n\n // 배터리 테두리 다시 그리기\n ctx.beginPath()\n ctx.strokeStyle = 'black'\n ctx.lineWidth = lineWidth\n ctx.strokeRect(batteryX, batteryY, batteryWidth, batteryHeight)\n\n // 배터리 잔량 텍스트 표시\n const fontSize = batteryHeight * 0.8\n\n // 실제 렌더링될 텍스트 크기 계산\n const transform = ctx.getTransform()\n const scaleX = Math.sqrt(transform.m11 * transform.m11 + transform.m21 * transform.m21)\n const actualFontSize = (fontSize * (scaleX || 1)) / window.devicePixelRatio\n\n // 실제 텍스트 크기가 10px 이상일 때만 텍스트 표시\n if (actualFontSize >= 10) {\n ctx.font = `${fontSize}px Roboto, Arial, sans-serif`\n ctx.fillStyle = 'black'\n ctx.textAlign = 'center'\n ctx.textBaseline = 'middle'\n\n // 배터리 잔량 텍스트 (정수 + % 포함)\n const { BATTERYRATETEXT } = this.data || {}\n const text = BATTERYRATETEXT || `${Math.round(BATTERYRATE)}%`\n\n // 텍스트 크기 측정\n const textMetrics = ctx.measureText(text)\n const textWidth = textMetrics.width * 1.1 // 여백을 위해 10% 추가\n const textHeight = fontSize * 1.1 // 여백을 위해 10% 추가\n\n // 배터리 중심점 계산\n const batteryCenterX = batteryX + batteryWidth / 2\n const batteryCenterY = batteryY + batteryHeight / 2\n\n // 현재 변환 상태 저장\n ctx.save()\n\n // 배터리 중심점으로 이동\n ctx.translate(batteryCenterX, batteryCenterY)\n\n // 순수한 회전 각도 계산 (라디안)\n const normalizedM11 = transform.m11 / scaleX\n const normalizedM21 = transform.m21 / scaleX\n let angle = Math.atan2(normalizedM21, normalizedM11)\n\n // 회전 보정 (같은 방향으로 회전)\n ctx.rotate(angle)\n\n // 텍스트 배경 위치 계산 (중심점 기준)\n const textBgX = -textWidth / 2\n const textBgY = -textHeight / 2\n\n // 텍스트 배경 그리기\n ctx.fillStyle = 'white'\n ctx.fillRect(textBgX, textBgY, textWidth, textHeight)\n\n // 텍스트 그리기\n ctx.fillStyle = 'black'\n ctx.textAlign = 'center'\n ctx.textBaseline = 'middle'\n ctx.fillText(text, 0, textHeight * 0.1) // 텍스트를 약간 아래로 이동\n\n // 변환 상태 복원\n ctx.restore()\n }\n\n ctx.translate(-left, -top)\n }\n }\n}\n"]}
1
+ {"version":3,"file":"agv.js","sourceRoot":"","sources":["../src/agv.ts"],"names":[],"mappings":"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","sourcesContent":["import { ComponentNature, sceneComponent } from '@hatiolab/things-scene'\nimport { themesColorRange } from '@fmsim/api'\nimport MCSVehicle from './mcs-vehicle'\nimport { getValueOnRanges } from './utils/get-value-on-ranges'\nimport { BATTERY_RATE_LEGEND } from './features/mcs-status-default'\n\nconst NATURE: ComponentNature = {\n mutable: false,\n resizable: true,\n rotatable: true,\n properties: [\n ...MCSVehicle.properties,\n {\n type: 'select',\n label: 'battery-rate-legend-name',\n name: 'batteryRateLegendName',\n property: {\n options: themesColorRange\n }\n }\n ]\n}\n\nconst GAP = 2\n\n@sceneComponent('AGV')\nexport default class AGV extends MCSVehicle {\n static get nature() {\n return NATURE\n }\n\n get auxColor() {\n return 'black'\n }\n\n /** AGV 의 parentObject 는 AGVLine. */\n get parentObjectTypes(): string[] {\n return ['AGVLine']\n }\n\n getLegendFallback() {\n return (this.root as any).agvLegendTheme || super.getLegendFallback()\n }\n\n getBatteryLegendFallback() {\n return (this.root as any).batteryRateLegendTheme || BATTERY_RATE_LEGEND\n }\n\n getBatteryLegendTheme() {\n const { batteryRateLegendName } = this.state\n\n if (batteryRateLegendName) {\n return (this.root as any)?.style?.[batteryRateLegendName]\n }\n }\n\n getBatteryColor(rate: number) {\n return getValueOnRanges(rate, this.getBatteryLegendTheme() || this.getBatteryLegendFallback())\n }\n\n render(ctx: CanvasRenderingContext2D) {\n var { left, top, width, height } = this.bounds\n\n ctx.translate(left, top)\n ctx.beginPath()\n\n // 작은 크기에서도 보이도록 최소 선 두께 설정\n const minLineWidth = 0.5\n const scaleFactor = Math.min(width, height) / 100\n const lineWidth = Math.max(minLineWidth, scaleFactor)\n\n ctx.strokeStyle = this.auxColor\n ctx.fillStyle = this.statusColor!\n ctx.lineWidth = lineWidth\n ctx.lineCap = 'round'\n ctx.setLineDash([])\n\n // 기본 사각형 그리기\n ctx.rect(1, 0, width - 2, height)\n ctx.rect(GAP + 1, GAP, width - (GAP + 1) * 2, height - GAP * 2)\n\n ctx.fill()\n ctx.stroke()\n\n ctx.beginPath()\n\n // 바퀴 그리기\n ctx.strokeStyle = 'black'\n\n ctx.moveTo(0, (height * 1) / 3)\n ctx.lineTo(0, (height * 2) / 3)\n ctx.moveTo(width, (height * 1) / 3)\n ctx.lineTo(width, (height * 2) / 3)\n\n ctx.stroke()\n\n ctx.translate(-left, -top)\n }\n\n // 사선 패턴을 그리는 함수\n drawHatchPattern(ctx: CanvasRenderingContext2D, x: number, y: number, width: number, height: number) {\n // 6개의 빗금을 그리기 위한 간격 계산\n const spacing = (width + height) / 6\n\n // 선 두께 계산 (크기에 비례)\n const minLineWidth = 0.5\n const scaleFactor = Math.min(width, height) / 20 // 130에서 50으로 변경하여 더 두껍게\n const lineWidth = Math.max(minLineWidth, scaleFactor)\n\n ctx.beginPath()\n ctx.strokeStyle = 'black'\n ctx.lineWidth = lineWidth\n\n // 배터리 박스 내부로 클리핑 설정\n ctx.save()\n ctx.rect(x, y, width, height)\n ctx.clip()\n\n // 6개의 사선 패턴 그리기\n for (let i = 0; i < 6; i++) {\n const startX = x + i * spacing\n ctx.moveTo(startX, y)\n ctx.lineTo(startX - height, y + height)\n }\n\n ctx.stroke()\n // 클리핑 해제\n ctx.restore()\n }\n\n postrender(ctx: CanvasRenderingContext2D): void {\n super.postrender(ctx)\n\n const BATTERYRATE = this.data?.BATTERYRATE\n\n if (typeof BATTERYRATE === 'number') {\n const { left, top, width, height } = this.bounds\n ctx.translate(left, top)\n\n // 배터리 크기 및 위치 설정 (최소 크기 보장)\n const minBatteryHeight = 2\n const minBatteryWidth = 4\n const preferredHeight = height / 5\n const preferredWidth = (width * 3) / 4\n\n const batteryHeight = Math.max(minBatteryHeight, preferredHeight)\n const batteryWidth = Math.max(minBatteryWidth, preferredWidth)\n const batteryX = (width - batteryWidth) / 2\n const batteryY = (height - batteryHeight) * 1.1\n\n // 배터리 테두리 그리기 (크기에 비례한 선 두께)\n const minLineWidth = 0.5\n const scaleFactor = Math.min(width, height) / 100\n const lineWidth = Math.max(minLineWidth, scaleFactor)\n\n // 배터리 배경을 흰색으로 채우기\n ctx.fillStyle = 'white'\n ctx.fillRect(batteryX, batteryY, batteryWidth, batteryHeight)\n\n // 배터리 전체 영역에 사선 패턴 그리기\n this.drawHatchPattern(ctx, batteryX, batteryY, batteryWidth, batteryHeight)\n\n // 배터리 잔량 표시\n const fillWidth = (batteryWidth * BATTERYRATE) / 100\n\n // 배터리 잔량에 따른 색상 설정 - 테마 적용\n ctx.fillStyle = this.getBatteryColor(BATTERYRATE)\n\n // 배터리 잔량 영역 채우기\n ctx.fillRect(batteryX, batteryY, fillWidth, batteryHeight)\n\n // 배터리 테두리 다시 그리기\n ctx.beginPath()\n ctx.strokeStyle = 'black'\n ctx.lineWidth = lineWidth\n ctx.strokeRect(batteryX, batteryY, batteryWidth, batteryHeight)\n\n // 배터리 잔량 텍스트 표시\n const fontSize = batteryHeight * 0.8\n\n // 실제 렌더링될 텍스트 크기 계산\n const transform = ctx.getTransform()\n const scaleX = Math.sqrt(transform.m11 * transform.m11 + transform.m21 * transform.m21)\n const actualFontSize = (fontSize * (scaleX || 1)) / window.devicePixelRatio\n\n // 실제 텍스트 크기가 10px 이상일 때만 텍스트 표시\n if (actualFontSize >= 10) {\n ctx.font = `${fontSize}px Roboto, Arial, sans-serif`\n ctx.fillStyle = 'black'\n ctx.textAlign = 'center'\n ctx.textBaseline = 'middle'\n\n // 배터리 잔량 텍스트 (정수 + % 포함)\n const { BATTERYRATETEXT } = this.data || {}\n const text = BATTERYRATETEXT || `${Math.round(BATTERYRATE)}%`\n\n // 텍스트 크기 측정\n const textMetrics = ctx.measureText(text)\n const textWidth = textMetrics.width * 1.1 // 여백을 위해 10% 추가\n const textHeight = fontSize * 1.1 // 여백을 위해 10% 추가\n\n // 배터리 중심점 계산\n const batteryCenterX = batteryX + batteryWidth / 2\n const batteryCenterY = batteryY + batteryHeight / 2\n\n // 현재 변환 상태 저장\n ctx.save()\n\n // 배터리 중심점으로 이동\n ctx.translate(batteryCenterX, batteryCenterY)\n\n // 순수한 회전 각도 계산 (라디안)\n const normalizedM11 = transform.m11 / scaleX\n const normalizedM21 = transform.m21 / scaleX\n let angle = Math.atan2(normalizedM21, normalizedM11)\n\n // 회전 보정 (같은 방향으로 회전)\n ctx.rotate(angle)\n\n // 텍스트 배경 위치 계산 (중심점 기준)\n const textBgX = -textWidth / 2\n const textBgY = -textHeight / 2\n\n // 텍스트 배경 그리기\n ctx.fillStyle = 'white'\n ctx.fillRect(textBgX, textBgY, textWidth, textHeight)\n\n // 텍스트 그리기\n ctx.fillStyle = 'black'\n ctx.textAlign = 'center'\n ctx.textBaseline = 'middle'\n ctx.fillText(text, 0, textHeight * 0.1) // 텍스트를 약간 아래로 이동\n\n // 변환 상태 복원\n ctx.restore()\n }\n\n ctx.translate(-left, -top)\n }\n }\n}\n"]}
package/dist/crane.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { __decorate } from "tslib";
2
- import { sceneComponent } from '@hatiolab/things-scene';
2
+ import { sceneComponent, frameClock } from '@hatiolab/things-scene';
3
3
  import MCSVehicle from './mcs-vehicle';
4
+ import { axisRatio, mapCraneLocalPosition, ciGet, normalizeCranePosition } from './utils/crane-position';
4
5
  const NATURE = {
5
6
  mutable: false,
6
7
  resizable: true,
@@ -8,6 +9,14 @@ const NATURE = {
8
9
  properties: [...MCSVehicle.properties]
9
10
  };
10
11
  const GAP = 2;
12
+ /** 좌표 보간 기본 duration (ms). data.DURATION_MS 가 없으면 이 값 사용. */
13
+ const DEFAULT_DURATION_MS = 1000;
14
+ function lerp(a, b, t) {
15
+ return a + (b - a) * t;
16
+ }
17
+ function easeInOut(t) {
18
+ return t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2;
19
+ }
11
20
  let Crane = class Crane extends MCSVehicle {
12
21
  static get nature() {
13
22
  return NATURE;
@@ -15,9 +24,141 @@ let Crane = class Crane extends MCSVehicle {
15
24
  get auxColor() {
16
25
  return 'black';
17
26
  }
27
+ /** Crane 의 parentObject 는 STOCKER. */
28
+ get parentObjectTypes() {
29
+ return ['STOCKER'];
30
+ }
18
31
  getLegendFallback() {
19
32
  return this.root.craneLegendTheme || super.getLegendFallback();
20
33
  }
34
+ /**
35
+ * data.cranePosition = { current: {x,y,z}, limit: {x|y|z:{min,max}} } 분기.
36
+ * current 를 limit 범위 안의 0~1 비율로 환산한 뒤, 부모 (보통 Stocker) 기준으로
37
+ * 실 좌표 (state.left/top) 와 forkZ 로 변환해 lerp 시작. x→host 긴쪽, y→짧은쪽,
38
+ * z→포크 높이. 보간 중 새 값이 들어오면 *현재 보간 위치* 를 from 으로 잡고 새
39
+ * target 으로 새 lerp 시작.
40
+ * NODEID 경로는 super (MCSVehicle.onchangeData) 가 처리 — cranePosition 만 있는
41
+ * 데이터에선 NODEID 가 없어 자연스럽게 noop.
42
+ */
43
+ onchangeData(after, before) {
44
+ const afterData = after === null || after === void 0 ? void 0 : after.data;
45
+ // 데이터 명세 키 대소문자 무관: cranePosition / DURATION_MS 및 중첩 키(current/limit/x/y/z/min/max).
46
+ const afterCP = normalizeCranePosition(ciGet(afterData, 'cranePosition'));
47
+ if (afterCP) {
48
+ const beforeCP = normalizeCranePosition(ciGet(before === null || before === void 0 ? void 0 : before.data, 'cranePosition'));
49
+ if (this._cranePositionChanged(afterCP, beforeCP)) {
50
+ this._startPositionAnimation(afterCP, ciGet(afterData, 'DURATION_MS'));
51
+ }
52
+ }
53
+ super.onchangeData(after, before);
54
+ }
55
+ /** current 또는 limit 중 하나라도 바뀌면 재계산 필요 (limit 변화도 실 위치를 바꿈). */
56
+ _cranePositionChanged(a, b) {
57
+ var _a, _b, _c, _d;
58
+ if (!b)
59
+ return true;
60
+ return (JSON.stringify((_a = a === null || a === void 0 ? void 0 : a.current) !== null && _a !== void 0 ? _a : null) !== JSON.stringify((_b = b === null || b === void 0 ? void 0 : b.current) !== null && _b !== void 0 ? _b : null) ||
61
+ JSON.stringify((_c = a === null || a === void 0 ? void 0 : a.limit) !== null && _c !== void 0 ? _c : null) !== JSON.stringify((_d = b === null || b === void 0 ? void 0 : b.limit) !== null && _d !== void 0 ? _d : null));
62
+ }
63
+ dispose() {
64
+ if (this._posAnimator) {
65
+ this._posAnimator.unsubscribe();
66
+ this._posAnimator = undefined;
67
+ }
68
+ super.dispose();
69
+ }
70
+ _startPositionAnimation(cranePosition, durationMs) {
71
+ const target = this._ratioToReal(cranePosition);
72
+ if (!target)
73
+ return; // 부모(host) 없거나 크기 0 이면 변환 skip
74
+ const now = frameClock.simTime;
75
+ const duration = Math.max(1, Number(durationMs) || DEFAULT_DURATION_MS);
76
+ // 현재 위치 — 보간 중이면 그 시점 보간값을 from 으로
77
+ let fromLeft = Number(this.state.left) || 0;
78
+ let fromTop = Number(this.state.top) || 0;
79
+ let fromForkZ = Number(this.state.forkZ) || 0;
80
+ if (this._posAnimator) {
81
+ const a = this._posAnimator;
82
+ const t = Math.min(1, Math.max(0, (now - a.startTime) / a.duration));
83
+ const e = easeInOut(t);
84
+ fromLeft = lerp(a.fromLeft, a.toLeft, e);
85
+ fromTop = lerp(a.fromTop, a.toTop, e);
86
+ fromForkZ = lerp(a.fromForkZ, a.toForkZ, e);
87
+ a.unsubscribe();
88
+ this._posAnimator = undefined;
89
+ }
90
+ const unsubscribe = frameClock.subscribe((_dt, simTime) => this._tickPositionAnimation(simTime), { description: 'crane-position', component: this });
91
+ this._posAnimator = {
92
+ fromLeft,
93
+ fromTop,
94
+ fromForkZ,
95
+ toLeft: target.left,
96
+ toTop: target.top,
97
+ toForkZ: target.forkZ,
98
+ startTime: now,
99
+ duration,
100
+ unsubscribe
101
+ };
102
+ // 즉시 첫 frame 반영 (t=0) — set 한 번 호출. paused 환경에서도 from→target
103
+ // 진입 시점이 명확.
104
+ this._tickPositionAnimation(now);
105
+ }
106
+ _tickPositionAnimation(simTime) {
107
+ const a = this._posAnimator;
108
+ if (!a)
109
+ return;
110
+ const t = Math.min(1, Math.max(0, (simTime - a.startTime) / a.duration));
111
+ const e = easeInOut(t);
112
+ this.set({
113
+ left: lerp(a.fromLeft, a.toLeft, e),
114
+ top: lerp(a.fromTop, a.toTop, e),
115
+ forkZ: lerp(a.fromForkZ, a.toForkZ, e)
116
+ });
117
+ if (t >= 1) {
118
+ a.unsubscribe();
119
+ this._posAnimator = undefined;
120
+ }
121
+ }
122
+ /**
123
+ * cranePosition (current + limit) → state.left/top + forkZ.
124
+ *
125
+ * 부모 오브젝트 (보통은 Stocker 이지만 다른 컨테이너일 수도) 의 width/height
126
+ * 만 사용해 좌표 매핑 — 타입 무관 일반화. 가상 rail 은 host 의 긴쪽:
127
+ * - current.x → host 의 긴쪽 (자동 판별: max(width, height))
128
+ * - current.y → 짧은쪽
129
+ * - current.z → forkZ (state 에 0~100 으로 저장, 3D 측에서 mast 비율로 매핑)
130
+ *
131
+ * 각 축 비율 = (current - limit.min) / (limit.max - limit.min) ∈ [0,1].
132
+ *
133
+ * **스토커 안쪽 오프셋**: 크레인의 폭/두께를 감안해, 크레인 *전체* 가 host 안에
134
+ * 머무르도록 top-left 좌표를 [0, hostLen - craneLen] 범위로 매핑한다 (= 양끝에서
135
+ * craneLen/2 만큼 inset 된 중심선 위를 움직임). 크레인이 host 보다 크면 span 0
136
+ * 으로 떨어져 host 모서리에 정렬.
137
+ *
138
+ * 참조는 ParentObjectMixin 이 제공하는 `parentObject` 사용 —
139
+ * state.parentObjectId 명시 시 그것, 미명시 시 this.parent 자동 fallback.
140
+ */
141
+ _ratioToReal(cp) {
142
+ var _a, _b, _c, _d, _e, _f, _g, _h;
143
+ const host = this.parentObject;
144
+ if (!host)
145
+ return undefined;
146
+ const hw = Number((_a = host.state) === null || _a === void 0 ? void 0 : _a.width) || 0;
147
+ const hh = Number((_b = host.state) === null || _b === void 0 ? void 0 : _b.height) || 0;
148
+ const cw = Number(this.state.width) || 0;
149
+ const ch = Number(this.state.height) || 0;
150
+ if (!hw || !hh)
151
+ return undefined;
152
+ const rx = axisRatio((_c = cp.current) === null || _c === void 0 ? void 0 : _c.x, (_d = cp.limit) === null || _d === void 0 ? void 0 : _d.x);
153
+ const ry = axisRatio((_e = cp.current) === null || _e === void 0 ? void 0 : _e.y, (_f = cp.limit) === null || _f === void 0 ? void 0 : _f.y);
154
+ const rz = axisRatio((_g = cp.current) === null || _g === void 0 ? void 0 : _g.z, (_h = cp.limit) === null || _h === void 0 ? void 0 : _h.z);
155
+ const { left, top } = mapCraneLocalPosition(hw, hh, cw, ch, rx, ry);
156
+ return {
157
+ left,
158
+ top,
159
+ forkZ: rz * 100 // forkZ 0~100, 3D 측 (crane-3d) 에서 mast 비율로 매핑
160
+ };
161
+ }
21
162
  render(ctx) {
22
163
  var { left, top, width, height } = this.bounds;
23
164
  const { x: cx, y: cy } = this.center;
package/dist/crane.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"crane.js","sourceRoot":"","sources":["../src/crane.ts"],"names":[],"mappings":"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","sourcesContent":["import { Component, ComponentNature, sceneComponent } from '@hatiolab/things-scene'\nimport MCSVehicle from './mcs-vehicle'\nimport { LEGEND_CRANE } from './features/mcs-status-default'\n\nconst NATURE: ComponentNature = {\n mutable: false,\n resizable: true,\n rotatable: true,\n properties: [...MCSVehicle.properties]\n}\n\nconst GAP = 2\n\n@sceneComponent('Crane')\nexport default class Crane extends MCSVehicle {\n static get nature() {\n return NATURE\n }\n\n get auxColor() {\n return 'black'\n }\n\n getLegendFallback() {\n return (this.root as any).craneLegendTheme || super.getLegendFallback()\n }\n\n render(ctx: CanvasRenderingContext2D) {\n var { left, top, width, height } = this.bounds\n const { x: cx, y: cy } = this.center\n const rx = width / 2\n const ry = height / 2\n\n const round = Math.min(rx, ry) * 0.5\n\n ctx.translate(left, top)\n ctx.beginPath()\n\n ctx.strokeStyle = this.auxColor\n ctx.fillStyle = this.statusColor!\n ctx.lineWidth = 1\n ctx.lineCap = 'round'\n ctx.setLineDash([])\n\n const radius = Math.round(Math.min(width, height) * 0.1)\n ctx.roundRect(1, 0, width - 2, height, radius)\n ctx.roundRect(GAP + 1, GAP, width - (GAP + 1) * 2, height - GAP * 2, radius - 1)\n ctx.fill()\n ctx.stroke()\n\n ctx.beginPath()\n ctx.arc(cx - left, cy - top, round, 0, Math.PI * 2)\n ctx.stroke()\n\n ctx.beginPath()\n ctx.roundRect(width / 2 - (round * 2) / 4 - round / 8, height / 2 - (round * 6) / 5, round / 4, (round * 6) / 5, 1)\n ctx.roundRect(width / 2 + (round * 2) / 4 - round / 8, height / 2 - (round * 6) / 5, round / 4, (round * 6) / 5, 1)\n\n ctx.fill()\n ctx.stroke()\n\n ctx.beginPath()\n\n // draw crane bar metapore\n ctx.strokeStyle = 'black'\n ctx.lineWidth = 3\n ctx.lineCap = 'butt'\n\n ctx.moveTo(GAP * 3, height / 2)\n ctx.lineTo(width - GAP * 3, height / 2)\n\n ctx.stroke()\n\n ctx.translate(-left, -top)\n }\n}\n"]}
1
+ {"version":3,"file":"crane.js","sourceRoot":"","sources":["../src/crane.ts"],"names":[],"mappings":";AAAA,OAAO,EAA8B,cAAc,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAA;AAC/F,OAAO,UAAU,MAAM,eAAe,CAAA;AAEtC,OAAO,EACL,SAAS,EACT,qBAAqB,EACrB,KAAK,EACL,sBAAsB,EAEvB,MAAM,wBAAwB,CAAA;AAE/B,MAAM,MAAM,GAAoB;IAC9B,OAAO,EAAE,KAAK;IACd,SAAS,EAAE,IAAI;IACf,SAAS,EAAE,IAAI;IACf,UAAU,EAAE,CAAC,GAAG,UAAU,CAAC,UAAU,CAAC;CACvC,CAAA;AAED,MAAM,GAAG,GAAG,CAAC,CAAA;AAEb,6DAA6D;AAC7D,MAAM,mBAAmB,GAAG,IAAI,CAAA;AAEhC,SAAS,IAAI,CAAC,CAAS,EAAE,CAAS,EAAE,CAAS;IAC3C,OAAO,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAA;AACxB,CAAC;AAED,SAAS,SAAS,CAAC,CAAS;IAC1B,OAAO,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,GAAG,CAAC,CAAA;AAC9D,CAAC;AAec,IAAM,KAAK,GAAX,MAAM,KAAM,SAAQ,UAAU;IAC3C,MAAM,KAAK,MAAM;QACf,OAAO,MAAM,CAAA;IACf,CAAC;IAED,IAAI,QAAQ;QACV,OAAO,OAAO,CAAA;IAChB,CAAC;IAED,sCAAsC;IACtC,IAAI,iBAAiB;QACnB,OAAO,CAAC,SAAS,CAAC,CAAA;IACpB,CAAC;IAED,iBAAiB;QACf,OAAQ,IAAI,CAAC,IAAY,CAAC,gBAAgB,IAAI,KAAK,CAAC,iBAAiB,EAAE,CAAA;IACzE,CAAC;IAID;;;;;;;;OAQG;IACH,YAAY,CAAC,KAA0B,EAAE,MAA2B;QAClE,MAAM,SAAS,GAAG,KAAK,aAAL,KAAK,uBAAL,KAAK,CAAE,IAAI,CAAA;QAC7B,qFAAqF;QACrF,MAAM,OAAO,GAAG,sBAAsB,CAAC,KAAK,CAAC,SAAS,EAAE,eAAe,CAAC,CAAC,CAAA;QACzE,IAAI,OAAO,EAAE,CAAC;YACZ,MAAM,QAAQ,GAAG,sBAAsB,CAAC,KAAK,CAAC,MAAM,aAAN,MAAM,uBAAN,MAAM,CAAE,IAAI,EAAE,eAAe,CAAC,CAAC,CAAA;YAC7E,IAAI,IAAI,CAAC,qBAAqB,CAAC,OAAO,EAAE,QAAQ,CAAC,EAAE,CAAC;gBAClD,IAAI,CAAC,uBAAuB,CAAC,OAAO,EAAE,KAAK,CAAC,SAAS,EAAE,aAAa,CAAC,CAAC,CAAA;YACxE,CAAC;QACH,CAAC;QACD,KAAK,CAAC,YAAY,CAAC,KAAK,EAAE,MAAM,CAAC,CAAA;IACnC,CAAC;IAED,+DAA+D;IACvD,qBAAqB,CAAC,CAAoB,EAAE,CAAqB;;QACvE,IAAI,CAAC,CAAC;YAAE,OAAO,IAAI,CAAA;QACnB,OAAO,CACL,IAAI,CAAC,SAAS,CAAC,MAAA,CAAC,aAAD,CAAC,uBAAD,CAAC,CAAE,OAAO,mCAAI,IAAI,CAAC,KAAK,IAAI,CAAC,SAAS,CAAC,MAAA,CAAC,aAAD,CAAC,uBAAD,CAAC,CAAE,OAAO,mCAAI,IAAI,CAAC;YACzE,IAAI,CAAC,SAAS,CAAC,MAAA,CAAC,aAAD,CAAC,uBAAD,CAAC,CAAE,KAAK,mCAAI,IAAI,CAAC,KAAK,IAAI,CAAC,SAAS,CAAC,MAAA,CAAC,aAAD,CAAC,uBAAD,CAAC,CAAE,KAAK,mCAAI,IAAI,CAAC,CACtE,CAAA;IACH,CAAC;IAED,OAAO;QACL,IAAI,IAAI,CAAC,YAAY,EAAE,CAAC;YACtB,IAAI,CAAC,YAAY,CAAC,WAAW,EAAE,CAAA;YAC/B,IAAI,CAAC,YAAY,GAAG,SAAS,CAAA;QAC/B,CAAC;QACD,KAAK,CAAC,OAAO,EAAE,CAAA;IACjB,CAAC;IAEO,uBAAuB,CAAC,aAAgC,EAAE,UAAmB;QACnF,MAAM,MAAM,GAAG,IAAI,CAAC,YAAY,CAAC,aAAa,CAAC,CAAA;QAC/C,IAAI,CAAC,MAAM;YAAE,OAAM,CAAC,+BAA+B;QAEnD,MAAM,GAAG,GAAG,UAAU,CAAC,OAAO,CAAA;QAC9B,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC,IAAI,mBAAmB,CAAC,CAAA;QAEvE,mCAAmC;QACnC,IAAI,QAAQ,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QAC3C,IAAI,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;QACzC,IAAI,SAAS,GAAG,MAAM,CAAE,IAAI,CAAC,KAAa,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;QAEtD,IAAI,IAAI,CAAC,YAAY,EAAE,CAAC;YACtB,MAAM,CAAC,GAAG,IAAI,CAAC,YAAY,CAAA;YAC3B,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,GAAG,GAAG,CAAC,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAA;YACpE,MAAM,CAAC,GAAG,SAAS,CAAC,CAAC,CAAC,CAAA;YACtB,QAAQ,GAAG,IAAI,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,CAAA;YACxC,OAAO,GAAG,IAAI,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,CAAA;YACrC,SAAS,GAAG,IAAI,CAAC,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,CAAA;YAC3C,CAAC,CAAC,WAAW,EAAE,CAAA;YACf,IAAI,CAAC,YAAY,GAAG,SAAS,CAAA;QAC/B,CAAC;QAED,MAAM,WAAW,GAAG,UAAU,CAAC,SAAS,CACtC,CAAC,GAAW,EAAE,OAAe,EAAE,EAAE,CAAC,IAAI,CAAC,sBAAsB,CAAC,OAAO,CAAC,EACtE,EAAE,WAAW,EAAE,gBAAgB,EAAE,SAAS,EAAE,IAAI,EAAE,CACnD,CAAA;QAED,IAAI,CAAC,YAAY,GAAG;YAClB,QAAQ;YACR,OAAO;YACP,SAAS;YACT,MAAM,EAAE,MAAM,CAAC,IAAI;YACnB,KAAK,EAAE,MAAM,CAAC,GAAG;YACjB,OAAO,EAAE,MAAM,CAAC,KAAK;YACrB,SAAS,EAAE,GAAG;YACd,QAAQ;YACR,WAAW;SACZ,CAAA;QACD,6DAA6D;QAC7D,aAAa;QACb,IAAI,CAAC,sBAAsB,CAAC,GAAG,CAAC,CAAA;IAClC,CAAC;IAEO,sBAAsB,CAAC,OAAe;QAC5C,MAAM,CAAC,GAAG,IAAI,CAAC,YAAY,CAAA;QAC3B,IAAI,CAAC,CAAC;YAAE,OAAM;QACd,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,OAAO,GAAG,CAAC,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAA;QACxE,MAAM,CAAC,GAAG,SAAS,CAAC,CAAC,CAAC,CAAA;QACtB,IAAI,CAAC,GAAG,CAAC;YACP,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC;YACnC,GAAG,EAAE,IAAI,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC;YAChC,KAAK,EAAE,IAAI,CAAC,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC;SACvC,CAAC,CAAA;QACF,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;YACX,CAAC,CAAC,WAAW,EAAE,CAAA;YACf,IAAI,CAAC,YAAY,GAAG,SAAS,CAAA;QAC/B,CAAC;IACH,CAAC;IAED;;;;;;;;;;;;;;;;;;OAkBG;IACK,YAAY,CAClB,EAAqB;;QAErB,MAAM,IAAI,GAAS,IAAY,CAAC,YAAY,CAAA;QAC5C,IAAI,CAAC,IAAI;YAAE,OAAO,SAAS,CAAA;QAE3B,MAAM,EAAE,GAAG,MAAM,CAAC,MAAA,IAAI,CAAC,KAAK,0CAAE,KAAK,CAAC,IAAI,CAAC,CAAA;QACzC,MAAM,EAAE,GAAG,MAAM,CAAC,MAAA,IAAI,CAAC,KAAK,0CAAE,MAAM,CAAC,IAAI,CAAC,CAAA;QAC1C,MAAM,EAAE,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;QACxC,MAAM,EAAE,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;QACzC,IAAI,CAAC,EAAE,IAAI,CAAC,EAAE;YAAE,OAAO,SAAS,CAAA;QAEhC,MAAM,EAAE,GAAG,SAAS,CAAC,MAAA,EAAE,CAAC,OAAO,0CAAE,CAAC,EAAE,MAAA,EAAE,CAAC,KAAK,0CAAE,CAAC,CAAC,CAAA;QAChD,MAAM,EAAE,GAAG,SAAS,CAAC,MAAA,EAAE,CAAC,OAAO,0CAAE,CAAC,EAAE,MAAA,EAAE,CAAC,KAAK,0CAAE,CAAC,CAAC,CAAA;QAChD,MAAM,EAAE,GAAG,SAAS,CAAC,MAAA,EAAE,CAAC,OAAO,0CAAE,CAAC,EAAE,MAAA,EAAE,CAAC,KAAK,0CAAE,CAAC,CAAC,CAAA;QAEhD,MAAM,EAAE,IAAI,EAAE,GAAG,EAAE,GAAG,qBAAqB,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,CAAA;QAEnE,OAAO;YACL,IAAI;YACJ,GAAG;YACH,KAAK,EAAE,EAAE,GAAG,GAAG,CAAC,8CAA8C;SAC/D,CAAA;IACH,CAAC;IAED,MAAM,CAAC,GAA6B;QAClC,IAAI,EAAE,IAAI,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC,MAAM,CAAA;QAC9C,MAAM,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,EAAE,EAAE,GAAG,IAAI,CAAC,MAAM,CAAA;QACpC,MAAM,EAAE,GAAG,KAAK,GAAG,CAAC,CAAA;QACpB,MAAM,EAAE,GAAG,MAAM,GAAG,CAAC,CAAA;QAErB,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,EAAE,CAAC,GAAG,GAAG,CAAA;QAEpC,GAAG,CAAC,SAAS,CAAC,IAAI,EAAE,GAAG,CAAC,CAAA;QACxB,GAAG,CAAC,SAAS,EAAE,CAAA;QAEf,GAAG,CAAC,WAAW,GAAG,IAAI,CAAC,QAAQ,CAAA;QAC/B,GAAG,CAAC,SAAS,GAAG,IAAI,CAAC,WAAY,CAAA;QACjC,GAAG,CAAC,SAAS,GAAG,CAAC,CAAA;QACjB,GAAG,CAAC,OAAO,GAAG,OAAO,CAAA;QACrB,GAAG,CAAC,WAAW,CAAC,EAAE,CAAC,CAAA;QAEnB,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,MAAM,CAAC,GAAG,GAAG,CAAC,CAAA;QACxD,GAAG,CAAC,SAAS,CAAC,CAAC,EAAE,CAAC,EAAE,KAAK,GAAG,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,CAAA;QAC9C,GAAG,CAAC,SAAS,CAAC,GAAG,GAAG,CAAC,EAAE,GAAG,EAAE,KAAK,GAAG,CAAC,GAAG,GAAG,CAAC,CAAC,GAAG,CAAC,EAAE,MAAM,GAAG,GAAG,GAAG,CAAC,EAAE,MAAM,GAAG,CAAC,CAAC,CAAA;QAChF,GAAG,CAAC,IAAI,EAAE,CAAA;QACV,GAAG,CAAC,MAAM,EAAE,CAAA;QAEZ,GAAG,CAAC,SAAS,EAAE,CAAA;QACf,GAAG,CAAC,GAAG,CAAC,EAAE,GAAG,IAAI,EAAE,EAAE,GAAG,GAAG,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,CAAC,EAAE,GAAG,CAAC,CAAC,CAAA;QACnD,GAAG,CAAC,MAAM,EAAE,CAAA;QAEZ,GAAG,CAAC,SAAS,EAAE,CAAA;QACf,GAAG,CAAC,SAAS,CAAC,KAAK,GAAG,CAAC,GAAG,CAAC,KAAK,GAAG,CAAC,CAAC,GAAG,CAAC,GAAG,KAAK,GAAG,CAAC,EAAE,MAAM,GAAG,CAAC,GAAG,CAAC,KAAK,GAAG,CAAC,CAAC,GAAG,CAAC,EAAE,KAAK,GAAG,CAAC,EAAE,CAAC,KAAK,GAAG,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAA;QACnH,GAAG,CAAC,SAAS,CAAC,KAAK,GAAG,CAAC,GAAG,CAAC,KAAK,GAAG,CAAC,CAAC,GAAG,CAAC,GAAG,KAAK,GAAG,CAAC,EAAE,MAAM,GAAG,CAAC,GAAG,CAAC,KAAK,GAAG,CAAC,CAAC,GAAG,CAAC,EAAE,KAAK,GAAG,CAAC,EAAE,CAAC,KAAK,GAAG,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAA;QAEnH,GAAG,CAAC,IAAI,EAAE,CAAA;QACV,GAAG,CAAC,MAAM,EAAE,CAAA;QAEZ,GAAG,CAAC,SAAS,EAAE,CAAA;QAEf,0BAA0B;QAC1B,GAAG,CAAC,WAAW,GAAG,OAAO,CAAA;QACzB,GAAG,CAAC,SAAS,GAAG,CAAC,CAAA;QACjB,GAAG,CAAC,OAAO,GAAG,MAAM,CAAA;QAEpB,GAAG,CAAC,MAAM,CAAC,GAAG,GAAG,CAAC,EAAE,MAAM,GAAG,CAAC,CAAC,CAAA;QAC/B,GAAG,CAAC,MAAM,CAAC,KAAK,GAAG,GAAG,GAAG,CAAC,EAAE,MAAM,GAAG,CAAC,CAAC,CAAA;QAEvC,GAAG,CAAC,MAAM,EAAE,CAAA;QAEZ,GAAG,CAAC,SAAS,CAAC,CAAC,IAAI,EAAE,CAAC,GAAG,CAAC,CAAA;IAC5B,CAAC;CACF,CAAA;AAnNoB,KAAK;IADzB,cAAc,CAAC,OAAO,CAAC;GACH,KAAK,CAmNzB;eAnNoB,KAAK","sourcesContent":["import { Component, ComponentNature, sceneComponent, frameClock } from '@hatiolab/things-scene'\nimport MCSVehicle from './mcs-vehicle'\nimport { LEGEND_CRANE } from './features/mcs-status-default'\nimport {\n axisRatio,\n mapCraneLocalPosition,\n ciGet,\n normalizeCranePosition,\n type CranePositionData\n} from './utils/crane-position'\n\nconst NATURE: ComponentNature = {\n mutable: false,\n resizable: true,\n rotatable: true,\n properties: [...MCSVehicle.properties]\n}\n\nconst GAP = 2\n\n/** 좌표 보간 기본 duration (ms). data.DURATION_MS 가 없으면 이 값 사용. */\nconst DEFAULT_DURATION_MS = 1000\n\nfunction lerp(a: number, b: number, t: number): number {\n return a + (b - a) * t\n}\n\nfunction easeInOut(t: number): number {\n return t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2\n}\n\ninterface CranePositionAnimator {\n fromLeft: number\n fromTop: number\n fromForkZ: number\n toLeft: number\n toTop: number\n toForkZ: number\n startTime: number\n duration: number\n unsubscribe: () => void\n}\n\n@sceneComponent('Crane')\nexport default class Crane extends MCSVehicle {\n static get nature() {\n return NATURE\n }\n\n get auxColor() {\n return 'black'\n }\n\n /** Crane 의 parentObject 는 STOCKER. */\n get parentObjectTypes(): string[] {\n return ['STOCKER']\n }\n\n getLegendFallback() {\n return (this.root as any).craneLegendTheme || super.getLegendFallback()\n }\n\n private _posAnimator?: CranePositionAnimator\n\n /**\n * data.cranePosition = { current: {x,y,z}, limit: {x|y|z:{min,max}} } 분기.\n * current 를 limit 범위 안의 0~1 비율로 환산한 뒤, 부모 (보통 Stocker) 기준으로\n * 실 좌표 (state.left/top) 와 forkZ 로 변환해 lerp 시작. x→host 긴쪽, y→짧은쪽,\n * z→포크 높이. 보간 중 새 값이 들어오면 *현재 보간 위치* 를 from 으로 잡고 새\n * target 으로 새 lerp 시작.\n * NODEID 경로는 super (MCSVehicle.onchangeData) 가 처리 — cranePosition 만 있는\n * 데이터에선 NODEID 가 없어 자연스럽게 noop.\n */\n onchangeData(after: Record<string, any>, before: Record<string, any>): void {\n const afterData = after?.data\n // 데이터 명세 키 대소문자 무관: cranePosition / DURATION_MS 및 중첩 키(current/limit/x/y/z/min/max).\n const afterCP = normalizeCranePosition(ciGet(afterData, 'cranePosition'))\n if (afterCP) {\n const beforeCP = normalizeCranePosition(ciGet(before?.data, 'cranePosition'))\n if (this._cranePositionChanged(afterCP, beforeCP)) {\n this._startPositionAnimation(afterCP, ciGet(afterData, 'DURATION_MS'))\n }\n }\n super.onchangeData(after, before)\n }\n\n /** current 또는 limit 중 하나라도 바뀌면 재계산 필요 (limit 변화도 실 위치를 바꿈). */\n private _cranePositionChanged(a: CranePositionData, b?: CranePositionData): boolean {\n if (!b) return true\n return (\n JSON.stringify(a?.current ?? null) !== JSON.stringify(b?.current ?? null) ||\n JSON.stringify(a?.limit ?? null) !== JSON.stringify(b?.limit ?? null)\n )\n }\n\n dispose() {\n if (this._posAnimator) {\n this._posAnimator.unsubscribe()\n this._posAnimator = undefined\n }\n super.dispose()\n }\n\n private _startPositionAnimation(cranePosition: CranePositionData, durationMs?: number): void {\n const target = this._ratioToReal(cranePosition)\n if (!target) return // 부모(host) 없거나 크기 0 이면 변환 skip\n\n const now = frameClock.simTime\n const duration = Math.max(1, Number(durationMs) || DEFAULT_DURATION_MS)\n\n // 현재 위치 — 보간 중이면 그 시점 보간값을 from 으로\n let fromLeft = Number(this.state.left) || 0\n let fromTop = Number(this.state.top) || 0\n let fromForkZ = Number((this.state as any).forkZ) || 0\n\n if (this._posAnimator) {\n const a = this._posAnimator\n const t = Math.min(1, Math.max(0, (now - a.startTime) / a.duration))\n const e = easeInOut(t)\n fromLeft = lerp(a.fromLeft, a.toLeft, e)\n fromTop = lerp(a.fromTop, a.toTop, e)\n fromForkZ = lerp(a.fromForkZ, a.toForkZ, e)\n a.unsubscribe()\n this._posAnimator = undefined\n }\n\n const unsubscribe = frameClock.subscribe(\n (_dt: number, simTime: number) => this._tickPositionAnimation(simTime),\n { description: 'crane-position', component: this }\n )\n\n this._posAnimator = {\n fromLeft,\n fromTop,\n fromForkZ,\n toLeft: target.left,\n toTop: target.top,\n toForkZ: target.forkZ,\n startTime: now,\n duration,\n unsubscribe\n }\n // 즉시 첫 frame 반영 (t=0) — set 한 번 호출. paused 환경에서도 from→target\n // 진입 시점이 명확.\n this._tickPositionAnimation(now)\n }\n\n private _tickPositionAnimation(simTime: number): void {\n const a = this._posAnimator\n if (!a) return\n const t = Math.min(1, Math.max(0, (simTime - a.startTime) / a.duration))\n const e = easeInOut(t)\n this.set({\n left: lerp(a.fromLeft, a.toLeft, e),\n top: lerp(a.fromTop, a.toTop, e),\n forkZ: lerp(a.fromForkZ, a.toForkZ, e)\n })\n if (t >= 1) {\n a.unsubscribe()\n this._posAnimator = undefined\n }\n }\n\n /**\n * cranePosition (current + limit) → state.left/top + forkZ.\n *\n * 부모 오브젝트 (보통은 Stocker 이지만 다른 컨테이너일 수도) 의 width/height\n * 만 사용해 좌표 매핑 — 타입 무관 일반화. 가상 rail 은 host 의 긴쪽:\n * - current.x → host 의 긴쪽 (자동 판별: max(width, height))\n * - current.y → 짧은쪽\n * - current.z → forkZ (state 에 0~100 으로 저장, 3D 측에서 mast 비율로 매핑)\n *\n * 각 축 비율 = (current - limit.min) / (limit.max - limit.min) ∈ [0,1].\n *\n * **스토커 안쪽 오프셋**: 크레인의 폭/두께를 감안해, 크레인 *전체* 가 host 안에\n * 머무르도록 top-left 좌표를 [0, hostLen - craneLen] 범위로 매핑한다 (= 양끝에서\n * craneLen/2 만큼 inset 된 중심선 위를 움직임). 크레인이 host 보다 크면 span 0\n * 으로 떨어져 host 모서리에 정렬.\n *\n * 참조는 ParentObjectMixin 이 제공하는 `parentObject` 사용 —\n * state.parentObjectId 명시 시 그것, 미명시 시 this.parent 자동 fallback.\n */\n private _ratioToReal(\n cp: CranePositionData\n ): { left: number; top: number; forkZ: number } | undefined {\n const host: any = (this as any).parentObject\n if (!host) return undefined\n\n const hw = Number(host.state?.width) || 0\n const hh = Number(host.state?.height) || 0\n const cw = Number(this.state.width) || 0\n const ch = Number(this.state.height) || 0\n if (!hw || !hh) return undefined\n\n const rx = axisRatio(cp.current?.x, cp.limit?.x)\n const ry = axisRatio(cp.current?.y, cp.limit?.y)\n const rz = axisRatio(cp.current?.z, cp.limit?.z)\n\n const { left, top } = mapCraneLocalPosition(hw, hh, cw, ch, rx, ry)\n\n return {\n left,\n top,\n forkZ: rz * 100 // forkZ 0~100, 3D 측 (crane-3d) 에서 mast 비율로 매핑\n }\n }\n\n render(ctx: CanvasRenderingContext2D) {\n var { left, top, width, height } = this.bounds\n const { x: cx, y: cy } = this.center\n const rx = width / 2\n const ry = height / 2\n\n const round = Math.min(rx, ry) * 0.5\n\n ctx.translate(left, top)\n ctx.beginPath()\n\n ctx.strokeStyle = this.auxColor\n ctx.fillStyle = this.statusColor!\n ctx.lineWidth = 1\n ctx.lineCap = 'round'\n ctx.setLineDash([])\n\n const radius = Math.round(Math.min(width, height) * 0.1)\n ctx.roundRect(1, 0, width - 2, height, radius)\n ctx.roundRect(GAP + 1, GAP, width - (GAP + 1) * 2, height - GAP * 2, radius - 1)\n ctx.fill()\n ctx.stroke()\n\n ctx.beginPath()\n ctx.arc(cx - left, cy - top, round, 0, Math.PI * 2)\n ctx.stroke()\n\n ctx.beginPath()\n ctx.roundRect(width / 2 - (round * 2) / 4 - round / 8, height / 2 - (round * 6) / 5, round / 4, (round * 6) / 5, 1)\n ctx.roundRect(width / 2 + (round * 2) / 4 - round / 8, height / 2 - (round * 6) / 5, round / 4, (round * 6) / 5, 1)\n\n ctx.fill()\n ctx.stroke()\n\n ctx.beginPath()\n\n // draw crane bar metapore\n ctx.strokeStyle = 'black'\n ctx.lineWidth = 3\n ctx.lineCap = 'butt'\n\n ctx.moveTo(GAP * 3, height / 2)\n ctx.lineTo(width - GAP * 3, height / 2)\n\n ctx.stroke()\n\n ctx.translate(-left, -top)\n }\n}\n"]}
@@ -18,10 +18,26 @@ const CLAMP_COLOR = 0x444444;
18
18
  const FORK_COLOR = 0x999999;
19
19
  const DEFAULT_BODY_COLOR = 0xcc8844;
20
20
  export class Crane3D extends RealObjectGroup {
21
+ constructor() {
22
+ super(...arguments);
23
+ this.forkMastBaseY = 0;
24
+ this.forkMastH = 0;
25
+ }
21
26
  get effectiveDepth() {
22
27
  const { depth } = this.component.state;
23
28
  return depth || 5 * TRANSPORT_SURFACE_HEIGHT - 2;
24
29
  }
30
+ /**
31
+ * 0~100 정규화 z (= state.forkZ) 를 mast 높이 비율로 변환해서 포크 group 의
32
+ * three-up y 위치 갱신. build() 가 안 끝났으면 noop — build 가 forkZ 를 다시
33
+ * 반영하면서 끝난다.
34
+ */
35
+ applyForkZ(z) {
36
+ if (!this.forkGroup)
37
+ return;
38
+ const clamped = Math.max(0, Math.min(100, Number(z) || 0));
39
+ this.forkGroup.position.y = this.forkMastBaseY + (clamped / 100) * this.forkMastH;
40
+ }
25
41
  resolveColor() {
26
42
  const comp = this.component;
27
43
  try {
@@ -97,22 +113,22 @@ export class Crane3D extends RealObjectGroup {
97
113
  const mastMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(mastGeometries), mastMat);
98
114
  mastMesh.castShadow = true;
99
115
  inner.add(mastMesh);
100
- // ── 3. 캐리지 포크 (마스트 40% 높이, 양쪽으로 뻗는 포크 암) ──
101
- const forkY = mastBaseY + mastH * 0.4;
116
+ // ── 3. 캐리지 포크 (별도 group — state.forkZ 변경 시 group.position.y lerp) ──
117
+ // 포크 geometry 는 *_mastBaseY 기준 local 0 에 둠* — 즉 group.position.y = 0
118
+ // 일 때 mast 바닥 자리에. forkZ 0~100 에 따라 group.position.y 만 변경하면
119
+ // mast 따라 위/아래 이동. mesh 재구축 불필요.
102
120
  const forkMat = new THREE.MeshStandardMaterial({
103
121
  color: FORK_COLOR,
104
122
  roughness: 0.4,
105
123
  metalness: 0.5,
106
124
  });
107
125
  const forkGeometries = [];
108
- // 캐리지 중심 플레이트 (마스트에 부착, 양쪽 포크의 베이스)
109
126
  const plateW = w * 0.9;
110
127
  const plateH = d * 0.08;
111
128
  const plateD = pillarThick * 0.8;
112
129
  const centerPlate = new THREE.BoxGeometry(plateW, plateH, plateD);
113
- centerPlate.translate(0, forkY + plateH / 2, 0);
130
+ centerPlate.translate(0, plateH / 2, 0); // local 0 = mast 바닥
114
131
  forkGeometries.push(centerPlate);
115
- // 양쪽 포크 암 (좌/우 선반 방향으로 뻗음)
116
132
  const armW = pillarThick * 0.6;
117
133
  const armH = plateH * 0.4;
118
134
  const armD = h * 0.6;
@@ -120,13 +136,24 @@ export class Crane3D extends RealObjectGroup {
120
136
  for (const zSign of [-1, 1]) {
121
137
  for (const xSign of [-1, 1]) {
122
138
  const arm = new THREE.BoxGeometry(armW, armH, armD);
123
- arm.translate(xSign * (armSpacing / 2), forkY + armH / 2, zSign * (plateD / 2 + armD / 2));
139
+ arm.translate(xSign * (armSpacing / 2), armH / 2, zSign * (plateD / 2 + armD / 2));
124
140
  forkGeometries.push(arm);
125
141
  }
126
142
  }
127
143
  const forkMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(forkGeometries), forkMat);
128
144
  forkMesh.castShadow = true;
129
- inner.add(forkMesh);
145
+ const forkGroup = new THREE.Group();
146
+ forkGroup.add(forkMesh);
147
+ inner.add(forkGroup);
148
+ this.forkGroup = forkGroup;
149
+ this.forkMastBaseY = mastBaseY;
150
+ // forkZ=100 시점에 mast 의 *_위쪽 한계*. 포크 자체 두께 (plateH) 만큼은 마스트
151
+ // 안에 머무르도록 limit 보정 — 그렇지 않으면 forkZ=100 일 때 포크가 상단
152
+ // 클램프와 충돌. mastH * 0.92 로 약간 보수적 (= 마스트 본체 92% 까지만 이동).
153
+ this.forkMastH = mastH * 0.92;
154
+ // 초기 forkZ 반영 — state.forkZ 가 이미 set 되어 있으면 그 위치에 배치
155
+ const initialForkZ = Number(this.component.state.forkZ);
156
+ this.applyForkZ(Number.isFinite(initialForkZ) ? initialForkZ : 0);
130
157
  // ── 4. 상단 클램프 (천장 레일에 물리는 가이드부) ──
131
158
  const topClampGeo = new THREE.BoxGeometry(clampW, clampH, clampD);
132
159
  const topClamp = new THREE.Mesh(topClampGeo, clampMat);
@@ -136,11 +163,22 @@ export class Crane3D extends RealObjectGroup {
136
163
  }
137
164
  updateDimension() { }
138
165
  updateAlpha() { }
166
+ /** build() 직전 호출. forkGroup 참조를 깨끗이 비워둔다 — 새 build 가 다시 set. */
167
+ clear() {
168
+ this.forkGroup = undefined;
169
+ super.clear();
170
+ }
139
171
  onchange(after, before) {
140
172
  if ('width' in after || 'height' in after || 'depth' in after || 'fillStyle' in after || 'strokeStyle' in after) {
141
173
  this.update();
142
174
  return;
143
175
  }
176
+ // forkZ 는 mesh 재구축 없이 group.position.y 만 갱신. 같은 onchange 에서
177
+ // left/top 도 함께 변경될 수 있으므로 super 도 반드시 호출해 본체 transform
178
+ // 갱신 path (state.left/top → obj3d.position.x/z) 가 동작하게 함.
179
+ if ('forkZ' in after) {
180
+ this.applyForkZ(Number(after.forkZ) || 0);
181
+ }
144
182
  super.onchange(after, before);
145
183
  }
146
184
  }
@@ -1 +1 @@
1
- {"version":3,"file":"crane-3d.js","sourceRoot":"","sources":["../../src/factories/crane-3d.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Crane (Stacker Crane) 3D Factory\n *\n * 스토커 내부의 스태커 크레인 — CraneRail 위를 이동하는 차체.\n * 레일 인프라는 CraneRail이 담당하므로 크레인은 이동체만 표현.\n *\n * 구조 (바닥 원점, y=0~depth):\n * - 하단 바퀴/클램프 (4%, 바닥 레일에 물리는 부분)\n * - 수직 마스트 (좌우 기둥 88%, statusColor 적용)\n * - 캐리지 포크 (마스트 40% 높이, 양쪽 포크 암)\n * - 상단 클램프 (4%, 천장 레일에 물리는 부분)\n */\n\nimport * as THREE from 'three'\nimport * as BufferGeometryUtils from 'three/examples/jsm/utils/BufferGeometryUtils.js'\nimport { RealObjectGroup, registerRealObjectFactory } from '@hatiolab/things-scene'\nimport type { Component } from '@hatiolab/things-scene'\nimport { TRANSPORT_SURFACE_HEIGHT } from './machine-3d.js'\n\nconst CLAMP_COLOR = 0x444444\nconst FORK_COLOR = 0x999999\nconst DEFAULT_BODY_COLOR = 0xcc8844\n\nexport class Crane3D extends RealObjectGroup {\n get effectiveDepth(): number {\n const { depth } = this.component.state\n return depth || 5 * TRANSPORT_SURFACE_HEIGHT - 2\n }\n\n private resolveColor(): number {\n const comp = this.component as any\n\n try {\n const sc = comp.statusColor\n if (typeof sc === 'string' && sc && sc.toUpperCase() !== '#F0F0F0') return new THREE.Color(sc).getHex()\n } catch {\n /* statusColor 미구현 시 무시 */\n }\n\n const { fillStyle } = this.component.state\n if (fillStyle && typeof fillStyle === 'string') {\n try {\n return new THREE.Color(fillStyle).getHex()\n } catch {\n /* 파싱 실패 시 무시 */\n }\n }\n\n return DEFAULT_BODY_COLOR\n }\n\n build() {\n super.build()\n\n const { width = 10, height = 10 } = this.component.state\n const w = Math.abs(width)\n const h = Math.abs(height)\n const d = this.effectiveDepth\n if (!w || !h) return\n\n const inner = this.components3D\n\n // 높이 배분: 하단 클램프 4%, 마스트 92%, 상단 클램프 4%\n const clampH = d * 0.04\n const mastH = d * 0.92\n\n const pillarThick = Math.min(w, h) * 0.12\n\n const clampMat = new THREE.MeshStandardMaterial({\n color: CLAMP_COLOR,\n roughness: 0.5,\n metalness: 0.5,\n })\n\n // ── 1. 하단 클램프 (바닥 레일에 물리는 바퀴부) ──\n const clampW = w * 0.7\n const clampD = h * 0.5\n const bottomClampGeo = new THREE.BoxGeometry(clampW, clampH, clampD)\n const bottomClamp = new THREE.Mesh(bottomClampGeo, clampMat)\n bottomClamp.position.y = clampH / 2\n bottomClamp.castShadow = true\n inner.add(bottomClamp)\n\n // ── 2. 수직 마스트 (좌우 기둥 + 상·하 가로 연결 바) ──\n const mastBaseY = clampH\n const bodyColor = this.resolveColor()\n const mastMat = new THREE.MeshStandardMaterial({\n color: bodyColor,\n roughness: 0.3,\n metalness: 0.6,\n })\n\n const mastGeometries: THREE.BufferGeometry[] = []\n\n // 좌우 수직 기둥\n for (const xSign of [-1, 1]) {\n const pillar = new THREE.BoxGeometry(pillarThick, mastH, pillarThick)\n pillar.translate(xSign * (w / 2 - pillarThick / 2), mastBaseY + mastH / 2, 0)\n mastGeometries.push(pillar)\n }\n\n // 하단 가로 연결 바\n const crossW = w - pillarThick\n const crossH = pillarThick * 0.5\n const lowerCross = new THREE.BoxGeometry(crossW, crossH, pillarThick)\n lowerCross.translate(0, mastBaseY + crossH / 2, 0)\n mastGeometries.push(lowerCross)\n\n // 상단 가로 연결 바\n const upperCross = new THREE.BoxGeometry(crossW, crossH, pillarThick)\n upperCross.translate(0, mastBaseY + mastH - crossH / 2, 0)\n mastGeometries.push(upperCross)\n\n const mastMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(mastGeometries), mastMat)\n mastMesh.castShadow = true\n inner.add(mastMesh)\n\n // ── 3. 캐리지 포크 (마스트 40% 높이, 양쪽으로 뻗는 포크 암) ──\n const forkY = mastBaseY + mastH * 0.4\n const forkMat = new THREE.MeshStandardMaterial({\n color: FORK_COLOR,\n roughness: 0.4,\n metalness: 0.5,\n })\n\n const forkGeometries: THREE.BufferGeometry[] = []\n\n // 캐리지 중심 플레이트 (마스트에 부착, 양쪽 포크의 베이스)\n const plateW = w * 0.9\n const plateH = d * 0.08\n const plateD = pillarThick * 0.8\n const centerPlate = new THREE.BoxGeometry(plateW, plateH, plateD)\n centerPlate.translate(0, forkY + plateH / 2, 0)\n forkGeometries.push(centerPlate)\n\n // 양쪽 포크 암 (좌/우 선반 방향으로 뻗음)\n const armW = pillarThick * 0.6\n const armH = plateH * 0.4\n const armD = h * 0.6\n const armSpacing = w * 0.5\n\n for (const zSign of [-1, 1]) {\n for (const xSign of [-1, 1]) {\n const arm = new THREE.BoxGeometry(armW, armH, armD)\n arm.translate(xSign * (armSpacing / 2), forkY + armH / 2, zSign * (plateD / 2 + armD / 2))\n forkGeometries.push(arm)\n }\n }\n\n const forkMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(forkGeometries), forkMat)\n forkMesh.castShadow = true\n inner.add(forkMesh)\n\n // ── 4. 상단 클램프 (천장 레일에 물리는 가이드부) ──\n const topClampGeo = new THREE.BoxGeometry(clampW, clampH, clampD)\n const topClamp = new THREE.Mesh(topClampGeo, clampMat)\n topClamp.position.y = mastBaseY + mastH + clampH / 2\n topClamp.castShadow = true\n inner.add(topClamp)\n }\n\n updateDimension() {}\n updateAlpha() {}\n\n onchange(after: Record<string, unknown>, before: Record<string, unknown>) {\n if ('width' in after || 'height' in after || 'depth' in after || 'fillStyle' in after || 'strokeStyle' in after) {\n this.update()\n return\n }\n super.onchange(after, before)\n }\n}\n\nregisterRealObjectFactory('Crane', (component: Component) => new Crane3D(component))\n"]}
1
+ {"version":3,"file":"crane-3d.js","sourceRoot":"","sources":["../../src/factories/crane-3d.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Crane (Stacker Crane) 3D Factory\n *\n * 스토커 내부의 스태커 크레인 — CraneRail 위를 이동하는 차체.\n * 레일 인프라는 CraneRail이 담당하므로 크레인은 이동체만 표현.\n *\n * 구조 (바닥 원점, y=0~depth):\n * - 하단 바퀴/클램프 (4%, 바닥 레일에 물리는 부분)\n * - 수직 마스트 (좌우 기둥 88%, statusColor 적용)\n * - 캐리지 포크 (마스트 40% 높이, 양쪽 포크 암)\n * - 상단 클램프 (4%, 천장 레일에 물리는 부분)\n */\n\nimport * as THREE from 'three'\nimport * as BufferGeometryUtils from 'three/examples/jsm/utils/BufferGeometryUtils.js'\nimport { RealObjectGroup, registerRealObjectFactory } from '@hatiolab/things-scene'\nimport type { Component } from '@hatiolab/things-scene'\nimport { TRANSPORT_SURFACE_HEIGHT } from './machine-3d.js'\n\nconst CLAMP_COLOR = 0x444444\nconst FORK_COLOR = 0x999999\nconst DEFAULT_BODY_COLOR = 0xcc8844\n\nexport class Crane3D extends RealObjectGroup {\n /** 캐리지 포크 group — Crane.state.forkZ (0~100) 가 변하면 position.y 만 lerp.\n * build() 시 inner 에 add 되고, *_mastBaseY 와 mastH 를 함께 보관* 해서\n * setForkZ 가 비율 계산에 사용. */\n private forkGroup?: THREE.Group\n private forkMastBaseY = 0\n private forkMastH = 0\n\n get effectiveDepth(): number {\n const { depth } = this.component.state\n return depth || 5 * TRANSPORT_SURFACE_HEIGHT - 2\n }\n\n /**\n * 0~100 정규화 z (= state.forkZ) 를 mast 높이 비율로 변환해서 포크 group 의\n * three-up y 위치 갱신. build() 가 안 끝났으면 noop — build 가 forkZ 를 다시\n * 반영하면서 끝난다.\n */\n applyForkZ(z: number): void {\n if (!this.forkGroup) return\n const clamped = Math.max(0, Math.min(100, Number(z) || 0))\n this.forkGroup.position.y = this.forkMastBaseY + (clamped / 100) * this.forkMastH\n }\n\n private resolveColor(): number {\n const comp = this.component as any\n\n try {\n const sc = comp.statusColor\n if (typeof sc === 'string' && sc && sc.toUpperCase() !== '#F0F0F0') return new THREE.Color(sc).getHex()\n } catch {\n /* statusColor 미구현 시 무시 */\n }\n\n const { fillStyle } = this.component.state\n if (fillStyle && typeof fillStyle === 'string') {\n try {\n return new THREE.Color(fillStyle).getHex()\n } catch {\n /* 파싱 실패 시 무시 */\n }\n }\n\n return DEFAULT_BODY_COLOR\n }\n\n build() {\n super.build()\n\n const { width = 10, height = 10 } = this.component.state\n const w = Math.abs(width)\n const h = Math.abs(height)\n const d = this.effectiveDepth\n if (!w || !h) return\n\n const inner = this.components3D\n\n // 높이 배분: 하단 클램프 4%, 마스트 92%, 상단 클램프 4%\n const clampH = d * 0.04\n const mastH = d * 0.92\n\n const pillarThick = Math.min(w, h) * 0.12\n\n const clampMat = new THREE.MeshStandardMaterial({\n color: CLAMP_COLOR,\n roughness: 0.5,\n metalness: 0.5,\n })\n\n // ── 1. 하단 클램프 (바닥 레일에 물리는 바퀴부) ──\n const clampW = w * 0.7\n const clampD = h * 0.5\n const bottomClampGeo = new THREE.BoxGeometry(clampW, clampH, clampD)\n const bottomClamp = new THREE.Mesh(bottomClampGeo, clampMat)\n bottomClamp.position.y = clampH / 2\n bottomClamp.castShadow = true\n inner.add(bottomClamp)\n\n // ── 2. 수직 마스트 (좌우 기둥 + 상·하 가로 연결 바) ──\n const mastBaseY = clampH\n const bodyColor = this.resolveColor()\n const mastMat = new THREE.MeshStandardMaterial({\n color: bodyColor,\n roughness: 0.3,\n metalness: 0.6,\n })\n\n const mastGeometries: THREE.BufferGeometry[] = []\n\n // 좌우 수직 기둥\n for (const xSign of [-1, 1]) {\n const pillar = new THREE.BoxGeometry(pillarThick, mastH, pillarThick)\n pillar.translate(xSign * (w / 2 - pillarThick / 2), mastBaseY + mastH / 2, 0)\n mastGeometries.push(pillar)\n }\n\n // 하단 가로 연결 바\n const crossW = w - pillarThick\n const crossH = pillarThick * 0.5\n const lowerCross = new THREE.BoxGeometry(crossW, crossH, pillarThick)\n lowerCross.translate(0, mastBaseY + crossH / 2, 0)\n mastGeometries.push(lowerCross)\n\n // 상단 가로 연결 바\n const upperCross = new THREE.BoxGeometry(crossW, crossH, pillarThick)\n upperCross.translate(0, mastBaseY + mastH - crossH / 2, 0)\n mastGeometries.push(upperCross)\n\n const mastMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(mastGeometries), mastMat)\n mastMesh.castShadow = true\n inner.add(mastMesh)\n\n // ── 3. 캐리지 포크 (별도 group — state.forkZ 변경 시 group.position.y lerp) ──\n // 포크 geometry 는 *_mastBaseY 기준 local 0 에 둠* — 즉 group.position.y = 0\n // 일 때 mast 바닥 자리에. forkZ 0~100 에 따라 group.position.y 만 변경하면\n // mast 따라 위/아래 이동. mesh 재구축 불필요.\n const forkMat = new THREE.MeshStandardMaterial({\n color: FORK_COLOR,\n roughness: 0.4,\n metalness: 0.5,\n })\n\n const forkGeometries: THREE.BufferGeometry[] = []\n\n const plateW = w * 0.9\n const plateH = d * 0.08\n const plateD = pillarThick * 0.8\n const centerPlate = new THREE.BoxGeometry(plateW, plateH, plateD)\n centerPlate.translate(0, plateH / 2, 0) // local 0 = mast 바닥\n forkGeometries.push(centerPlate)\n\n const armW = pillarThick * 0.6\n const armH = plateH * 0.4\n const armD = h * 0.6\n const armSpacing = w * 0.5\n\n for (const zSign of [-1, 1]) {\n for (const xSign of [-1, 1]) {\n const arm = new THREE.BoxGeometry(armW, armH, armD)\n arm.translate(xSign * (armSpacing / 2), armH / 2, zSign * (plateD / 2 + armD / 2))\n forkGeometries.push(arm)\n }\n }\n\n const forkMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(forkGeometries), forkMat)\n forkMesh.castShadow = true\n\n const forkGroup = new THREE.Group()\n forkGroup.add(forkMesh)\n inner.add(forkGroup)\n this.forkGroup = forkGroup\n this.forkMastBaseY = mastBaseY\n // forkZ=100 시점에 mast 의 *_위쪽 한계*. 포크 자체 두께 (plateH) 만큼은 마스트\n // 안에 머무르도록 limit 보정 — 그렇지 않으면 forkZ=100 일 때 포크가 상단\n // 클램프와 충돌. mastH * 0.92 로 약간 보수적 (= 마스트 본체 92% 까지만 이동).\n this.forkMastH = mastH * 0.92\n\n // 초기 forkZ 반영 — state.forkZ 가 이미 set 되어 있으면 그 위치에 배치\n const initialForkZ = Number((this.component.state as any).forkZ)\n this.applyForkZ(Number.isFinite(initialForkZ) ? initialForkZ : 0)\n\n // ── 4. 상단 클램프 (천장 레일에 물리는 가이드부) ──\n const topClampGeo = new THREE.BoxGeometry(clampW, clampH, clampD)\n const topClamp = new THREE.Mesh(topClampGeo, clampMat)\n topClamp.position.y = mastBaseY + mastH + clampH / 2\n topClamp.castShadow = true\n inner.add(topClamp)\n }\n\n updateDimension() {}\n updateAlpha() {}\n\n /** build() 직전 호출. forkGroup 참조를 깨끗이 비워둔다 — 새 build 가 다시 set. */\n clear() {\n this.forkGroup = undefined\n super.clear()\n }\n\n onchange(after: Record<string, unknown>, before: Record<string, unknown>) {\n if ('width' in after || 'height' in after || 'depth' in after || 'fillStyle' in after || 'strokeStyle' in after) {\n this.update()\n return\n }\n // forkZ 는 mesh 재구축 없이 group.position.y 만 갱신. 같은 onchange 에서\n // left/top 도 함께 변경될 수 있으므로 super 도 반드시 호출해 본체 transform\n // 갱신 path (state.left/top → obj3d.position.x/z) 가 동작하게 함.\n if ('forkZ' in after) {\n this.applyForkZ(Number(after.forkZ) || 0)\n }\n super.onchange(after, before)\n }\n}\n\nregisterRealObjectFactory('Crane', (component: Component) => new Crane3D(component))\n"]}
@@ -12,6 +12,7 @@ import * as BufferGeometryUtils from 'three/examples/jsm/utils/BufferGeometryUti
12
12
  import { RealObjectGroup, registerRealObjectFactory, disposeObject3D } from '@hatiolab/things-scene';
13
13
  import { LEGEND_CAPACITY } from '../features/mcs-status-default';
14
14
  import { getValueOnRanges } from '../utils/get-value-on-ranges';
15
+ import { fitGaugeScale } from '../utils/fit-gauge-scale';
15
16
  import { Z_FMSIM } from './z-priority';
16
17
  /**
17
18
  * 포트 기본 높이 (절대값).
@@ -534,12 +535,18 @@ class StockerRealObject3D extends MCSRealObject3D {
534
535
  const { w, h } = this.absSize;
535
536
  const depth = this.effectiveDepth;
536
537
  const isWidthLonger = w >= h;
537
- // 게이지 캔버스 (고해상도)
538
- const barW = 130;
539
- const barH = 26;
538
+ // 게이지 최대 크기 (스토커가 충분히 크면 이 크기 그대로 사용).
539
+ const MAX_BAR_W = 130;
540
+ const MAX_BAR_H = 26;
541
+ // 스토커 지붕 footprint 에 맞춰 비례 축소 (확대는 안 함 — 현재 크기가 상한).
542
+ const fit = fitGaugeScale(w, h, MAX_BAR_W, MAX_BAR_H);
543
+ // PlaneGeometry 의 월드 크기는 clamp, 텍스처 캔버스 해상도는 최대 크기 기준 고정
544
+ // (축소되어도 선명도 유지).
545
+ const barW = MAX_BAR_W * fit;
546
+ const barH = MAX_BAR_H * fit;
540
547
  const scale = 4;
541
- const canvasW = barW * scale;
542
- const canvasH = barH * scale;
548
+ const canvasW = MAX_BAR_W * scale;
549
+ const canvasH = MAX_BAR_H * scale;
543
550
  const canvas = document.createElement('canvas');
544
551
  canvas.width = canvasW;
545
552
  canvas.height = canvasH;