@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.
Files changed (56) hide show
  1. package/dist/agv.js +4 -0
  2. package/dist/agv.js.map +1 -1
  3. package/dist/carrier-manager.js +10 -4
  4. package/dist/carrier-manager.js.map +1 -1
  5. package/dist/crane.js +49 -41
  6. package/dist/crane.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-carrier-holder.js +13 -10
  12. package/dist/mcs-carrier-holder.js.map +1 -1
  13. package/dist/mcs-machine.js +11 -0
  14. package/dist/mcs-machine.js.map +1 -1
  15. package/dist/mcs-unit.js +10 -0
  16. package/dist/mcs-unit.js.map +1 -1
  17. package/dist/oht.js +4 -0
  18. package/dist/oht.js.map +1 -1
  19. package/dist/port-flow.js +4 -0
  20. package/dist/port-flow.js.map +1 -1
  21. package/dist/port.js +4 -0
  22. package/dist/port.js.map +1 -1
  23. package/dist/shelf.js +4 -0
  24. package/dist/shelf.js.map +1 -1
  25. package/dist/stocker-abnormal-bar.js +4 -0
  26. package/dist/stocker-abnormal-bar.js.map +1 -1
  27. package/dist/stocker-capacity-bar.js +4 -0
  28. package/dist/stocker-capacity-bar.js.map +1 -1
  29. package/dist/stocker-carrier-bar.js +4 -0
  30. package/dist/stocker-carrier-bar.js.map +1 -1
  31. package/dist/tsconfig.tsbuildinfo +1 -1
  32. package/dist/utils/carrier-data.js +55 -0
  33. package/dist/utils/carrier-data.js.map +1 -0
  34. package/dist/utils/crane-position.js +96 -0
  35. package/dist/utils/crane-position.js.map +1 -0
  36. package/dist/utils/fit-gauge-scale.js +28 -0
  37. package/dist/utils/fit-gauge-scale.js.map +1 -0
  38. package/package.json +3 -3
  39. package/src/agv.ts +5 -0
  40. package/src/carrier-manager.ts +11 -4
  41. package/src/crane.ts +57 -47
  42. package/src/factories/machine-3d.ts +14 -5
  43. package/src/features/parent-object-mixin.ts +16 -7
  44. package/src/mcs-carrier-holder.ts +13 -10
  45. package/src/mcs-machine.ts +13 -0
  46. package/src/mcs-unit.ts +12 -0
  47. package/src/oht.ts +5 -0
  48. package/src/port-flow.ts +5 -0
  49. package/src/port.ts +5 -0
  50. package/src/shelf.ts +5 -0
  51. package/src/stocker-abnormal-bar.ts +5 -0
  52. package/src/stocker-capacity-bar.ts +5 -0
  53. package/src/stocker-carrier-bar.ts +5 -0
  54. package/src/utils/carrier-data.ts +57 -0
  55. package/src/utils/crane-position.ts +115 -0
  56. 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"]}
@@ -1,4 +1,5 @@
1
1
  import { Model } from '@hatiolab/things-scene';
2
+ import { normalizeCarrierData, MULTI_CARRIER_KEY } from './utils/carrier-data';
2
3
  // 사라진 carrier 의 dispose 지연. 같은 macrotask 안에서 holder A 의 사라짐
3
4
  // 보고와 holder B 의 등장 보고가 순차로 들어오는 reparent 시나리오 (= 같은
4
5
  // frame snapshot 의 여러 record 적용) 를 보호하기 위해 setTimeout(0) 의
@@ -32,6 +33,10 @@ export default class CarrierManager {
32
33
  * holder 가 같은 이름을 보고하면 cancel 되어 reparent 로 전환.
33
34
  */
34
35
  update(holder, after, before) {
36
+ // 진입 경계 1회 정규화 — 키 대소문자 무관 + multiCarriers 통일 (idempotent).
37
+ // 이후 _buildStack / diffing 은 캐노니컬 키를 O(1) 직접 접근으로 읽는다.
38
+ after = normalizeCarrierData(after);
39
+ before = normalizeCarrierData(before);
35
40
  const afterStack = this._buildStack(after);
36
41
  const beforeStack = this._buildStack(before);
37
42
  const afterNames = new Set(afterStack.map(i => i.CARRIERNAME));
@@ -95,15 +100,16 @@ export default class CarrierManager {
95
100
  });
96
101
  }
