@operato/scene-storage 10.0.0-beta.35 → 10.0.0-beta.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,15 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ ## [10.0.0-beta.36](https://github.com/things-scene/operato-scene/compare/v10.0.0-beta.35...v10.0.0-beta.36) (2026-05-14)
7
+
8
+
9
+ ### :rocket: New Features
10
+
11
+ * **storage:** Crane 2D/3D 시각화 개편 — fork/carrier visualization + 높이감 표현 ([f3b7398](https://github.com/things-scene/operato-scene/commit/f3b73987a665a2e87c634b4c64b0854a4b314e52))
12
+
13
+
14
+
6
15
  ## [10.0.0-beta.35](https://github.com/things-scene/operato-scene/compare/v10.0.0-beta.34...v10.0.0-beta.35) (2026-05-13)
7
16
 
8
17
 
package/dist/crane-3d.js CHANGED
@@ -144,8 +144,11 @@ export class Crane3D extends RealObjectGroup {
144
144
  mesh.receiveShadow = true;
145
145
  this.object3d.add(mesh);
146
146
  }
147
- // ── Carriage (between masts) ──────────────────────────────────────
148
- const carriageY = baseTrolleyY + baseH / 2 + carriageHeight + carriageH / 2;
147
+ // ── Carriage + Fork 어셈블리 (forkLift 시 함께 이동) ───────────────
148
+ // 시각 단절 방지를 위해 carriage fork *함께* forkLift 만큼 올림.
149
+ // 의미상으로는 carriageHeight 가 mast 위 carriage 위치, forkLift 가 *그 어셈블리의*
150
+ // 추가 미세 Y. 둘 다 적용된 위치를 carriage / fork 둘 다 공유.
151
+ const carriageY = baseTrolleyY + baseH / 2 + carriageHeight + forkLift + carriageH / 2;
149
152
  {
150
153
  const geo = new THREE.BoxGeometry(carriageW, carriageH, carriageZ);
151
154
  const mesh = new THREE.Mesh(geo, carriageMat);
@@ -154,29 +157,60 @@ export class Crane3D extends RealObjectGroup {
154
157
  mesh.receiveShadow = true;
155
158
  this.object3d.add(mesh);
156
159
  }
157
- // ── Two-prong forks (telescopic ±Z) ───────────────────────────────
158
- // forkExtension 부호: + = +Z, - = -Z. forkExtension=0 blade 중심이 carriage 안쪽
159
- // (= retracted, blade tip carriage 앞면 근처).
160
- const forkSign = forkExtension >= 0 ? 1 : -1;
161
- const forkBaseZ = forkSign * (carriageZ / 2 - bladeL / 2 + Math.abs(forkExtension));
162
- const forkY = carriageY + forkLift - bladeH / 2;
160
+ // ── Two-prong forks (양옆 stub + active 신축, 2D 와 동일 모델) ─────
161
+ // ext=0: ±Z 면에 작은 stub. carriage 안에 들어있는 인상 (튀어나옴 없음)
162
+ // ext=±forkLen: active stub + |ext| 길이로 신장. 반대쪽 stub 유지.
163
+ // 회전 flip 없음 ext 0 지날 시각 점프 없음.
164
+ const forkY = carriageY; // carriage 중심 Y (embed)
165
+ const stubL = Math.min(carriageZ * 0.2, Math.max(bladeL * 0.05, 6));
166
+ const absExt = Math.abs(forkExtension);
167
+ const sign = forkExtension >= 0 ? 1 : -1;
163
168
  {
164
169
  const group = new THREE.Group();
165
- group.position.set(0, forkY, forkBaseZ);
166
- // forkSign 으로 blade tip 방향 설정
167
- group.rotation.y = forkSign < 0 ? Math.PI : 0;
170
+ group.position.set(0, forkY, 0);
171
+ // 양옆 stub prong × 두 측면 = 4 box
172
+ const stubGeo = new THREE.BoxGeometry(bladeW, bladeH, stubL);
168
173
  for (const xOff of [-bladeSpacing / 2, +bladeSpacing / 2]) {
169
- const geo = new THREE.BoxGeometry(bladeW, bladeH, bladeL);
170
- const mesh = new THREE.Mesh(geo, forkMat);
171
- mesh.position.set(xOff, 0, 0);
172
- mesh.castShadow = true;
173
- mesh.receiveShadow = true;
174
- group.add(mesh);
174
+ for (const zSide of [-1, +1]) {
175
+ const mesh = new THREE.Mesh(stubGeo, forkMat);
176
+ mesh.position.set(xOff, 0, zSide * (carriageZ / 2 + stubL / 2));
177
+ mesh.castShadow = true;
178
+ mesh.receiveShadow = true;
179
+ group.add(mesh);
180
+ }
181
+ }
182
+ // Active side 신장
183
+ if (absExt > 0.5) {
184
+ const extGeo = new THREE.BoxGeometry(bladeW, bladeH, absExt);
185
+ for (const xOff of [-bladeSpacing / 2, +bladeSpacing / 2]) {
186
+ const mesh = new THREE.Mesh(extGeo, forkMat);
187
+ mesh.position.set(xOff, 0, sign * (carriageZ / 2 + stubL + absExt / 2));
188
+ mesh.castShadow = true;
189
+ mesh.receiveShadow = true;
190
+ group.add(mesh);
191
+ }
192
+ }
193
+ // Pallet — 정지 시 carriage 중심 Z, 신축 시 fork 중간으로 이동.
194
+ const loaded = forkLift > 0 || !!this.component.state.loaded || !!this.component.state.carrying;
195
+ if (loaded) {
196
+ const palletMat = new THREE.MeshStandardMaterial({
197
+ color: 0xa08864, metalness: 0.1, roughness: 0.85
198
+ });
199
+ const palletW = bladeSpacing * 1.2;
200
+ const palletH = Math.max(bladeH * 2.5, carriageH * 0.5);
201
+ const palletL = Math.max(bladeL * 0.3, carriageZ * 0.6);
202
+ const geo = new THREE.BoxGeometry(palletW, palletH, palletL);
203
+ const pallet = new THREE.Mesh(geo, palletMat);
204
+ const palletZ = absExt < 0.5 ? 0 : sign * (carriageZ / 2 + absExt / 2);
205
+ pallet.position.set(0, carriageH / 2 + palletH / 2, palletZ);
206
+ pallet.castShadow = true;
207
+ pallet.receiveShadow = true;
208
+ group.add(pallet);
175
209
  }
176
210
  this.object3d.add(group);
177
211
  this._forkGroup = group;
178
- this._forkTopY = bladeH / 2;
179
- this._bladeMidZ = 0;
212
+ this._forkTopY = carriageH / 2;
213
+ this._bladeMidZ = absExt < 0.5 ? 0 : sign * (carriageZ / 2 + absExt / 2);
180
214
  }
181
215
  // ── Top frame (connects mast tops) ────────────────────────────────
182
216
  const topFrameY = mastY + mastH / 2 + topFrameH / 2;
@@ -1 +1 @@
1
- {"version":3,"file":"crane-3d.js","sourceRoot":"","sources":["../src/crane-3d.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * Copyright © HatioLab Inc. All rights reserved.\n *\n * Crane 3D — heavy-duty pallet AS/RS stacker crane (twin-mast).\n *\n * Axis convention (things-scene 3D):\n * +X = aisle 진행 방향 (Crane 좌우 이동, *옆으로 길게* 축)\n * +Y = 수직 (Crane 키, carriage 상하 이동)\n * +Z = aisle 폭 방향 (Rack cell 쪽 / fork 신축 방향)\n *\n * **스케일 룰**:\n * - 모든 가로(X,Z) dimension 과 Y 두께 → 2D footprint (state.width, state.height) 비례\n * - Mast Y 길이만 → state.depth 사용 (수직 키)\n * - Y 두께는 절대 depth (크레인 키) 에 비례 안 함 → 키워도 뭉치 안 두꺼워짐\n * - mm/cm/scene-unit 무관 — 사용자의 footprint 스케일에 자동 맞춤\n *\n * 부품:\n * [Ceiling rail]\n * [Top guide trolley] — 천장 rail 위\n * [Top frame] — 두 mast 상부 연결\n * [Mast L][Mast R] — twin yellow column (X 축으로 떨어져 있음)\n * [Carriage] — 두 mast 사이 가로 frame (carriageHeight 로 상하)\n * └── [Two forks] — carriage 에서 ±Z 신축 (forkExtension)\n * [Base trolley] — 바닥 motor housing (X 길게)\n * └── [Cabinet] — base 한쪽 red 컨트롤박스\n * └── [Status lamp]\n * [Floor rail]\n *\n * Actuators (state):\n * carriageHeight, forkExtension (±), forkLift\n */\n\nimport * as THREE from 'three'\nimport { RealObjectGroup } from '@hatiolab/things-scene'\n\nconst MAST_COLOR = 0xff7a00 // mast — orange\nconst TROLLEY_COLOR = 0x3a4048 // base / top — dark charcoal\nconst CARRIAGE_COLOR = 0xffcc00 // carriage (shuttle) — yellow\nconst CONTROLLER_COLOR = 0xc63333 // cabinet — red\nconst FORK_COLOR = 0xffcc00 // fork — yellow (same as carriage)\nconst RAIL_COLOR = 0x1a1f24 // rail — dark steel\nconst LAMP_OFF = 0x222222\n\nexport class Crane3D extends RealObjectGroup {\n private _forkGroup?: THREE.Group\n private _forkTopY: number = 0\n private _bladeMidZ: number = 0\n\n build() {\n super.build()\n\n this._forkGroup = undefined\n this._forkTopY = 0\n this._bladeMidZ = 0\n\n const { width, height, depth } = this.component.state\n const emissiveColor = (this.component.state.lampEmissive as string) || '#222222'\n const status = this.component.state.status\n const lampOn = status && status !== 'idle'\n\n // Actuators\n const D = numOr(depth, Math.max(width, height) * 4)\n const carriageRaw = numOr((this.component.state as any).carriageHeight, D * 0.4)\n const carriageHeight = Math.max(0, Math.min(carriageRaw, D * 0.85))\n\n const forkLength = numOr((this.component.state as any).forkLength, height * 0.6)\n const forkExtensionRaw = numOr((this.component.state as any).forkExtension, 0)\n const forkExtension = Math.max(-forkLength, Math.min(forkLength, forkExtensionRaw))\n const forkLift = numOr((this.component.state as any).forkLift, 0)\n\n // ── Axis convention (FIXED): ─────────────────────────────────────\n // Rail = X (state.left = 2D X = 3D X). Crane 좌우 이동.\n // Fork = Z (2D Y = 3D Z). Fork 앞뒤 신축.\n // 사용자: \"포크는 앞뒤로, 크레인 본체는 좌우로.\"\n // width = rail-direction 풋프린트 (좁음)\n // height = cross-aisle (fork 방향) 풋프린트 (깊음, fork 가 cell 까지 닿는 거리)\n const S = Math.min(width, height)\n const railH = S * 0.04\n const baseH = S * 0.18\n const topFrameH = S * 0.1\n const topGuideH = S * 0.1\n const carriageH = S * 0.12\n const mastW = width * 0.1 // mast X 단면 (along rail)\n const mastD = height * 0.25 // mast Z 단면 (cross-rail)\n const mastSpacing = width * 0.7 // 두 mast X 간격\n const bladeW = S * 0.1\n const bladeH = S * 0.05\n const bladeL = forkLength\n const bladeSpacing = mastSpacing * 0.45\n const carriageW = mastSpacing - mastW * 0.2\n const carriageZ = height * 0.55\n const cabW = S * 0.4\n const cabH = S * 0.4\n const cabD = S * 0.3\n\n const baseY = -D / 2\n const mastH = Math.max(D - railH * 2 - baseH - topFrameH - topGuideH, S * 0.5)\n\n // ── Materials ─────────────────────────────────────────────────────\n const mastMat = new THREE.MeshStandardMaterial({ color: MAST_COLOR, metalness: 0.3, roughness: 0.5 })\n const trolleyMat = new THREE.MeshStandardMaterial({ color: TROLLEY_COLOR, metalness: 0.6, roughness: 0.5 })\n const carriageMat = new THREE.MeshStandardMaterial({ color: CARRIAGE_COLOR, metalness: 0.85, roughness: 0.35 })\n const cabinetMat = new THREE.MeshStandardMaterial({ color: CONTROLLER_COLOR, metalness: 0.2, roughness: 0.6 })\n const forkMat = new THREE.MeshStandardMaterial({ color: FORK_COLOR, metalness: 0.85, roughness: 0.3 })\n const railMat = new THREE.MeshStandardMaterial({ color: RAIL_COLOR, metalness: 0.9, roughness: 0.3 })\n\n // ── Floor rail ────────────────────────────────────────────────────\n {\n const geo = new THREE.BoxGeometry(width * 1.1, railH, height * 0.35)\n const mesh = new THREE.Mesh(geo, railMat)\n mesh.position.set(0, baseY + railH / 2, 0)\n mesh.receiveShadow = true\n this.object3d.add(mesh)\n }\n\n // ── Base trolley ──────────────────────────────────────────────────\n const baseTrolleyY = baseY + railH + baseH / 2\n {\n const geo = new THREE.BoxGeometry(width * 0.95, baseH, height * 0.7)\n const mesh = new THREE.Mesh(geo, trolleyMat)\n mesh.position.set(0, baseTrolleyY, 0)\n mesh.castShadow = true\n mesh.receiveShadow = true\n this.object3d.add(mesh)\n }\n\n // ── Control cabinet on one side of base ───────────────────────────\n {\n const geo = new THREE.BoxGeometry(cabW, cabH, cabD)\n const cab = new THREE.Mesh(geo, cabinetMat)\n cab.position.set(\n -width * 0.4 + cabW / 2,\n baseTrolleyY + baseH / 2 + cabH / 2,\n -height * 0.25 + cabD / 2\n )\n cab.castShadow = true\n this.object3d.add(cab)\n }\n\n // ── Status lamp ───────────────────────────────────────────────────\n {\n const lampR = S * 0.04\n const lampH = S * 0.1\n const lampMat = new THREE.MeshStandardMaterial({\n color: lampOn ? emissiveColor : LAMP_OFF,\n emissive: lampOn ? emissiveColor : LAMP_OFF,\n emissiveIntensity: lampOn ? 1.5 : 0.2,\n metalness: 0,\n roughness: 0.3\n })\n const geo = new THREE.CylinderGeometry(lampR, lampR * 0.8, lampH, 12)\n const lamp = new THREE.Mesh(geo, lampMat)\n lamp.position.set(width * 0.4, baseTrolleyY + baseH / 2 + lampH / 2, 0)\n this.object3d.add(lamp)\n }\n\n // ── Twin masts ────────────────────────────────────────────────────\n const mastY = baseTrolleyY + baseH / 2 + mastH / 2\n for (const xOff of [-mastSpacing / 2, +mastSpacing / 2]) {\n const geo = new THREE.BoxGeometry(mastW, mastH, mastD)\n const mesh = new THREE.Mesh(geo, mastMat)\n mesh.position.set(xOff, mastY, 0)\n mesh.castShadow = true\n mesh.receiveShadow = true\n this.object3d.add(mesh)\n }\n\n // ── Carriage (between masts) ──────────────────────────────────────\n const carriageY = baseTrolleyY + baseH / 2 + carriageHeight + carriageH / 2\n {\n const geo = new THREE.BoxGeometry(carriageW, carriageH, carriageZ)\n const mesh = new THREE.Mesh(geo, carriageMat)\n mesh.position.set(0, carriageY, 0)\n mesh.castShadow = true\n mesh.receiveShadow = true\n this.object3d.add(mesh)\n }\n\n // ── Two-prong forks (telescopic ±Z) ───────────────────────────────\n // forkExtension 부호: + = +Z, - = -Z. forkExtension=0 일 때 blade 중심이 carriage 안쪽\n // (= retracted, blade tip 이 carriage 앞면 근처).\n const forkSign = forkExtension >= 0 ? 1 : -1\n const forkBaseZ = forkSign * (carriageZ / 2 - bladeL / 2 + Math.abs(forkExtension))\n const forkY = carriageY + forkLift - bladeH / 2\n {\n const group = new THREE.Group()\n group.position.set(0, forkY, forkBaseZ)\n // forkSign 으로 blade tip 방향 설정\n group.rotation.y = forkSign < 0 ? Math.PI : 0\n\n for (const xOff of [-bladeSpacing / 2, +bladeSpacing / 2]) {\n const geo = new THREE.BoxGeometry(bladeW, bladeH, bladeL)\n const mesh = new THREE.Mesh(geo, forkMat)\n mesh.position.set(xOff, 0, 0)\n mesh.castShadow = true\n mesh.receiveShadow = true\n group.add(mesh)\n }\n\n this.object3d.add(group)\n this._forkGroup = group\n this._forkTopY = bladeH / 2\n this._bladeMidZ = 0\n }\n\n // ── Top frame (connects mast tops) ────────────────────────────────\n const topFrameY = mastY + mastH / 2 + topFrameH / 2\n {\n const geo = new THREE.BoxGeometry(mastSpacing + mastW, topFrameH, height * 0.35)\n const mesh = new THREE.Mesh(geo, trolleyMat)\n mesh.position.set(0, topFrameY, 0)\n mesh.castShadow = true\n this.object3d.add(mesh)\n }\n\n // ── Top guide trolley ─────────────────────────────────────────────\n const topGuideY = topFrameY + topFrameH / 2 + topGuideH / 2\n {\n const geo = new THREE.BoxGeometry(mastSpacing + mastW * 2, topGuideH, height * 0.3)\n const mesh = new THREE.Mesh(geo, trolleyMat)\n mesh.position.set(0, topGuideY, 0)\n mesh.castShadow = true\n this.object3d.add(mesh)\n }\n\n // ── Ceiling rail ──────────────────────────────────────────────────\n {\n const geo = new THREE.BoxGeometry(width * 1.1, railH, height * 0.3)\n const mesh = new THREE.Mesh(geo, railMat)\n mesh.position.set(0, topGuideY + topGuideH / 2 + railH / 2, 0)\n this.object3d.add(mesh)\n }\n }\n\n getCarriageFrame(): THREE.Object3D | undefined {\n return this._forkGroup ?? this.object3d\n }\n\n get platformTopY(): number {\n return this._forkTopY\n }\n\n get bladeMidZ(): number {\n return this._bladeMidZ\n }\n\n updateDimension() {}\n\n onchange(after: Record<string, unknown>, before: Record<string, unknown>) {\n if (\n 'width' in after ||\n 'height' in after ||\n 'depth' in after ||\n 'carriageHeight' in after ||\n 'forkLength' in after ||\n 'forkExtension' in after ||\n 'forkLift' in after ||\n 'status' in after ||\n 'bodyColor' in after ||\n 'lampEmissive' in after\n ) {\n this.update()\n return\n }\n super.onchange(after, before)\n }\n\n updateAlpha() {}\n}\n\nfunction numOr(v: unknown, dflt: number): number {\n return typeof v === 'number' && Number.isFinite(v) ? v : dflt\n}\n"]}
1
+ {"version":3,"file":"crane-3d.js","sourceRoot":"","sources":["../src/crane-3d.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * Copyright © HatioLab Inc. All rights reserved.\n *\n * Crane 3D — heavy-duty pallet AS/RS stacker crane (twin-mast).\n *\n * Axis convention (things-scene 3D):\n * +X = aisle 진행 방향 (Crane 좌우 이동, *옆으로 길게* 축)\n * +Y = 수직 (Crane 키, carriage 상하 이동)\n * +Z = aisle 폭 방향 (Rack cell 쪽 / fork 신축 방향)\n *\n * **스케일 룰**:\n * - 모든 가로(X,Z) dimension 과 Y 두께 → 2D footprint (state.width, state.height) 비례\n * - Mast Y 길이만 → state.depth 사용 (수직 키)\n * - Y 두께는 절대 depth (크레인 키) 에 비례 안 함 → 키워도 뭉치 안 두꺼워짐\n * - mm/cm/scene-unit 무관 — 사용자의 footprint 스케일에 자동 맞춤\n *\n * 부품:\n * [Ceiling rail]\n * [Top guide trolley] — 천장 rail 위\n * [Top frame] — 두 mast 상부 연결\n * [Mast L][Mast R] — twin yellow column (X 축으로 떨어져 있음)\n * [Carriage] — 두 mast 사이 가로 frame (carriageHeight 로 상하)\n * └── [Two forks] — carriage 에서 ±Z 신축 (forkExtension)\n * [Base trolley] — 바닥 motor housing (X 길게)\n * └── [Cabinet] — base 한쪽 red 컨트롤박스\n * └── [Status lamp]\n * [Floor rail]\n *\n * Actuators (state):\n * carriageHeight, forkExtension (±), forkLift\n */\n\nimport * as THREE from 'three'\nimport { RealObjectGroup } from '@hatiolab/things-scene'\n\nconst MAST_COLOR = 0xff7a00 // mast — orange\nconst TROLLEY_COLOR = 0x3a4048 // base / top — dark charcoal\nconst CARRIAGE_COLOR = 0xffcc00 // carriage (shuttle) — yellow\nconst CONTROLLER_COLOR = 0xc63333 // cabinet — red\nconst FORK_COLOR = 0xffcc00 // fork — yellow (same as carriage)\nconst RAIL_COLOR = 0x1a1f24 // rail — dark steel\nconst LAMP_OFF = 0x222222\n\nexport class Crane3D extends RealObjectGroup {\n private _forkGroup?: THREE.Group\n private _forkTopY: number = 0\n private _bladeMidZ: number = 0\n\n build() {\n super.build()\n\n this._forkGroup = undefined\n this._forkTopY = 0\n this._bladeMidZ = 0\n\n const { width, height, depth } = this.component.state\n const emissiveColor = (this.component.state.lampEmissive as string) || '#222222'\n const status = this.component.state.status\n const lampOn = status && status !== 'idle'\n\n // Actuators\n const D = numOr(depth, Math.max(width, height) * 4)\n const carriageRaw = numOr((this.component.state as any).carriageHeight, D * 0.4)\n const carriageHeight = Math.max(0, Math.min(carriageRaw, D * 0.85))\n\n const forkLength = numOr((this.component.state as any).forkLength, height * 0.6)\n const forkExtensionRaw = numOr((this.component.state as any).forkExtension, 0)\n const forkExtension = Math.max(-forkLength, Math.min(forkLength, forkExtensionRaw))\n const forkLift = numOr((this.component.state as any).forkLift, 0)\n\n // ── Axis convention (FIXED): ─────────────────────────────────────\n // Rail = X (state.left = 2D X = 3D X). Crane 좌우 이동.\n // Fork = Z (2D Y = 3D Z). Fork 앞뒤 신축.\n // 사용자: \"포크는 앞뒤로, 크레인 본체는 좌우로.\"\n // width = rail-direction 풋프린트 (좁음)\n // height = cross-aisle (fork 방향) 풋프린트 (깊음, fork 가 cell 까지 닿는 거리)\n const S = Math.min(width, height)\n const railH = S * 0.04\n const baseH = S * 0.18\n const topFrameH = S * 0.1\n const topGuideH = S * 0.1\n const carriageH = S * 0.12\n const mastW = width * 0.1 // mast X 단면 (along rail)\n const mastD = height * 0.25 // mast Z 단면 (cross-rail)\n const mastSpacing = width * 0.7 // 두 mast X 간격\n const bladeW = S * 0.1\n const bladeH = S * 0.05\n const bladeL = forkLength\n const bladeSpacing = mastSpacing * 0.45\n const carriageW = mastSpacing - mastW * 0.2\n const carriageZ = height * 0.55\n const cabW = S * 0.4\n const cabH = S * 0.4\n const cabD = S * 0.3\n\n const baseY = -D / 2\n const mastH = Math.max(D - railH * 2 - baseH - topFrameH - topGuideH, S * 0.5)\n\n // ── Materials ─────────────────────────────────────────────────────\n const mastMat = new THREE.MeshStandardMaterial({ color: MAST_COLOR, metalness: 0.3, roughness: 0.5 })\n const trolleyMat = new THREE.MeshStandardMaterial({ color: TROLLEY_COLOR, metalness: 0.6, roughness: 0.5 })\n const carriageMat = new THREE.MeshStandardMaterial({ color: CARRIAGE_COLOR, metalness: 0.85, roughness: 0.35 })\n const cabinetMat = new THREE.MeshStandardMaterial({ color: CONTROLLER_COLOR, metalness: 0.2, roughness: 0.6 })\n const forkMat = new THREE.MeshStandardMaterial({ color: FORK_COLOR, metalness: 0.85, roughness: 0.3 })\n const railMat = new THREE.MeshStandardMaterial({ color: RAIL_COLOR, metalness: 0.9, roughness: 0.3 })\n\n // ── Floor rail ────────────────────────────────────────────────────\n {\n const geo = new THREE.BoxGeometry(width * 1.1, railH, height * 0.35)\n const mesh = new THREE.Mesh(geo, railMat)\n mesh.position.set(0, baseY + railH / 2, 0)\n mesh.receiveShadow = true\n this.object3d.add(mesh)\n }\n\n // ── Base trolley ──────────────────────────────────────────────────\n const baseTrolleyY = baseY + railH + baseH / 2\n {\n const geo = new THREE.BoxGeometry(width * 0.95, baseH, height * 0.7)\n const mesh = new THREE.Mesh(geo, trolleyMat)\n mesh.position.set(0, baseTrolleyY, 0)\n mesh.castShadow = true\n mesh.receiveShadow = true\n this.object3d.add(mesh)\n }\n\n // ── Control cabinet on one side of base ───────────────────────────\n {\n const geo = new THREE.BoxGeometry(cabW, cabH, cabD)\n const cab = new THREE.Mesh(geo, cabinetMat)\n cab.position.set(\n -width * 0.4 + cabW / 2,\n baseTrolleyY + baseH / 2 + cabH / 2,\n -height * 0.25 + cabD / 2\n )\n cab.castShadow = true\n this.object3d.add(cab)\n }\n\n // ── Status lamp ───────────────────────────────────────────────────\n {\n const lampR = S * 0.04\n const lampH = S * 0.1\n const lampMat = new THREE.MeshStandardMaterial({\n color: lampOn ? emissiveColor : LAMP_OFF,\n emissive: lampOn ? emissiveColor : LAMP_OFF,\n emissiveIntensity: lampOn ? 1.5 : 0.2,\n metalness: 0,\n roughness: 0.3\n })\n const geo = new THREE.CylinderGeometry(lampR, lampR * 0.8, lampH, 12)\n const lamp = new THREE.Mesh(geo, lampMat)\n lamp.position.set(width * 0.4, baseTrolleyY + baseH / 2 + lampH / 2, 0)\n this.object3d.add(lamp)\n }\n\n // ── Twin masts ────────────────────────────────────────────────────\n const mastY = baseTrolleyY + baseH / 2 + mastH / 2\n for (const xOff of [-mastSpacing / 2, +mastSpacing / 2]) {\n const geo = new THREE.BoxGeometry(mastW, mastH, mastD)\n const mesh = new THREE.Mesh(geo, mastMat)\n mesh.position.set(xOff, mastY, 0)\n mesh.castShadow = true\n mesh.receiveShadow = true\n this.object3d.add(mesh)\n }\n\n // ── Carriage + Fork 어셈블리 (forkLift 시 함께 이동) ───────────────\n // 시각 단절 방지를 위해 carriage 와 fork 를 *함께* forkLift 만큼 올림.\n // 의미상으로는 carriageHeight 가 mast 위 carriage 위치, forkLift 가 *그 어셈블리의*\n // 추가 미세 Y. 둘 다 적용된 위치를 carriage / fork 둘 다 공유.\n const carriageY = baseTrolleyY + baseH / 2 + carriageHeight + forkLift + carriageH / 2\n {\n const geo = new THREE.BoxGeometry(carriageW, carriageH, carriageZ)\n const mesh = new THREE.Mesh(geo, carriageMat)\n mesh.position.set(0, carriageY, 0)\n mesh.castShadow = true\n mesh.receiveShadow = true\n this.object3d.add(mesh)\n }\n\n // ── Two-prong forks (양옆 stub + active 신축, 2D 와 동일 모델) ─────\n // ext=0: 양 ±Z 면에 작은 stub. carriage 안에 들어있는 인상 (튀어나옴 없음)\n // ext=±forkLen: active 쪽 stub + |ext| 길이로 신장. 반대쪽 stub 유지.\n // 회전 flip 없음 → ext 가 0 을 지날 때 시각 점프 없음.\n const forkY = carriageY // carriage 중심 Y (embed)\n const stubL = Math.min(carriageZ * 0.2, Math.max(bladeL * 0.05, 6))\n const absExt = Math.abs(forkExtension)\n const sign = forkExtension >= 0 ? 1 : -1\n {\n const group = new THREE.Group()\n group.position.set(0, forkY, 0)\n\n // 양옆 stub — 두 prong × 두 측면 = 4 box\n const stubGeo = new THREE.BoxGeometry(bladeW, bladeH, stubL)\n for (const xOff of [-bladeSpacing / 2, +bladeSpacing / 2]) {\n for (const zSide of [-1, +1]) {\n const mesh = new THREE.Mesh(stubGeo, forkMat)\n mesh.position.set(xOff, 0, zSide * (carriageZ / 2 + stubL / 2))\n mesh.castShadow = true\n mesh.receiveShadow = true\n group.add(mesh)\n }\n }\n\n // Active side 신장\n if (absExt > 0.5) {\n const extGeo = new THREE.BoxGeometry(bladeW, bladeH, absExt)\n for (const xOff of [-bladeSpacing / 2, +bladeSpacing / 2]) {\n const mesh = new THREE.Mesh(extGeo, forkMat)\n mesh.position.set(xOff, 0, sign * (carriageZ / 2 + stubL + absExt / 2))\n mesh.castShadow = true\n mesh.receiveShadow = true\n group.add(mesh)\n }\n }\n\n // Pallet — 정지 시 carriage 중심 Z, 신축 시 fork 중간으로 이동.\n const loaded = forkLift > 0 || !!(this.component.state as any).loaded || !!(this.component.state as any).carrying\n if (loaded) {\n const palletMat = new THREE.MeshStandardMaterial({\n color: 0xa08864, metalness: 0.1, roughness: 0.85\n })\n const palletW = bladeSpacing * 1.2\n const palletH = Math.max(bladeH * 2.5, carriageH * 0.5)\n const palletL = Math.max(bladeL * 0.3, carriageZ * 0.6)\n const geo = new THREE.BoxGeometry(palletW, palletH, palletL)\n const pallet = new THREE.Mesh(geo, palletMat)\n const palletZ = absExt < 0.5 ? 0 : sign * (carriageZ / 2 + absExt / 2)\n pallet.position.set(0, carriageH / 2 + palletH / 2, palletZ)\n pallet.castShadow = true\n pallet.receiveShadow = true\n group.add(pallet)\n }\n\n this.object3d.add(group)\n this._forkGroup = group\n this._forkTopY = carriageH / 2\n this._bladeMidZ = absExt < 0.5 ? 0 : sign * (carriageZ / 2 + absExt / 2)\n }\n\n // ── Top frame (connects mast tops) ────────────────────────────────\n const topFrameY = mastY + mastH / 2 + topFrameH / 2\n {\n const geo = new THREE.BoxGeometry(mastSpacing + mastW, topFrameH, height * 0.35)\n const mesh = new THREE.Mesh(geo, trolleyMat)\n mesh.position.set(0, topFrameY, 0)\n mesh.castShadow = true\n this.object3d.add(mesh)\n }\n\n // ── Top guide trolley ─────────────────────────────────────────────\n const topGuideY = topFrameY + topFrameH / 2 + topGuideH / 2\n {\n const geo = new THREE.BoxGeometry(mastSpacing + mastW * 2, topGuideH, height * 0.3)\n const mesh = new THREE.Mesh(geo, trolleyMat)\n mesh.position.set(0, topGuideY, 0)\n mesh.castShadow = true\n this.object3d.add(mesh)\n }\n\n // ── Ceiling rail ──────────────────────────────────────────────────\n {\n const geo = new THREE.BoxGeometry(width * 1.1, railH, height * 0.3)\n const mesh = new THREE.Mesh(geo, railMat)\n mesh.position.set(0, topGuideY + topGuideH / 2 + railH / 2, 0)\n this.object3d.add(mesh)\n }\n }\n\n getCarriageFrame(): THREE.Object3D | undefined {\n return this._forkGroup ?? this.object3d\n }\n\n get platformTopY(): number {\n return this._forkTopY\n }\n\n get bladeMidZ(): number {\n return this._bladeMidZ\n }\n\n updateDimension() {}\n\n onchange(after: Record<string, unknown>, before: Record<string, unknown>) {\n if (\n 'width' in after ||\n 'height' in after ||\n 'depth' in after ||\n 'carriageHeight' in after ||\n 'forkLength' in after ||\n 'forkExtension' in after ||\n 'forkLift' in after ||\n 'status' in after ||\n 'bodyColor' in after ||\n 'lampEmissive' in after\n ) {\n this.update()\n return\n }\n super.onchange(after, before)\n }\n\n updateAlpha() {}\n}\n\nfunction numOr(v: unknown, dflt: number): number {\n return typeof v === 'number' && Number.isFinite(v) ? v : dflt\n}\n"]}
package/dist/crane.d.ts CHANGED
@@ -124,10 +124,23 @@ export default class Crane extends Crane_base {
124
124
  /** Deposit a carrier into a rack cell (semantically = place). */
125
125
  deposit(carrier: Component, cell: Component, options?: MoveOptions): Promise<void>;
126
126
  /**
127
- * 2D top-down rectangle showing the crane's footprint along the rail.
128
- * The crane is much taller than wide, so the 2D mark is small.
127
+ * 2D top-down 표현 크레인으로 명확히 인식되도록 핵심 부품 그림:
128
+ *
129
+ * - rail (테두리 양쪽 가장자리, dark gray)
130
+ * - twin masts (orange, 옆으로 배치) ← 시각 identity 핵심
131
+ * - carriage (yellow, mast 사이) ← 작은 사각형
132
+ * - forks (yellow, 두 평행선, cross-rail 방향으로 forkLength × extension 비율로 신축)
133
+ * - carrier (lift 됐을 때만, gray 박스 + 들림 표시 ▲)
134
+ *
135
+ * 숨은 축 표현:
136
+ * - carriageHeight (mast Y): 오른쪽 mast 옆 세로 게이지 — fill 비율 = carriageHeight/depth
137
+ * - forkLift (carrier 들기): carrier 가 lift 상태일 때 outline + ▲ 마크
138
+ *
139
+ * 컨벤션: width = rail (X), height = cross-rail (Y, fork 신축 방향).
140
+ * state.rotation 은 canvas 가 처리 (다른 컴포넌트와 동일).
129
141
  */
130
142
  render(ctx: CanvasRenderingContext2D): void;
143
+ added(parent: any): void;
131
144
  buildRealObject(): RealObject | undefined;
132
145
  private _simStarted;
133
146
  private _simRunning;
package/dist/crane.js CHANGED
@@ -210,29 +210,171 @@ let Crane = class Crane extends Mover(CarrierHolder(ContainerCapacity(Legendable
210
210
  }
211
211
  // ── 2D rendering ─────────────────────────────────────────────────────────
212
212
  /**
213
- * 2D top-down rectangle showing the crane's footprint along the rail.
214
- * The crane is much taller than wide, so the 2D mark is small.
213
+ * 2D top-down 표현 크레인으로 명확히 인식되도록 핵심 부품 그림:
214
+ *
215
+ * - rail (테두리 양쪽 가장자리, dark gray)
216
+ * - twin masts (orange, 옆으로 배치) ← 시각 identity 핵심
217
+ * - carriage (yellow, mast 사이) ← 작은 사각형
218
+ * - forks (yellow, 두 평행선, cross-rail 방향으로 forkLength × extension 비율로 신축)
219
+ * - carrier (lift 됐을 때만, gray 박스 + 들림 표시 ▲)
220
+ *
221
+ * 숨은 축 표현:
222
+ * - carriageHeight (mast Y): 오른쪽 mast 옆 세로 게이지 — fill 비율 = carriageHeight/depth
223
+ * - forkLift (carrier 들기): carrier 가 lift 상태일 때 outline + ▲ 마크
224
+ *
225
+ * 컨벤션: width = rail (X), height = cross-rail (Y, fork 신축 방향).
226
+ * state.rotation 은 canvas 가 처리 (다른 컴포넌트와 동일).
215
227
  */
216
228
  render(ctx) {
217
- const { width, height, left, top } = this.state;
218
- const fillColor = this.state.bodyColor || '#888';
229
+ const state = this.state;
230
+ const width = num(state.width, 100);
231
+ const height = num(state.height, 200);
232
+ const left = num(state.left, 0);
233
+ const top = num(state.top, 0);
234
+ const depth = num(state.depth, Math.max(width, height) * 4);
235
+ const carriageHeight = clamp(num(state.carriageHeight, 0), 0, depth);
236
+ const forkExtension = num(state.forkExtension, 0); // ± (rail 양쪽 rack)
237
+ const forkLift = num(state.forkLift, 0);
238
+ const cx = left + width / 2;
239
+ const cy = top + height / 2;
240
+ // 적재 여부 — forkLift > 0 (시뮬 중 들어올린 상태) 또는 state.loaded 명시.
241
+ // monitoring 모드는 외부 데이터로 state.loaded 바인딩.
242
+ const carrying = forkLift > 0 || !!state.loaded || !!state.carrying;
243
+ // 높이 비율 (1F=0 → 최상층=1). carriage / carrier 의 크기 scale 에 공통 사용.
244
+ const heightRatio = depth > 0 ? clamp(carriageHeight / depth, 0, 1) : 0;
245
+ const sizeMul = 0.5 + heightRatio * 0.5; // 50% (1F) → 100% (최상층)
246
+ const MAST = '#ff7a00'; // 주황 — 마스트
247
+ const CARRIAGE_C = '#ffcc00'; // 노랑 — carriage shuttle 본체
248
+ const FORK_C = '#a8b0b8'; // 은회색 — fork prong (금속 느낌, carriage 와 구분)
249
+ const DARK = '#3a4048';
250
+ // Carrier — 팔레트 갈색 (carriage 노랑 위에서 대비). lift 시 darker + 두꺼운 외곽.
251
+ const CARRIER = forkLift > 0 ? '#7d5530' : '#a08864';
252
+ const CARRIER_LINE = '#3a2a18';
219
253
  ctx.save();
220
- ctx.fillStyle = fillColor;
221
- ctx.beginPath();
222
- ctx.rect(left, top, width, height);
223
- ctx.fill();
254
+ // 1. Rail — 위/아래 두 줄
255
+ const railW = Math.max(1.5, height * 0.03);
256
+ ctx.fillStyle = DARK;
257
+ ctx.fillRect(left, top, width, railW);
258
+ ctx.fillRect(left, top + height - railW, width, railW);
259
+ // 2. Twin masts — 두 orange 사각형. mast 길이가 *3D 의 mast height (depth)* 를
260
+ // 의미하므로, 그 위에 carriage 위치 indicator 를 매핑하면 1층/N층 인식 가능.
261
+ const mastW = Math.max(2.5, width * 0.07);
262
+ const mastSpacing = width * 0.75; // 넉넉히 — carrier 가 mast 잠식 안 하도록
263
+ const mastY = top + railW;
264
+ const mastH = height - railW * 2;
265
+ const mastX1 = cx - mastSpacing / 2 - mastW / 2;
266
+ const mastX2 = cx + mastSpacing / 2 - mastW / 2;
267
+ ctx.fillStyle = MAST;
268
+ ctx.fillRect(mastX1, mastY, mastW, mastH);
269
+ ctx.fillRect(mastX2, mastY, mastW, mastH);
270
+ // 2-a. Floor tick marks — mast 위에 N 단계 (예: 5 단) 가로 줄. 층 가늠.
271
+ const numFloors = 5;
272
+ ctx.fillStyle = 'rgba(0,0,0,0.25)';
273
+ for (let i = 1; i < numFloors; i++) {
274
+ const tickY = mastY + (mastH * i) / numFloors - 0.5;
275
+ ctx.fillRect(mastX1, tickY, mastW, 1);
276
+ ctx.fillRect(mastX2, tickY, mastW, 1);
277
+ }
278
+ // 2-b. Carriage 위치 indicator — 마스트 *중심 고정* + *size = carriageHeight 비율*.
279
+ // 1층 (=0): 얇은 선. 최상층 (=depth): 마스트 길이의 100%. 성장/수축 = 상승/하강.
280
+ const indLen = Math.max(2, mastH * heightRatio);
281
+ const indY = mastY + mastH / 2 - indLen / 2;
282
+ const indWidth = mastW + 4;
283
+ ctx.fillStyle = '#5a3a00'; // 어두운 갈색 — orange mast 위에서 강한 대비
284
+ ctx.fillRect(mastX1 - 2, indY, indWidth, indLen);
285
+ ctx.fillRect(mastX2 - 2, indY, indWidth, indLen);
286
+ // 3. Carriage (= 적재 deck) — 팔레트 1200×800mm aspect 반영. *폭만* sizeMul 적용 (높이감),
287
+ // 길이 (Y) 고정. carrier / fork 모두 이 carriage 기준으로 derive.
288
+ const carriageW = (mastSpacing - mastW) * sizeMul;
289
+ const carriageH = Math.min((mastSpacing - mastW) * 1.3, (height - railW * 2) * 0.55);
290
+ const carriageX = cx - carriageW / 2;
291
+ const carriageY = cy - carriageH / 2;
292
+ ctx.fillStyle = CARRIAGE_C;
293
+ ctx.fillRect(carriageX, carriageY, carriageW, carriageH);
294
+ // 4. Fork — 좌우 균형:
295
+ // - 양쪽 모두 *작은 stub* 항상 표시 (crane 이 양쪽 rack 사이 있는 인상)
296
+ // - extension 부호 (+/-) 에 따라 active 쪽이 추가로 신축
297
+ // - 신축 길이 = |forkExtension| (실제 reach 만큼)
298
+ const stubLen = Math.max(2, carriageH * 0.5);
299
+ // 포크 폭 — carriage 의 80% (carrier 90% 보다 약간 좁아 carrier 가 fork 양옆으로
300
+ // 살짝 overhang). carriage 가 sizeMul 적용된 폭이라 fork 도 자동 scale 반영.
301
+ const forkW = carriageW * 0.8;
302
+ const forkX = cx - forkW / 2;
303
+ const extLen = Math.abs(forkExtension);
304
+ const sign = forkExtension >= 0 ? 1 : -1;
305
+ ctx.fillStyle = FORK_C;
306
+ // 항상 양쪽 stub
307
+ ctx.fillRect(forkX, carriageY - stubLen, forkW, stubLen); // top stub
308
+ ctx.fillRect(forkX, carriageY + carriageH, forkW, stubLen); // bottom stub
309
+ // active 쪽 추가 신축
310
+ if (extLen > 0.5) {
311
+ if (sign > 0) {
312
+ ctx.fillRect(forkX, carriageY + carriageH + stubLen, forkW, extLen);
313
+ }
314
+ else {
315
+ ctx.fillRect(forkX, carriageY - stubLen - extLen, forkW, extLen);
316
+ }
317
+ }
318
+ // 5. Carrier — *팔레트 크기* 정사각형. 신축 시 fork 끝 (cell 안쪽) 에 배치.
319
+ // - extLen > 0: carrier 가 fork 끝 (cell 안) — pallet 이 cell 로 들어가는 인상
320
+ // - retracted (extLen=0): carriage 중심 — transit 자세
321
+ // - forkLift > 0: 진한 갈색 + 두꺼운 outline (들린 상태 시인)
322
+ if (carrying) {
323
+ // 팔레트 — 항상 carriage 의 90% (폭/길이 모두). carriage 가 sizeMul 적용된 폭이라
324
+ // carrier 도 자동으로 sizeMul 반영. 길이는 carriage 길이 고정에 따라 고정.
325
+ const carrW = carriageW * 0.9;
326
+ const carrH = carriageH * 0.9;
327
+ let carrCenterY;
328
+ if (extLen > 0.5) {
329
+ // 포크의 *중간* 에 carrier 배치 — fork prong 이 pallet pocket 에 끼워진 자연스러운
330
+ // 위치 (현실 AS/RS 에서 pallet 이 fork 끝이 아닌 fork 길이 중간 부근에 안착).
331
+ const forkMid = (stubLen + extLen) / 2;
332
+ carrCenterY = sign > 0
333
+ ? carriageY + carriageH + forkMid
334
+ : carriageY - forkMid;
335
+ }
336
+ else {
337
+ carrCenterY = cy; // transit
338
+ }
339
+ const carrX = cx - carrW / 2;
340
+ const carrY = carrCenterY - carrH / 2;
341
+ // 들린 상태 — drop shadow 로 *떠 있는* 인상 (forkLift > 0). offset 비율 = lift 강도.
342
+ if (forkLift > 0) {
343
+ const forkLengthRef = num(state.forkLength, height * 0.4);
344
+ const liftMax = Math.max(forkLengthRef * 0.05, 20); // forkLift 정상 범위
345
+ const liftRatio = clamp(forkLift / liftMax, 0.3, 1); // 최소 30% 표시 (있다는 것만 보이게)
346
+ const shadowOff = Math.max(2, Math.min(carrW, carrH) * 0.14) * liftRatio;
347
+ ctx.fillStyle = 'rgba(0, 0, 0, 0.4)';
348
+ ctx.fillRect(carrX + shadowOff, carrY + shadowOff, carrW, carrH);
349
+ }
350
+ // Carrier 본체
351
+ ctx.fillStyle = CARRIER;
352
+ ctx.fillRect(carrX, carrY, carrW, carrH);
353
+ ctx.strokeStyle = CARRIER_LINE;
354
+ ctx.lineWidth = forkLift > 0 ? 1.5 : 0.8;
355
+ ctx.strokeRect(carrX, carrY, carrW, carrH);
356
+ }
357
+ // Cargo (carrier) 가 fork 위에 얹혀 fork 신축과 함께 이동하므로 그 자체로 방향
358
+ // (loading/unloading) 이 자연 인식. 별도 status 배지 없음.
359
+ // (carriageHeight 시각화는 mast 위 dark band + floor ticks 로 통합 — 별도 측면 게이지 제거)
224
360
  ctx.restore();
225
361
  }
362
+ // ── Lifecycle — simulate() 자동 시작 ────────────────────────────────────
363
+ added(parent) {
364
+ super.added?.(parent);
365
+ if (this._simStarted)
366
+ return;
367
+ this._simStarted = true;
368
+ // 초기 지연 800~2800ms 사이 random — 여러 crane 이 동기적으로 시작하지 않도록.
369
+ // added 에서 시작 → 2D/3D 모드 무관. buildRealObject 는 3D 진입 시만 호출돼 시점이
370
+ // 늦거나 누락될 수 있으므로 부적합.
371
+ const initialDelay = 800 + Math.random() * 2000;
372
+ setTimeout(() => {
373
+ this.simulate().catch(e => console.error('[Crane] simulate', e));
374
+ }, initialDelay);
375
+ }
226
376
  // ── 3D ───────────────────────────────────────────────────────────────────
227
377
  buildRealObject() {
228
- if (!this._simStarted) {
229
- this._simStarted = true;
230
- // 초기 지연 800~2800ms 사이 random — 여러 crane 이 동기적으로 시작하지 않도록.
231
- const initialDelay = 800 + Math.random() * 2000;
232
- setTimeout(() => {
233
- this.simulate().catch(e => console.error('[Crane] simulate', e));
234
- }, initialDelay);
235
- }
236
378
  return new Crane3D(this);
237
379
  }
238
380
  // ── Random animation (visual smoke test) ─────────────────────────────────
@@ -371,16 +513,18 @@ let Crane = class Crane extends Mover(CarrierHolder(ContainerCapacity(Legendable
371
513
  const sourceCH = Math.random() * D * 0.75;
372
514
  const destCH = Math.random() * D * 0.75;
373
515
  const liftH = Math.max(20, D * 0.02);
374
- const side = this._targetSide;
516
+ // 양쪽 rack 모두 서비스 — source/dest 각각 ±Z random (cycle 별 다름).
517
+ const sideA = Math.random() < 0.5 ? -1 : +1;
518
+ const sideB = Math.random() < 0.5 ? -1 : +1;
375
519
  // Tween duration 은 base 의 70~130% 사이 random — 여러 crane 이 같은 타이밍으로 안 보이도록.
376
520
  const jitter = (base) => base * (0.7 + Math.random() * 0.6);
377
521
  // 이동: crane local X 방향 → canvas (left, top) 둘 다 동시 변경 (rotation 적용 시 diagonal)
378
522
  await this._tween({ status: 'moving', left: source.left, top: source.top, carriageHeight: sourceCH }, jitter(1500));
379
- await this._tween({ status: 'loading', forkExtension: side * forkLen }, jitter(700));
523
+ await this._tween({ status: 'loading', forkExtension: sideA * forkLen }, jitter(700));
380
524
  await this._tween({ forkLift: liftH }, jitter(400));
381
525
  await this._tween({ forkExtension: 0 }, jitter(700));
382
526
  await this._tween({ status: 'moving', left: dest.left, top: dest.top, carriageHeight: destCH }, jitter(1500));
383
- await this._tween({ status: 'unloading', forkExtension: side * forkLen }, jitter(700));
527
+ await this._tween({ status: 'unloading', forkExtension: sideB * forkLen }, jitter(700));
384
528
  await this._tween({ forkLift: 0 }, jitter(400));
385
529
  await this._tween({ status: 'idle', forkExtension: 0 }, jitter(700));
386
530
  // 사이클 사이 짧은 idle (200~1000ms) — 자연스러운 phase 분산
@@ -456,4 +600,22 @@ function resolveCarrierCenterY(c) {
456
600
  function numOr(v, dflt) {
457
601
  return typeof v === 'number' && Number.isFinite(v) ? v : dflt;
458
602
  }
603
+ // ── 2D render helpers ────────────────────────────────────────────────────
604
+ function num(v, dflt) {
605
+ return typeof v === 'number' && Number.isFinite(v) ? v : dflt;
606
+ }
607
+ function clamp(v, min, max) {
608
+ return v < min ? min : v > max ? max : v;
609
+ }
610
+ /** "#rrggbb" 또는 "name" 색에 alpha 적용. 실패 시 fallback rgba. */
611
+ function withAlpha(color, alpha) {
612
+ // "#rrggbb" 형식만 지원 — name color 는 rgba 직접 변환 못해서 그대로 사용
613
+ if (color && color[0] === '#' && color.length === 7) {
614
+ const r = parseInt(color.slice(1, 3), 16);
615
+ const g = parseInt(color.slice(3, 5), 16);
616
+ const b = parseInt(color.slice(5, 7), 16);
617
+ return `rgba(${r},${g},${b},${alpha})`;
618
+ }
619
+ return color;
620
+ }
459
621
  //# sourceMappingURL=crane.js.map