97
102
  /**
98
- * data.CARRIERNAME (대표) + data.ADDITIONS[] 를 하나의 stack list 로 정규화.
99
- * 적층 순서: ADDITIONS[0..n-1] (bottom → up) + 대표 (top, 맨 마지막).
103
+ * data.CARRIERNAME (대표) + data.multiCarriers[] 를 하나의 stack list 로 정규화.
104
+ * 적층 순서: multiCarriers[0..n-1] (bottom → up) + 대표 (top, 맨 마지막).
105
+ * (data 는 update() 에서 이미 정규화되어 캐노니컬 키 + multiCarriers 로 통일된 상태)
100
106
  */
101
107
  _buildStack(data) {
102
108
  if (!data)
103
109
  return [];
104
110
  const stack = [];
105
- if (Array.isArray(data.ADDITIONS)) {
106
- for (const a of data.ADDITIONS) {
111
+ if (Array.isArray(data[MULTI_CARRIER_KEY])) {
112
+ for (const a of data[MULTI_CARRIER_KEY]) {
107
113
  if (a && typeof a === 'object' && a.CARRIERNAME) {
108
114
  // 항목 *원본 그대로* — 외부 시스템이 임의 키를 추가로 보낼 수 있고,
109
115
  // 누락된 4 키는 default 없이 그대로 비워둔다 (carrier.data 에 흘리는
@@ -1 +1 @@
1
- {"version":3,"file":"carrier-manager.js","sourceRoot":"","sources":["../src/carrier-manager.ts"],"names":[],"mappings":"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","sourcesContent":["import { Model } from '@hatiolab/things-scene'\nimport type Carrier from './carrier'\nimport type MCSCarrierHolder from './mcs-carrier-holder'\n\n// 사라진 carrier 의 dispose 지연. 같은 macrotask 안에서 holder A 의 사라짐\n// 보고와 holder B 의 등장 보고가 순차로 들어오는 reparent 시나리오 (= 같은\n// frame snapshot 의 여러 record 적용) 를 보호하기 위해 setTimeout(0) 의\n// next-macrotask delay 만 둔다. 과거 1000ms grace 가 있었으나 seek 시 2단계\n// 갱신 (1초 후 carrier 사라짐) 부작용이 명확하고 frame 누락 bouncing 깜빡임\n// 보호 효용은 작다 판단되어 0 으로 단축 (2026-06-09).\nconst DYING_TIMEOUT = 0\n\n/**\n * Carrier 가 들고 있는 4 키 묶음. data.CARRIERNAME 의 형제 키 (CARRIERTYPE,\n * EMPTYTYPE, CARRIERSTATUS) 를 한 item 으로 묶는다. holder.data 의 *대표*\n * 와 ADDITIONS 의 *각 항목* 모두 동일 shape 으로 정규화돼 stack diff 에\n * 들어간다.\n *\n * 주의 — 외부 시스템이 ADDITIONS 의 각 항목 키를 다른 이름으로 (예:\n * NAME / TYPE / EMPTY / STATUS) 줄 가능성에 대비해 이 build 함수가 단일\n * 정규화 지점이 된다. 키 매핑 변경은 여기 한 곳만 손대면 된다.\n */\nexport interface CarrierStackItem {\n CARRIERNAME: string\n CARRIERTYPE?: string\n EMPTYTYPE?: string\n CARRIERSTATUS?: string\n}\n\nexport default class CarrierManager {\n private carriers: Map<string, Carrier> = new Map()\n private dyingTimers: Map<string, ReturnType<typeof setTimeout>> = new Map()\n private root: any\n\n constructor(root: any) {\n this.root = root\n }\n\n get(carrierName: string): Carrier | undefined {\n return this.carriers.get(carrierName)\n }\n\n /**\n * Holder 의 data 변경에 대응해 carrier pool 을 동기화.\n *\n * 정책:\n * - holder.data 가 *single source of truth*. 대표(CARRIERNAME) +\n * ADDITIONS[] 를 하나의 *stack list* 로 정규화한 뒤 before/after 의\n * 이름 set diff 로 추가/제거/유지 분기.\n * - \"대표\" 와 \"ADDITIONS\" 는 *holder.data 안의 키 위치 차이일 뿐* 이고,\n * carrier 인스턴스끼리는 **종속/그룹 관계 없음 — 모두 독립 동등**.\n * stack 안의 위치 (stackIndex) 만 carrier.state 에 주입한다. 2D render\n * skip 등의 판정은 *위치 (= parent.components 의 마지막 여부)* 로 한다.\n * - 동일 CARRIERNAME 이 *다른 holder* 에 새로 등장하면 reparent (애니메이션).\n * *해당 carrier 만 이동* — 같은 stack 의 다른 carrier 는 따라가지 않는다.\n * - 사라진 carrier 는 next-macrotask 에 dispose. 같은 macrotask 안에서 다른\n * holder 가 같은 이름을 보고하면 cancel 되어 reparent 로 전환.\n */\n update(holder: MCSCarrierHolder, after: Record<string, any>, before: Record<string, any>): void {\n const afterStack = this._buildStack(after)\n const beforeStack = this._buildStack(before)\n\n const afterNames = new Set(afterStack.map(i => i.CARRIERNAME))\n const beforeNames = new Set(beforeStack.map(i => i.CARRIERNAME))\n\n // 1) 사라진 것 → dying schedule (자기 holder 의 자식인 carrier 만)\n for (const name of beforeNames) {\n if (afterNames.has(name)) continue\n const carrier = this.carriers.get(name)\n if (carrier && carrier.parent === holder) {\n this.scheduleDying(name)\n }\n }\n\n // 2) afterStack 의 *순서대로* 처리 — bottom (ADDITIONS[0]) 부터 top (대표)\n // 그래야 parent.components 가 자연스럽게 같은 순서로 정렬되어 stack index\n // 가 일관된다.\n afterStack.forEach((item, idx) => {\n const name = item.CARRIERNAME\n this.cancelDying(name)\n\n let carrier = this.carriers.get(name)\n const { width, height } = holder.state\n\n if (!carrier) {\n carrier = Model.compile(\n {\n id: name,\n type: 'Carrier',\n left: 0,\n top: 0,\n width,\n height,\n showText: holder.getState('showCarrierText') || false,\n stackIndex: idx\n },\n this.root.app\n ) as unknown as Carrier\n\n this.carriers.set(name, carrier)\n carrier.data = item\n holder.addComponent(carrier, { silent: true })\n } else if (carrier.parent !== holder && !beforeNames.has(name)) {\n // 다른 holder 에 있던 carrier 가 이번 update 에서 *새로 등장* 한 경우만\n // reparent. before 에도 같은 이름이 있었으면 ping-pong (옛 시점의\n // 반복 보고) 이므로 trigger 하지 않는다. (F05 의 bouncing 방지)\n ;(holder as any).reparent(carrier, { animated: true, duration: 2000, ease: 'inout' })\n }\n\n if (carrier.parent === holder) {\n carrier.setState('stackIndex', idx)\n carrier.data = item\n }\n })\n\n // *_모든 carrier 추가 완료 후* — 각자의 transform 강제 갱신. 한 줄로 처리\n // 안 되는 이유: cz override 의 offset 계산이 *형제 carrier 의 _realObject.\n // effectiveDepth 누적*에 의존하는데, 첫 iteration 시점엔 자기 뒤 carrier 의\n // _realObject 가 아직 없어 offset 이 부족하게 평가될 수 있다. 모두 빌드된\n // 뒤 한 번 더 갱신해야 적층 정확.\n afterStack.forEach(item => {\n const carrier = this.carriers.get(item.CARRIERNAME)\n if (carrier && carrier.parent === holder) {\n ;(carrier as any)._realObject?.updateTransform?.()\n }\n })\n }\n\n /**\n * data.CARRIERNAME (대표) + data.ADDITIONS[] 를 하나의 stack list 로 정규화.\n * 적층 순서: ADDITIONS[0..n-1] (bottom → up) + 대표 (top, 맨 마지막).\n */\n private _buildStack(data: any): CarrierStackItem[] {\n if (!data) return []\n\n const stack: CarrierStackItem[] = []\n\n if (Array.isArray(data.ADDITIONS)) {\n for (const a of data.ADDITIONS) {\n if (a && typeof a === 'object' && a.CARRIERNAME) {\n // 항목 *원본 그대로* — 외부 시스템이 임의 키를 추가로 보낼 수 있고,\n // 누락된 4 키는 default 없이 그대로 비워둔다 (carrier.data 에 흘리는\n // 가공 최소화 — F03 같은 기존 테스트의 raw shape 기대와도 정합).\n stack.push({ ...a } as CarrierStackItem)\n }\n }\n }\n\n if (data.CARRIERNAME) {\n // 대표는 *holder.data 의 carrier 관련 키만 추출* — showText / carrierDepth\n // 같은 holder 자체 속성은 carrier 로 흘리지 않는다. 명시되지 않은 키는\n // 생략 (default 안 채움).\n const repr: any = { CARRIERNAME: data.CARRIERNAME }\n if ('CARRIERTYPE' in data) repr.CARRIERTYPE = data.CARRIERTYPE\n if ('EMPTYTYPE' in data) repr.EMPTYTYPE = data.EMPTYTYPE\n if ('CARRIERSTATUS' in data) repr.CARRIERSTATUS = data.CARRIERSTATUS\n stack.push(repr)\n }\n\n return stack\n }\n\n /**\n * carrier dispose 를 *next macrotask* 로 미룬다. 동기 즉시 dispose 가 아닌\n * 이유: 같은 macrotask 안에서 holder A 의 사라짐 보고가 step 1 에 들어오고\n * holder B 의 등장 보고가 (별도 carrier-manager.update 호출의) step 2 에\n * 들어오는 reparent 시나리오를 보호하기 위함. 즉시 dispose 면 step 1 에서\n * carrier 인스턴스가 파괴돼 step 2 의 reparent 분기가 *새 인스턴스 생성*\n * 으로 떨어지고, 3D mesh 재구축 + 이동 애니메이션 손실이 발생.\n *\n * 같은 macrotask 안에서 cancelDying 호출이 도달하면 clearTimeout 으로\n * dispose 회수 → reparent 정상.\n */\n private scheduleDying(carrierName: string): void {\n this.cancelDying(carrierName)\n\n const timer = setTimeout(() => {\n this.dyingTimers.delete(carrierName)\n\n const carrier = this.carriers.get(carrierName)\n if (carrier) {\n this.carriers.delete(carrierName)\n if (!carrier.disposed) {\n carrier.removeSelf(true)\n }\n }\n }, DYING_TIMEOUT)\n\n this.dyingTimers.set(carrierName, timer)\n }\n\n private cancelDying(carrierName: string): void {\n const timer = this.dyingTimers.get(carrierName)\n if (timer) {\n clearTimeout(timer)\n this.dyingTimers.delete(carrierName)\n }\n }\n\n onCarrierDisposed(carrierName: string): void {\n this.cancelDying(carrierName)\n this.carriers.delete(carrierName)\n }\n\n dispose(): void {\n for (const timer of this.dyingTimers.values()) {\n clearTimeout(timer)\n }\n this.dyingTimers.clear()\n\n for (const carrier of this.carriers.values()) {\n if (!carrier.disposed) {\n carrier.removeSelf(true)\n }\n }\n\n this.carriers.clear()\n }\n}\n"]}
1
+ {"version":3,"file":"carrier-manager.js","sourceRoot":"","sources":["../src/carrier-manager.ts"],"names":[],"mappings":"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","sourcesContent":["import { Model } from '@hatiolab/things-scene'\nimport type Carrier from './carrier'\nimport type MCSCarrierHolder from './mcs-carrier-holder'\nimport { normalizeCarrierData, MULTI_CARRIER_KEY } from './utils/carrier-data'\n\n// 사라진 carrier 의 dispose 지연. 같은 macrotask 안에서 holder A 의 사라짐\n// 보고와 holder B 의 등장 보고가 순차로 들어오는 reparent 시나리오 (= 같은\n// frame snapshot 의 여러 record 적용) 를 보호하기 위해 setTimeout(0) 의\n// next-macrotask delay 만 둔다. 과거 1000ms grace 가 있었으나 seek 시 2단계\n// 갱신 (1초 후 carrier 사라짐) 부작용이 명확하고 frame 누락 bouncing 깜빡임\n// 보호 효용은 작다 판단되어 0 으로 단축 (2026-06-09).\nconst DYING_TIMEOUT = 0\n\n/**\n * Carrier 가 들고 있는 4 키 묶음. data.CARRIERNAME 의 형제 키 (CARRIERTYPE,\n * EMPTYTYPE, CARRIERSTATUS) 를 한 item 으로 묶는다. holder.data 의 *대표*\n * 와 ADDITIONS 의 *각 항목* 모두 동일 shape 으로 정규화돼 stack diff 에\n * 들어간다.\n *\n * 주의 — 외부 시스템이 ADDITIONS 의 각 항목 키를 다른 이름으로 (예:\n * NAME / TYPE / EMPTY / STATUS) 줄 가능성에 대비해 이 build 함수가 단일\n * 정규화 지점이 된다. 키 매핑 변경은 여기 한 곳만 손대면 된다.\n */\nexport interface CarrierStackItem {\n CARRIERNAME: string\n CARRIERTYPE?: string\n EMPTYTYPE?: string\n CARRIERSTATUS?: string\n}\n\nexport default class CarrierManager {\n private carriers: Map<string, Carrier> = new Map()\n private dyingTimers: Map<string, ReturnType<typeof setTimeout>> = new Map()\n private root: any\n\n constructor(root: any) {\n this.root = root\n }\n\n get(carrierName: string): Carrier | undefined {\n return this.carriers.get(carrierName)\n }\n\n /**\n * Holder 의 data 변경에 대응해 carrier pool 을 동기화.\n *\n * 정책:\n * - holder.data 가 *single source of truth*. 대표(CARRIERNAME) +\n * ADDITIONS[] 를 하나의 *stack list* 로 정규화한 뒤 before/after 의\n * 이름 set diff 로 추가/제거/유지 분기.\n * - \"대표\" 와 \"ADDITIONS\" 는 *holder.data 안의 키 위치 차이일 뿐* 이고,\n * carrier 인스턴스끼리는 **종속/그룹 관계 없음 — 모두 독립 동등**.\n * stack 안의 위치 (stackIndex) 만 carrier.state 에 주입한다. 2D render\n * skip 등의 판정은 *위치 (= parent.components 의 마지막 여부)* 로 한다.\n * - 동일 CARRIERNAME 이 *다른 holder* 에 새로 등장하면 reparent (애니메이션).\n * *해당 carrier 만 이동* — 같은 stack 의 다른 carrier 는 따라가지 않는다.\n * - 사라진 carrier 는 next-macrotask 에 dispose. 같은 macrotask 안에서 다른\n * holder 가 같은 이름을 보고하면 cancel 되어 reparent 로 전환.\n */\n update(holder: MCSCarrierHolder, after: Record<string, any>, before: Record<string, any>): void {\n // 진입 경계 1회 정규화 — 키 대소문자 무관 + multiCarriers 통일 (idempotent).\n // 이후 _buildStack / diffing 은 캐노니컬 키를 O(1) 직접 접근으로 읽는다.\n after = normalizeCarrierData(after)\n before = normalizeCarrierData(before)\n\n const afterStack = this._buildStack(after)\n const beforeStack = this._buildStack(before)\n\n const afterNames = new Set(afterStack.map(i => i.CARRIERNAME))\n const beforeNames = new Set(beforeStack.map(i => i.CARRIERNAME))\n\n // 1) 사라진 것 → dying schedule (자기 holder 의 자식인 carrier 만)\n for (const name of beforeNames) {\n if (afterNames.has(name)) continue\n const carrier = this.carriers.get(name)\n if (carrier && carrier.parent === holder) {\n this.scheduleDying(name)\n }\n }\n\n // 2) afterStack 의 *순서대로* 처리 — bottom (ADDITIONS[0]) 부터 top (대표)\n // 그래야 parent.components 가 자연스럽게 같은 순서로 정렬되어 stack index\n // 가 일관된다.\n afterStack.forEach((item, idx) => {\n const name = item.CARRIERNAME\n this.cancelDying(name)\n\n let carrier = this.carriers.get(name)\n const { width, height } = holder.state\n\n if (!carrier) {\n carrier = Model.compile(\n {\n id: name,\n type: 'Carrier',\n left: 0,\n top: 0,\n width,\n height,\n showText: holder.getState('showCarrierText') || false,\n stackIndex: idx\n },\n this.root.app\n ) as unknown as Carrier\n\n this.carriers.set(name, carrier)\n carrier.data = item\n holder.addComponent(carrier, { silent: true })\n } else if (carrier.parent !== holder && !beforeNames.has(name)) {\n // 다른 holder 에 있던 carrier 가 이번 update 에서 *새로 등장* 한 경우만\n // reparent. before 에도 같은 이름이 있었으면 ping-pong (옛 시점의\n // 반복 보고) 이므로 trigger 하지 않는다. (F05 의 bouncing 방지)\n ;(holder as any).reparent(carrier, { animated: true, duration: 2000, ease: 'inout' })\n }\n\n if (carrier.parent === holder) {\n carrier.setState('stackIndex', idx)\n carrier.data = item\n }\n })\n\n // *_모든 carrier 추가 완료 후* — 각자의 transform 강제 갱신. 한 줄로 처리\n // 안 되는 이유: cz override 의 offset 계산이 *형제 carrier 의 _realObject.\n // effectiveDepth 누적*에 의존하는데, 첫 iteration 시점엔 자기 뒤 carrier 의\n // _realObject 가 아직 없어 offset 이 부족하게 평가될 수 있다. 모두 빌드된\n // 뒤 한 번 더 갱신해야 적층 정확.\n afterStack.forEach(item => {\n const carrier = this.carriers.get(item.CARRIERNAME)\n if (carrier && carrier.parent === holder) {\n ;(carrier as any)._realObject?.updateTransform?.()\n }\n })\n }\n\n /**\n * data.CARRIERNAME (대표) + data.multiCarriers[] 를 하나의 stack list 로 정규화.\n * 적층 순서: multiCarriers[0..n-1] (bottom → up) + 대표 (top, 맨 마지막).\n * (data 는 update() 에서 이미 정규화되어 캐노니컬 키 + multiCarriers 로 통일된 상태)\n */\n private _buildStack(data: any): CarrierStackItem[] {\n if (!data) return []\n\n const stack: CarrierStackItem[] = []\n\n if (Array.isArray(data[MULTI_CARRIER_KEY])) {\n for (const a of data[MULTI_CARRIER_KEY]) {\n if (a && typeof a === 'object' && a.CARRIERNAME) {\n // 항목 *원본 그대로* — 외부 시스템이 임의 키를 추가로 보낼 수 있고,\n // 누락된 4 키는 default 없이 그대로 비워둔다 (carrier.data 에 흘리는\n // 가공 최소화 — F03 같은 기존 테스트의 raw shape 기대와도 정합).\n stack.push({ ...a } as CarrierStackItem)\n }\n }\n }\n\n if (data.CARRIERNAME) {\n // 대표는 *holder.data 의 carrier 관련 키만 추출* — showText / carrierDepth\n // 같은 holder 자체 속성은 carrier 로 흘리지 않는다. 명시되지 않은 키는\n // 생략 (default 안 채움).\n const repr: any = { CARRIERNAME: data.CARRIERNAME }\n if ('CARRIERTYPE' in data) repr.CARRIERTYPE = data.CARRIERTYPE\n if ('EMPTYTYPE' in data) repr.EMPTYTYPE = data.EMPTYTYPE\n if ('CARRIERSTATUS' in data) repr.CARRIERSTATUS = data.CARRIERSTATUS\n stack.push(repr)\n }\n\n return stack\n }\n\n /**\n * carrier dispose 를 *next macrotask* 로 미룬다. 동기 즉시 dispose 가 아닌\n * 이유: 같은 macrotask 안에서 holder A 의 사라짐 보고가 step 1 에 들어오고\n * holder B 의 등장 보고가 (별도 carrier-manager.update 호출의) step 2 에\n * 들어오는 reparent 시나리오를 보호하기 위함. 즉시 dispose 면 step 1 에서\n * carrier 인스턴스가 파괴돼 step 2 의 reparent 분기가 *새 인스턴스 생성*\n * 으로 떨어지고, 3D mesh 재구축 + 이동 애니메이션 손실이 발생.\n *\n * 같은 macrotask 안에서 cancelDying 호출이 도달하면 clearTimeout 으로\n * dispose 회수 → reparent 정상.\n */\n private scheduleDying(carrierName: string): void {\n this.cancelDying(carrierName)\n\n const timer = setTimeout(() => {\n this.dyingTimers.delete(carrierName)\n\n const carrier = this.carriers.get(carrierName)\n if (carrier) {\n this.carriers.delete(carrierName)\n if (!carrier.disposed) {\n carrier.removeSelf(true)\n }\n }\n }, DYING_TIMEOUT)\n\n this.dyingTimers.set(carrierName, timer)\n }\n\n private cancelDying(carrierName: string): void {\n const timer = this.dyingTimers.get(carrierName)\n if (timer) {\n clearTimeout(timer)\n this.dyingTimers.delete(carrierName)\n }\n }\n\n onCarrierDisposed(carrierName: string): void {\n this.cancelDying(carrierName)\n this.carriers.delete(carrierName)\n }\n\n dispose(): void {\n for (const timer of this.dyingTimers.values()) {\n clearTimeout(timer)\n }\n this.dyingTimers.clear()\n\n for (const carrier of this.carriers.values()) {\n if (!carrier.disposed) {\n carrier.removeSelf(true)\n }\n }\n\n this.carriers.clear()\n }\n}\n"]}
package/dist/crane.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { __decorate } from "tslib";
2
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,
@@ -10,9 +11,6 @@ const NATURE = {
10
11
  const GAP = 2;
11
12
  /** 좌표 보간 기본 duration (ms). data.DURATION_MS 가 없으면 이 값 사용. */
12
13
  const DEFAULT_DURATION_MS = 1000;
13
- function clamp01(v) {
14
- return Math.max(0, Math.min(100, Number(v) || 0));
15
- }
16
14
  function lerp(a, b, t) {
17
15
  return a + (b - a) * t;
18
16
  }
@@ -26,31 +24,42 @@ let Crane = class Crane extends MCSVehicle {
26
24
  get auxColor() {
27
25
  return 'black';
28
26
  }
27
+ /** Crane 의 parentObject 는 STOCKER. */
28
+ get parentObjectTypes() {
29
+ return ['STOCKER'];
30
+ }
29
31
  getLegendFallback() {
30
32
  return this.root.craneLegendTheme || super.getLegendFallback();
31
33
  }
32
34
  /**
33
- * data.POSITION = { X, Y, Z } (각 0~100 정규화) 분기. Stocker 부모 기준으로
34
- * 실 좌표 (state.left/top) 와 forkZ 로 변환해 lerp 시작. 보간 중 새 값이
35
- * 들어오면 *현재 보간 위치* 를 from 으로 잡고 새 target 으로 새 lerp 시작.
36
- * NODEID 경로는 super (MCSVehicle.onchangeData) 가 처리 — POSITION 만 있는
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 만 있는
37
41
  * 데이터에선 NODEID 가 없어 자연스럽게 noop.
38
42
  */
39
43
  onchangeData(after, before) {
40
44
  const afterData = after === null || after === void 0 ? void 0 : after.data;
41
- const beforeData = before === null || before === void 0 ? void 0 : before.data;
42
- const afterPos = afterData === null || afterData === void 0 ? void 0 : afterData.POSITION;
43
- if (afterPos && typeof afterPos === 'object') {
44
- const beforePos = beforeData === null || beforeData === void 0 ? void 0 : beforeData.POSITION;
45
- const changed = afterPos.X !== (beforePos === null || beforePos === void 0 ? void 0 : beforePos.X) ||
46
- afterPos.Y !== (beforePos === null || beforePos === void 0 ? void 0 : beforePos.Y) ||
47
- afterPos.Z !== (beforePos === null || beforePos === void 0 ? void 0 : beforePos.Z);
48
- if (changed) {
49
- this._startPositionAnimation(afterPos, afterData === null || afterData === void 0 ? void 0 : afterData.DURATION_MS);
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'));
50
51
  }
51
52
  }
52
53
  super.onchangeData(after, before);
53
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
+ }
54
63
  dispose() {
55
64
  if (this._posAnimator) {
56
65
  this._posAnimator.unsubscribe();
@@ -58,10 +67,10 @@ let Crane = class Crane extends MCSVehicle {
58
67
  }
59
68
  super.dispose();
60
69
  }
61
- _startPositionAnimation(pos, durationMs) {
62
- const target = this._normalizedToReal(pos);
70
+ _startPositionAnimation(cranePosition, durationMs) {
71
+ const target = this._ratioToReal(cranePosition);
63
72
  if (!target)
64
- return; // 부모가 Stocker 아니면 변환 skip
73
+ return; // 부모(host) 없거나 크기 0 이면 변환 skip
65
74
  const now = frameClock.simTime;
66
75
  const duration = Math.max(1, Number(durationMs) || DEFAULT_DURATION_MS);
67
76
  // 현재 위치 — 보간 중이면 그 시점 보간값을 from 으로
@@ -111,20 +120,26 @@ let Crane = class Crane extends MCSVehicle {
111
120
  }
112
121
  }
113
122
  /**
114
- * 0~100 정규화 좌표 → state.left/top + forkZ.
123
+ * cranePosition (current + limit) → state.left/top + forkZ.
115
124
  *
116
125
  * 부모 오브젝트 (보통은 Stocker 이지만 다른 컨테이너일 수도) 의 width/height
117
- * 만 사용해 좌표 매핑 — 타입 무관 일반화. 가상 rail 은 host 의 긴쪽 중심부
118
- * 가정:
119
- * - 데이터.X → host 의 긴쪽 (자동 판별: max(width, height))
120
- * - 데이터.Y → 짧은쪽 (rail 중심 = 50 근처가 일반적)
121
- * - 데이터.Z → forkZ (state 에 0~100 그대로 저장, 3D 측에서 mast 비율로 매핑)
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 모서리에 정렬.
122
137
  *
123
138
  * 참조는 ParentObjectMixin 이 제공하는 `parentObject` 사용 —
124
139
  * state.parentObjectId 명시 시 그것, 미명시 시 this.parent 자동 fallback.
125
140
  */
126
- _normalizedToReal(pos) {
127
- var _a, _b;
141
+ _ratioToReal(cp) {
142
+ var _a, _b, _c, _d, _e, _f, _g, _h;
128
143
  const host = this.parentObject;
129
144
  if (!host)
130
145
  return undefined;
@@ -134,21 +149,14 @@ let Crane = class Crane extends MCSVehicle {
134
149
  const ch = Number(this.state.height) || 0;
135
150
  if (!hw || !hh)
136
151
  return undefined;
137
- const longIsWidth = hw >= hh;
138
- const longLen = longIsWidth ? hw : hh;
139
- const shortLen = longIsWidth ? hh : hw;
140
- const craneLongSize = longIsWidth ? cw : ch;
141
- const craneShortSize = longIsWidth ? ch : cw;
142
- const X = clamp01(pos.X);
143
- const Y = clamp01(pos.Y);
144
- // crane 의 *center* 가 (X/100)*longLen, (Y/100)*shortLen 에 오도록.
145
- // state.left/top 은 top-left 기준이므로 size/2 차감.
146
- const longPos = (X / 100) * longLen - craneLongSize / 2;
147
- const shortPos = (Y / 100) * shortLen - craneShortSize / 2;
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);
148
156
  return {
149
- left: longIsWidth ? longPos : shortPos,
150
- top: longIsWidth ? shortPos : longPos,
151
- forkZ: clamp01(pos.Z)
157
+ left,
158
+ top,
159
+ forkZ: rz * 100 // forkZ 0~100, 3D 측 (crane-3d) 에서 mast 비율로 매핑
152
160
  };
153
161
  }
154
162
  render(ctx) {
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, frameClock } 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/** 좌표 보간 기본 duration (ms). data.DURATION_MS 가 없으면 이 값 사용. */\nconst DEFAULT_DURATION_MS = 1000\n\nfunction clamp01(v: number): number {\n return Math.max(0, Math.min(100, Number(v) || 0))\n}\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 getLegendFallback() {\n return (this.root as any).craneLegendTheme || super.getLegendFallback()\n }\n\n private _posAnimator?: CranePositionAnimator\n\n /**\n * data.POSITION = { X, Y, Z } (각 0~100 정규화) 분기. Stocker 부모 기준으로\n * 실 좌표 (state.left/top) 와 forkZ 로 변환해 lerp 시작. 보간 중 새 값이\n * 들어오면 *현재 보간 위치* 를 from 으로 잡고 새 target 으로 새 lerp 시작.\n * NODEID 경로는 super (MCSVehicle.onchangeData) 가 처리 — POSITION 만 있는\n * 데이터에선 NODEID 가 없어 자연스럽게 noop.\n */\n onchangeData(after: Record<string, any>, before: Record<string, any>): void {\n const afterData = after?.data\n const beforeData = before?.data\n const afterPos = afterData?.POSITION\n if (afterPos && typeof afterPos === 'object') {\n const beforePos = beforeData?.POSITION\n const changed =\n afterPos.X !== beforePos?.X ||\n afterPos.Y !== beforePos?.Y ||\n afterPos.Z !== beforePos?.Z\n if (changed) {\n this._startPositionAnimation(afterPos, afterData?.DURATION_MS)\n }\n }\n super.onchangeData(after, before)\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(\n pos: { X: number; Y: number; Z: number },\n durationMs?: number\n ): void {\n const target = this._normalizedToReal(pos)\n if (!target) return // 부모가 Stocker 아니면 변환 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 * 0~100 정규화 좌표 → state.left/top + forkZ.\n *\n * 부모 오브젝트 (보통은 Stocker 이지만 다른 컨테이너일 수도) 의 width/height\n * 만 사용해 좌표 매핑 — 타입 무관 일반화. 가상 rail 은 host 의 긴쪽 중심부\n * 가정:\n * - 데이터.X → host 의 긴쪽 (자동 판별: max(width, height))\n * - 데이터.Y → 짧은쪽 (rail 중심 = 50 근처가 일반적)\n * - 데이터.Z → forkZ (state 에 0~100 그대로 저장, 3D 측에서 mast 비율로 매핑)\n *\n * 참조는 ParentObjectMixin 이 제공하는 `parentObject` 사용 —\n * state.parentObjectId 명시 시 그것, 미명시 시 this.parent 자동 fallback.\n */\n private _normalizedToReal(\n pos: { X: number; Y: number; Z: number }\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 longIsWidth = hw >= hh\n const longLen = longIsWidth ? hw : hh\n const shortLen = longIsWidth ? hh : hw\n const craneLongSize = longIsWidth ? cw : ch\n const craneShortSize = longIsWidth ? ch : cw\n\n const X = clamp01(pos.X)\n const Y = clamp01(pos.Y)\n\n // crane 의 *center* 가 (X/100)*longLen, (Y/100)*shortLen 에 오도록.\n // state.left/top 은 top-left 기준이므로 size/2 차감.\n const longPos = (X / 100) * longLen - craneLongSize / 2\n const shortPos = (Y / 100) * shortLen - craneShortSize / 2\n\n return {\n left: longIsWidth ? longPos : shortPos,\n top: longIsWidth ? shortPos : longPos,\n forkZ: clamp01(pos.Z)\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"]}
1
+ {"version":3,"file":"crane.js","sourceRoot":"","sources":["../src/crane.ts"],"names":[],"mappings":"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","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"]}
@@ -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;