@operato/scene-storage 10.0.0-beta.40 → 10.0.0-beta.42
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 +29 -0
- package/MIGRATION-plan-a-slot-api.md +266 -0
- package/PLAN-A-rack-as-slot-holder.md +164 -0
- package/dist/box.js +18 -0
- package/dist/box.js.map +1 -1
- package/dist/crane-3d.d.ts +47 -2
- package/dist/crane-3d.js +246 -89
- package/dist/crane-3d.js.map +1 -1
- package/dist/crane.d.ts +96 -12
- package/dist/crane.js +395 -100
- package/dist/crane.js.map +1 -1
- package/dist/index.d.ts +3 -4
- package/dist/index.js +1 -2
- package/dist/index.js.map +1 -1
- package/dist/pallet.d.ts +15 -0
- package/dist/pallet.js +38 -2
- package/dist/pallet.js.map +1 -1
- package/dist/parcel-3d.js +22 -18
- package/dist/parcel-3d.js.map +1 -1
- package/dist/parcel.d.ts +4 -3
- package/dist/parcel.js +24 -5
- package/dist/parcel.js.map +1 -1
- package/dist/rack-grid-3d.d.ts +18 -7
- package/dist/rack-grid-3d.js +372 -69
- package/dist/rack-grid-3d.js.map +1 -1
- package/dist/rack-grid-cell.d.ts +21 -72
- package/dist/rack-grid-cell.js +147 -243
- package/dist/rack-grid-cell.js.map +1 -1
- package/dist/rack-grid.d.ts +277 -56
- package/dist/rack-grid.js +1230 -695
- package/dist/rack-grid.js.map +1 -1
- package/dist/rack-materials.d.ts +9 -0
- package/dist/rack-materials.js +55 -0
- package/dist/rack-materials.js.map +1 -0
- package/dist/storage-rack-3d.d.ts +15 -0
- package/dist/storage-rack-3d.js +165 -29
- package/dist/storage-rack-3d.js.map +1 -1
- package/dist/storage-rack.d.ts +253 -32
- package/dist/storage-rack.js +726 -66
- package/dist/storage-rack.js.map +1 -1
- package/package.json +3 -3
- package/src/box.ts +18 -0
- package/src/crane-3d.ts +258 -93
- package/src/crane.ts +445 -110
- package/src/index.ts +3 -4
- package/src/pallet.ts +50 -1
- package/src/parcel-3d.ts +23 -18
- package/src/parcel.ts +24 -5
- package/src/rack-grid-3d.ts +383 -80
- package/src/rack-grid-cell.ts +161 -305
- package/src/rack-grid.ts +1263 -762
- package/src/rack-materials.ts +61 -0
- package/src/storage-rack-3d.ts +182 -29
- package/src/storage-rack.ts +819 -67
- package/test/test-carrier-lifecycle.ts +361 -0
- package/test/test-coord-alignment.ts +201 -0
- package/test/test-crane-geometry.ts +167 -0
- package/test/test-external-to-rack.ts +461 -0
- package/test/test-mover-concurrent-bug.ts +304 -0
- package/test/test-mover-rollback.ts +290 -0
- package/test/test-phase-h-carrier-pickable.ts +4 -3
- package/test/test-r19-place-absorb.ts +174 -0
- package/test/test-rack-3d-attach-real.ts +301 -0
- package/test/test-rack-concurrent.ts +254 -0
- package/test/test-rack-edge-cases.ts +323 -0
- package/test/test-rack-grid-cell.ts +318 -0
- package/test/test-rack-grid-location.ts +657 -0
- package/test/test-real-3d-positioning.ts +158 -0
- package/test/test-slot-center-convention.ts +116 -0
- package/test/test-slot-target.ts +189 -0
- package/test/test-storage-rack-batched.ts +606 -0
- package/test/test-storage-rack-click.ts +329 -0
- package/test/test-storage-rack-slot-api.ts +357 -0
- package/test/test-toscene-convention.ts +162 -0
- package/test/test-user-scenario-sequential.ts +334 -0
- package/translations/en.json +7 -1
- package/translations/ja.json +7 -1
- package/translations/ko.json +7 -1
- package/translations/ms.json +7 -1
- package/translations/zh.json +7 -1
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/rack-column.d.ts +0 -35
- package/dist/rack-column.js +0 -258
- package/dist/rack-column.js.map +0 -1
- package/dist/rack-grid-helpers.d.ts +0 -28
- package/dist/rack-grid-helpers.js +0 -71
- package/dist/rack-grid-helpers.js.map +0 -1
- package/dist/rack-grid-location.d.ts +0 -37
- package/dist/rack-grid-location.js +0 -227
- package/dist/rack-grid-location.js.map +0 -1
- package/dist/storage-cell-3d.d.ts +0 -25
- package/dist/storage-cell-3d.js +0 -88
- package/dist/storage-cell-3d.js.map +0 -1
- package/dist/storage-cell.d.ts +0 -70
- package/dist/storage-cell.js +0 -197
- package/dist/storage-cell.js.map +0 -1
- package/src/rack-column.ts +0 -340
- package/src/rack-grid-helpers.ts +0 -77
- package/src/rack-grid-location.ts +0 -286
- package/src/storage-cell-3d.ts +0 -101
- package/src/storage-cell.ts +0 -247
- package/test/test-rack-grid.ts +0 -77
package/src/crane.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* Copyright © HatioLab Inc. All rights reserved.
|
|
3
3
|
*/
|
|
4
|
-
import { Component, ComponentNature, ContainerAbstract, ContainerCapacity, RealObject, sceneComponent } from '@hatiolab/things-scene'
|
|
5
|
-
import type { SlotDef, State, Material3D } from '@hatiolab/things-scene'
|
|
4
|
+
import { Component, ComponentNature, ContainerAbstract, ContainerCapacity, RealObject, Transfer, getWorldPose, sceneComponent } from '@hatiolab/things-scene'
|
|
5
|
+
import type { SlotDef, State, Material3D, Transferable } from '@hatiolab/things-scene'
|
|
6
6
|
import {
|
|
7
7
|
CarrierHolder,
|
|
8
8
|
Legendable,
|
|
@@ -35,14 +35,6 @@ export interface CraneState extends State {
|
|
|
35
35
|
// ── 운영 상태 ──
|
|
36
36
|
status?: CraneStatus
|
|
37
37
|
|
|
38
|
-
/**
|
|
39
|
-
* Crane 이 서비스 할 타깃 컴포넌트의 refid (또는 id).
|
|
40
|
-
* 명시되면 그 컴포넌트 (또는 그 children) 의 위치를 stop point 로 사용 — heuristic
|
|
41
|
-
* 으로 sibling 추정 안 함. 미명시면 sibling 안에서 추측.
|
|
42
|
-
* 예: RackGrid 의 refid 를 지정하면 그 안의 cell 들 사이에서만 이동.
|
|
43
|
-
*/
|
|
44
|
-
target?: string
|
|
45
|
-
|
|
46
38
|
// ── 액추에이터 ──
|
|
47
39
|
/** 마스트 따라 carriage 의 수직 위치 (mm). */
|
|
48
40
|
carriageHeight?: number
|
|
@@ -62,11 +54,42 @@ export interface CraneState extends State {
|
|
|
62
54
|
*/
|
|
63
55
|
forkExtension?: number
|
|
64
56
|
/**
|
|
65
|
-
* Fork
|
|
66
|
-
*
|
|
57
|
+
* Fork 가 carrier 를 *cell shelf 에서 분리하기 위해 들어올리는 높이* (mm).
|
|
58
|
+
* 사용자가 board 에서 설정하는 *configured 진폭*. engage 의 lift 단계 target.
|
|
59
|
+
* 미명시 시 default 30mm.
|
|
60
|
+
*
|
|
61
|
+
* Note: 시뮬 *진행 중 current 들림 값* 은 별도 internal transient — `forkLiftRT`
|
|
62
|
+
* state. 3D / 2D render 가 carriage Y 결정 시 그 값을 사용.
|
|
67
63
|
*/
|
|
68
64
|
forkLift?: number
|
|
69
65
|
|
|
66
|
+
/**
|
|
67
|
+
* Fork lift *runtime current value* (mm). engage 가 시뮬 진행 중 매 frame
|
|
68
|
+
* setState 로 lerp. *사용자가 설정하는 값 아님* — internal transient.
|
|
69
|
+
*/
|
|
70
|
+
forkLiftRT?: number
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* 자동 random simulate (visual smoke test) 시작 여부.
|
|
74
|
+
* - `undefined` 또는 `false` (default): 자동 시작 안 함. application 이 crane.
|
|
75
|
+
* pickAndPlace 등 직접 제어 시 사용.
|
|
76
|
+
* - `true`: added() 시 자동 시작.
|
|
77
|
+
*/
|
|
78
|
+
simulate?: boolean
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Carriage 의 rail-local X 위치 (0 ~ crane.width). Crane.moveTo / simulate 가
|
|
82
|
+
* lerp. crane 본체 (rail) 는 안 움직임 — carriage assembly (masts + carriage +
|
|
83
|
+
* forks) 만 rail 안 X 만 슬라이드.
|
|
84
|
+
*/
|
|
85
|
+
carriagePosition?: number
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Carriage 의 폭 (rail-local X 방향). 미명시 시 rail (crane.width) 의 ~10%.
|
|
89
|
+
* Target rack 의 cell 폭과 매칭 권유.
|
|
90
|
+
*/
|
|
91
|
+
carriageWidth?: number
|
|
92
|
+
|
|
70
93
|
// ── 3D 재질 ──
|
|
71
94
|
material3d?: Material3D
|
|
72
95
|
}
|
|
@@ -109,10 +132,21 @@ const NATURE: ComponentNature = {
|
|
|
109
132
|
}
|
|
110
133
|
},
|
|
111
134
|
{
|
|
112
|
-
type: '
|
|
113
|
-
label: '
|
|
114
|
-
name: '
|
|
115
|
-
|
|
135
|
+
type: 'checkbox',
|
|
136
|
+
label: 'simulate',
|
|
137
|
+
name: 'simulate'
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
type: 'number',
|
|
141
|
+
label: 'carriage-position',
|
|
142
|
+
name: 'carriagePosition',
|
|
143
|
+
placeholder: 'rail-local X (0 ~ rail width). Crane.moveTo 가 lerp.'
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
type: 'number',
|
|
147
|
+
label: 'carriage-width',
|
|
148
|
+
name: 'carriageWidth',
|
|
149
|
+
placeholder: 'rail-local 폭 (mm). 미명시 rail width × 10%.'
|
|
116
150
|
},
|
|
117
151
|
{
|
|
118
152
|
type: 'number',
|
|
@@ -214,9 +248,59 @@ export default class Crane extends Mover(CarrierHolder(ContainerCapacity(Legenda
|
|
|
214
248
|
|
|
215
249
|
// ── ContainerCapacity ─────────────────────────────────────────────────────
|
|
216
250
|
|
|
217
|
-
/**
|
|
251
|
+
/**
|
|
252
|
+
* Stacker crane carries at most one load at a time on its forks.
|
|
253
|
+
*
|
|
254
|
+
* localPosition.z = 0 명시 — ContainerCapacity.receive 의 atomic setState 가
|
|
255
|
+
* `carrier.state.zPos = slot.localPosition.z + parentGeoOffset = 0` 강제.
|
|
256
|
+
* 미명시 시 zPos update skip → carrier 의 *이전 holder 또는 board model 의
|
|
257
|
+
* 임의 zPos (음수 가능)* 그대로 → _realObject 미빌드 시점 (transient placement
|
|
258
|
+
* 미설정 = state-driven) 에 carrier 가 *지하* 로 가는 결함.
|
|
259
|
+
*/
|
|
218
260
|
get slots(): SlotDef[] {
|
|
219
|
-
return [{ id: 'forks', maxCount: 1 }]
|
|
261
|
+
return [{ id: 'forks', maxCount: 1, localPosition: { x: 0, y: 0, z: 0 } }]
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Acceptance rotation — carrier 가 crane.fork 에 attach 시 *crane 의 local
|
|
266
|
+
* axis (전방)* 으로 정렬. identity quaternion 반환 → atomic setState 가
|
|
267
|
+
* carrier.state.rotation/X/Y = 0 강제. _realObject 미빌드 시점 (transient
|
|
268
|
+
* placement 미설정 = state-driven) fallback 에서도 자세 정렬.
|
|
269
|
+
*
|
|
270
|
+
* follow-holder carryPolicy 와 짝 — 두 path 모두 동일 결과 (local identity).
|
|
271
|
+
*/
|
|
272
|
+
acceptanceRotation(_carrier: Component, _slot?: SlotDef): { x: number; y: number; z: number; w: number } {
|
|
273
|
+
return { x: 0, y: 0, z: 0, w: 1 }
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Arrangement — carrier.state.left/top 의 *기준* 이 *crane 전체 footprint* 가
|
|
278
|
+
* 아닌 *carriage X (carriagePosition) + fork Z (bladeMidZ)*. carriage / fork
|
|
279
|
+
* 의 위치가 carrier 의 위치.
|
|
280
|
+
*
|
|
281
|
+
* left = carriagePosition − carrier.w/2 (carriage X 중심)
|
|
282
|
+
* top = crane.h/2 + bladeMidZ − carrier.h/2 (fork Z 중심)
|
|
283
|
+
*/
|
|
284
|
+
get arrangementStrategy() {
|
|
285
|
+
const crane = this as any
|
|
286
|
+
return {
|
|
287
|
+
positionAt(_idx: number, _slot: any, _occ: number, component?: any) {
|
|
288
|
+
const cw = numOr(crane.state?.width, 100)
|
|
289
|
+
const ch = numOr(crane.state?.height, 100)
|
|
290
|
+
const carrierW = numOr(component?.state?.width, 0)
|
|
291
|
+
const carrierH = numOr(component?.state?.height, 0)
|
|
292
|
+
const carriagePos = numOr(crane.state?.carriagePosition, cw / 2)
|
|
293
|
+
const bladeMidZ = numOr(crane._realObject?.bladeMidZ, 0)
|
|
294
|
+
return {
|
|
295
|
+
x: carriagePos - carrierW / 2,
|
|
296
|
+
y: ch / 2 + bladeMidZ - carrierH / 2,
|
|
297
|
+
z: 0
|
|
298
|
+
}
|
|
299
|
+
},
|
|
300
|
+
capacity(slot: any) {
|
|
301
|
+
return slot.maxCount ?? Infinity
|
|
302
|
+
}
|
|
303
|
+
}
|
|
220
304
|
}
|
|
221
305
|
|
|
222
306
|
// ── CarrierHolder — attach frame (carriage fork position) ─────────────────
|
|
@@ -233,20 +317,17 @@ export default class Crane extends Mover(CarrierHolder(ContainerCapacity(Legenda
|
|
|
233
317
|
attachPointFor(carrier: Component): AttachFrame | null {
|
|
234
318
|
const ro = this._realObject
|
|
235
319
|
const frame = ro?.getCarriageFrame?.()
|
|
236
|
-
if (frame)
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
320
|
+
if (!frame) return null
|
|
321
|
+
const carrierDepth = resolveCarrierDepth(carrier)
|
|
322
|
+
const carrierBaseY = ro?.carrierBaseY ?? 0
|
|
323
|
+
const bladeMidZ = ro?.bladeMidZ ?? 0
|
|
324
|
+
// carrier 외부 bottom = fork blade bottom. carryPolicy: 'follow-holder' —
|
|
325
|
+
// carrier 가 crane 의 *local axis (전방)* 정렬.
|
|
326
|
+
return {
|
|
327
|
+
attach: frame,
|
|
328
|
+
localPosition: { x: 0, y: carrierBaseY + carrierDepth / 2, z: bladeMidZ },
|
|
329
|
+
carryPolicy: 'follow-holder'
|
|
246
330
|
}
|
|
247
|
-
const root = this._realObject?.object3d
|
|
248
|
-
if (!root) return null
|
|
249
|
-
return { attach: root }
|
|
250
331
|
}
|
|
251
332
|
|
|
252
333
|
// ── Mover overrides ───────────────────────────────────────────────────────
|
|
@@ -269,21 +350,184 @@ export default class Crane extends Mover(CarrierHolder(ContainerCapacity(Legenda
|
|
|
269
350
|
* binding sets 'moving' in monitoring mode; override pick()/place() to
|
|
270
351
|
* set it in full simulation environments.
|
|
271
352
|
*/
|
|
353
|
+
/**
|
|
354
|
+
* Crane.moveTo — **Crane 본체 (rail) 은 안 움직임**. *내부 carriage 의 rail-
|
|
355
|
+
* local X 위치 (carriagePosition) 만 lerp*. 실제 gantry / AS/RS crane 의
|
|
356
|
+
* 자연스러운 운동 패턴.
|
|
357
|
+
*
|
|
358
|
+
* target.center 의 world 좌표를 *crane 의 local frame (rail-aligned)* 로
|
|
359
|
+
* 변환 → rail-local X 추출 → carriagePosition setState.
|
|
360
|
+
*
|
|
361
|
+
* Crane.state.rotation = rail 방향. carriage 가 rail 위 X 만 이동.
|
|
362
|
+
*/
|
|
363
|
+
async moveTo(target: Component, options: MoveOptions = {}): Promise<void> {
|
|
364
|
+
// target.center 는 *parent-local* 좌표 — child component (cell) 의 경우
|
|
365
|
+
// *rack-local*. board-absolute 변환 필요 (= toScene).
|
|
366
|
+
const tcLocal = target?.center
|
|
367
|
+
if (!tcLocal || typeof tcLocal.x !== 'number' || typeof tcLocal.y !== 'number') {
|
|
368
|
+
return
|
|
369
|
+
}
|
|
370
|
+
const tcAbs = typeof (target as any).toScene === 'function'
|
|
371
|
+
? (target as any).toScene(tcLocal.x, tcLocal.y)
|
|
372
|
+
: tcLocal
|
|
373
|
+
const tcx = tcAbs.x
|
|
374
|
+
const tcy = tcAbs.y
|
|
375
|
+
|
|
376
|
+
// Crane 도 자체 center 를 toScene 으로 (crane 이 다른 container 의 child 일 수도)
|
|
377
|
+
const W = (this.state.width as number) ?? 100
|
|
378
|
+
const craneCenterLocal = (this as any).center ?? { x: 0, y: 0 }
|
|
379
|
+
const craneCenterAbs = typeof (this as any).toScene === 'function'
|
|
380
|
+
? (this as any).toScene(craneCenterLocal.x, craneCenterLocal.y)
|
|
381
|
+
: craneCenterLocal
|
|
382
|
+
const ccx = craneCenterAbs.x
|
|
383
|
+
const ccy = craneCenterAbs.y
|
|
384
|
+
const rotation = (this.state.rotation as number) ?? 0
|
|
385
|
+
const cos = Math.cos(rotation)
|
|
386
|
+
const sin = Math.sin(rotation)
|
|
387
|
+
|
|
388
|
+
const dx = tcx - ccx
|
|
389
|
+
const dy = tcy - ccy
|
|
390
|
+
const railLocalX = dx * cos + dy * sin
|
|
391
|
+
|
|
392
|
+
const cw = (this.state.carriageWidth as number) ?? W * 0.1
|
|
393
|
+
const minPos = cw / 2
|
|
394
|
+
const maxPos = W - cw / 2
|
|
395
|
+
const newCarriagePos = Math.max(minPos, Math.min(maxPos, railLocalX + W / 2))
|
|
396
|
+
|
|
397
|
+
// X + Y 동시 lerp — carriage 의 횡 (rail) + 종 (mast) 운동 동기.
|
|
398
|
+
//
|
|
399
|
+
// 진입 위치 (= fork blade bottom world Y):
|
|
400
|
+
// pick (no holding): cellBottom
|
|
401
|
+
// place (holding): cellBottom + liftH (들린 carrier 와 함께 진입)
|
|
402
|
+
const tween: Record<string, number> = { carriagePosition: newCarriagePos }
|
|
403
|
+
const targetBottomWorldY = resolveCarrierBottomY(target)
|
|
404
|
+
if (targetBottomWorldY !== null) {
|
|
405
|
+
const isHoldingCarrier = ((this as any).components as any[] | undefined)?.some?.(
|
|
406
|
+
(c: any) => c?._transferSlotId === 'forks'
|
|
407
|
+
) ?? false
|
|
408
|
+
const liftH = numOr((this.state as any).forkLift, 30)
|
|
409
|
+
const approachWorldY = targetBottomWorldY + (isHoldingCarrier ? liftH : 0)
|
|
410
|
+
|
|
411
|
+
const ro = this._realObject
|
|
412
|
+
const currentForkLift = numOr((this.state as any).forkLiftRT, 0)
|
|
413
|
+
const solver = ro?.solveCarriageHeightForCarrierBaseWorldY
|
|
414
|
+
if (typeof solver === 'function') {
|
|
415
|
+
tween.carriageHeight = solver.call(ro, approachWorldY, currentForkLift)
|
|
416
|
+
} else {
|
|
417
|
+
tween.carriageHeight = approachWorldY
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const duration = (options.duration as number) ?? 1500
|
|
422
|
+
this.setState({ status: 'moving' as CraneStatus })
|
|
423
|
+
await this._tween(tween, duration)
|
|
424
|
+
this.setState({ status: 'idle' as CraneStatus })
|
|
425
|
+
}
|
|
426
|
+
|
|
272
427
|
async engage(
|
|
273
428
|
target: Component,
|
|
274
429
|
kind: 'pick' | 'place',
|
|
275
430
|
_options: MoveOptions = {}
|
|
276
431
|
): Promise<void> {
|
|
432
|
+
// carriageHeight 는 moveTo 안 X+Y 동시 lerp 에서 처리.
|
|
433
|
+
// engage 가 책임: fork extension → lift / drop → retract.
|
|
434
|
+
const forkLen = numOr((this.state as any).forkLength, 600)
|
|
435
|
+
// liftH — 사용자 설정 `forkLift` (= configured 진폭). 미설정 시 default 30mm.
|
|
436
|
+
// 시뮬 lerp 는 *forkLiftRT* (runtime current) state 에 적용 — `forkLift` 자체
|
|
437
|
+
// 는 *진폭 의미 보존* 위해 안 건드림.
|
|
438
|
+
const liftH = numOr((this.state as any).forkLift, 30)
|
|
439
|
+
|
|
440
|
+
// Fork side 결정 — target 이 crane 의 어느 *local Z 방향* (cross-rail, fork
|
|
441
|
+
// axis) 인지. things-scene 의 `getWorldPose` 로 양쪽 world pose 산출 후
|
|
442
|
+
// delta 를 crane local frame 으로 inverse-rotate → local Z 부호 = side.
|
|
443
|
+
// pick/place 동일하게 동작 (이전 코드는 carrier=cell.child 케이스에서
|
|
444
|
+
// rack-local vs world 좌표계 혼선으로 pick 시 방향 반대였음).
|
|
445
|
+
let side: 1 | -1 = +1
|
|
446
|
+
try {
|
|
447
|
+
const cranePose = getWorldPose(this)
|
|
448
|
+
const targetPose = getWorldPose(target)
|
|
449
|
+
const dv = targetPose.position.clone().sub(cranePose.position)
|
|
450
|
+
dv.applyQuaternion(cranePose.rotation.clone().invert())
|
|
451
|
+
side = dv.z >= 0 ? +1 : -1
|
|
452
|
+
} catch {
|
|
453
|
+
// fallback — pose 계산 실패 시 +1 (rotation 무관, 의미적으로 안전한 default).
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// Animation duration — fork extend/retract 1000ms, lift/lower 600ms.
|
|
457
|
+
// 이전 (500ms / 300ms) 보다 2배 — 시각 자연스러움 강화.
|
|
458
|
+
const D_EXT = 1000
|
|
459
|
+
const D_LIFT = 600
|
|
460
|
+
|
|
461
|
+
// fork extension target — target 의 local Z 와 _bladeMidZ 매칭. helper 가
|
|
462
|
+
// inverse-solve. mismatch 시 carrier 가 *fork tip 위치 (= local Z 미일치)*
|
|
463
|
+
// 로 jump → 텔레포트 결함. helper 가 정확 일치 보장.
|
|
464
|
+
let extTarget = side * forkLen
|
|
465
|
+
try {
|
|
466
|
+
const cranePose = getWorldPose(this)
|
|
467
|
+
const targetPose = getWorldPose(target)
|
|
468
|
+
const dv = targetPose.position.clone().sub(cranePose.position)
|
|
469
|
+
dv.applyQuaternion(cranePose.rotation.clone().invert())
|
|
470
|
+
const localZ = dv.z
|
|
471
|
+
const solver = (this._realObject as any)?.solveForkExtensionForLocalZ
|
|
472
|
+
if (typeof solver === 'function') {
|
|
473
|
+
const ext = solver.call(this._realObject, localZ)
|
|
474
|
+
if (Number.isFinite(ext)) extTarget = ext
|
|
475
|
+
}
|
|
476
|
+
} catch {
|
|
477
|
+
// fallback — 기존 side * forkLen.
|
|
478
|
+
}
|
|
479
|
+
|
|
277
480
|
if (kind === 'pick') {
|
|
278
481
|
this.setState({ status: 'loading' as CraneStatus })
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
482
|
+
await this._tween({ forkExtension: extTarget }, D_EXT)
|
|
483
|
+
|
|
484
|
+
// Mid-engage reparent — fork 가 cell 안 도달. carrier 를 fork 위로 *즉시
|
|
485
|
+
// snap* (animated:false). Transfer 객체 통과 → monitor panel 추적.
|
|
486
|
+
const source = (target as any).parent
|
|
487
|
+
if (source) {
|
|
488
|
+
try {
|
|
489
|
+
new Transfer({
|
|
490
|
+
source,
|
|
491
|
+
target: this as unknown as Transferable,
|
|
492
|
+
carrier: target,
|
|
493
|
+
options: { animated: false }
|
|
494
|
+
}).executeSync()
|
|
495
|
+
} catch (e) {
|
|
496
|
+
// Transfer 실패 — carrier 그대로. 후속 Mover.pick receive 가 처리.
|
|
497
|
+
}
|
|
282
498
|
}
|
|
499
|
+
|
|
500
|
+
await this._tween({ forkLiftRT: liftH }, D_LIFT)
|
|
501
|
+
await this._tween({ forkExtension: 0 }, D_EXT)
|
|
283
502
|
} else {
|
|
284
503
|
this.setState({ status: 'unloading' as CraneStatus })
|
|
504
|
+
// 1. fork extend (cell 위에서 carrier 자리 위로 진입). extTarget = inverse-
|
|
505
|
+
// solved target.local.z 매칭 위치.
|
|
506
|
+
await this._tween({ forkExtension: extTarget }, D_EXT)
|
|
507
|
+
|
|
508
|
+
// 2. carrier lower — forkLiftRT liftH → 0. carrier 가 cell 바닥 안착.
|
|
509
|
+
await this._tween({ forkLiftRT: 0 }, D_LIFT)
|
|
510
|
+
|
|
511
|
+
// 3. dispatch — carrier world 위치 = cell 내 attach 위치, jump 없음. Transfer 추적.
|
|
512
|
+
const comps = (this as any).components as any[] | undefined
|
|
513
|
+
const carrier = comps?.find?.((c: any) => c?._transferSlotId === 'forks')
|
|
514
|
+
?? comps?.[0]
|
|
515
|
+
if (carrier) {
|
|
516
|
+
try {
|
|
517
|
+
new Transfer({
|
|
518
|
+
source: this as unknown as Transferable,
|
|
519
|
+
target: target as unknown as Transferable,
|
|
520
|
+
carrier,
|
|
521
|
+
options: { animated: false }
|
|
522
|
+
}).executeSync()
|
|
523
|
+
} catch (e) {
|
|
524
|
+
// dispatch 실패 — Mover.place 가 후속 처리.
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// 4. fork retract — 빈 fork.
|
|
529
|
+
await this._tween({ forkExtension: 0 }, D_EXT)
|
|
285
530
|
}
|
|
286
|
-
// In a full simulation: await carriage-motion tween here.
|
|
287
531
|
}
|
|
288
532
|
|
|
289
533
|
// ── Domain aliases ────────────────────────────────────────────────────────
|
|
@@ -326,9 +570,14 @@ export default class Crane extends Mover(CarrierHolder(ContainerCapacity(Legenda
|
|
|
326
570
|
const depth = num(state.depth, Math.max(width, height) * 4)
|
|
327
571
|
const carriageHeight = clamp(num(state.carriageHeight, 0), 0, depth)
|
|
328
572
|
const forkExtension = num(state.forkExtension, 0) // ± (rail 양쪽 rack)
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
const
|
|
573
|
+
// forkLiftRT — *시뮬 runtime current 들림*. *configured 진폭* (state.forkLift)
|
|
574
|
+
// 와 분리. 시뮬 미진행 시 0 (carriage 가 mast 위 carriageHeight 만큼).
|
|
575
|
+
const forkLift = num(state.forkLiftRT, 0)
|
|
576
|
+
|
|
577
|
+
// carriagePosition: rail-local X (0 ~ width). default = rail center.
|
|
578
|
+
// 사용자 의도 — Crane 본체 (rail) 안 움직이고 carriage assembly 만 X 슬라이드.
|
|
579
|
+
const carriagePosition = clamp(num(state.carriagePosition, width / 2), 0, width)
|
|
580
|
+
const cx = left + carriagePosition // masts/carriage/forks 의 중심 X
|
|
332
581
|
const cy = top + height / 2
|
|
333
582
|
// 적재 여부 — forkLift > 0 (시뮬 중 들어올린 상태) 또는 state.loaded 명시.
|
|
334
583
|
// monitoring 모드는 외부 데이터로 state.loaded 바인딩.
|
|
@@ -348,16 +597,28 @@ export default class Crane extends Mover(CarrierHolder(ContainerCapacity(Legenda
|
|
|
348
597
|
|
|
349
598
|
ctx.save()
|
|
350
599
|
|
|
351
|
-
//
|
|
352
|
-
|
|
600
|
+
// 0. Rail footprint — 반투명 회색 fill + 외곽선. 사용자가 board 에서 crane 의
|
|
601
|
+
// *rail 영역* 을 명확히 인식 + 편집 핸들 가능.
|
|
602
|
+
ctx.fillStyle = '#aab0b8'
|
|
603
|
+
ctx.globalAlpha = 0.15
|
|
604
|
+
ctx.fillRect(left, top, width, height)
|
|
605
|
+
ctx.globalAlpha = 1
|
|
606
|
+
ctx.strokeStyle = DARK
|
|
607
|
+
ctx.lineWidth = 1
|
|
608
|
+
ctx.strokeRect(left, top, width, height)
|
|
609
|
+
|
|
610
|
+
// 1. Rail — 하단 한 줄 (얇은 가이드). 톤 절제 — outer footprint 가 이미 영역 표시.
|
|
611
|
+
const railW = Math.max(1, height * 0.02)
|
|
353
612
|
ctx.fillStyle = DARK
|
|
354
|
-
ctx.
|
|
613
|
+
ctx.globalAlpha = 0.6
|
|
355
614
|
ctx.fillRect(left, top + height - railW, width, railW)
|
|
615
|
+
ctx.globalAlpha = 1
|
|
356
616
|
|
|
357
|
-
// 2. Twin masts —
|
|
358
|
-
//
|
|
359
|
-
const
|
|
360
|
-
const
|
|
617
|
+
// 2. Twin masts — *carriageWidth 기반* (crane.width 와 분리). rail (= crane.width)
|
|
618
|
+
// 이 넓어져도 carriage assembly 는 *carriageWidth 만큼* 만.
|
|
619
|
+
const carriageW = num(state.carriageWidth, width * 0.1) // default rail 의 10%
|
|
620
|
+
const mastW = Math.max(2.5, carriageW * 0.15)
|
|
621
|
+
const mastSpacing = carriageW * 0.85
|
|
361
622
|
const mastY = top + railW
|
|
362
623
|
const mastH = height - railW * 2
|
|
363
624
|
const mastX1 = cx - mastSpacing / 2 - mastW / 2
|
|
@@ -384,23 +645,22 @@ export default class Crane extends Mover(CarrierHolder(ContainerCapacity(Legenda
|
|
|
384
645
|
ctx.fillRect(mastX1 - 2, indY, indWidth, indLen)
|
|
385
646
|
ctx.fillRect(mastX2 - 2, indY, indWidth, indLen)
|
|
386
647
|
|
|
387
|
-
// 3. Carriage
|
|
388
|
-
// 길이 (Y) 고정. carrier / fork 모두 이
|
|
389
|
-
const
|
|
648
|
+
// 3. Carriage deck — 팔레트 1200×800mm aspect 반영. *폭만* sizeMul 적용 (높이감),
|
|
649
|
+
// 길이 (Y) 고정. carrier / fork 모두 이 deck 기준으로 derive.
|
|
650
|
+
const deckW = (mastSpacing - mastW) * sizeMul
|
|
390
651
|
const carriageH = Math.min((mastSpacing - mastW) * 1.3, (height - railW * 2) * 0.55)
|
|
391
|
-
const carriageX = cx -
|
|
652
|
+
const carriageX = cx - deckW / 2
|
|
392
653
|
const carriageY = cy - carriageH / 2
|
|
393
654
|
ctx.fillStyle = CARRIAGE_C
|
|
394
|
-
ctx.fillRect(carriageX, carriageY,
|
|
655
|
+
ctx.fillRect(carriageX, carriageY, deckW, carriageH)
|
|
395
656
|
|
|
396
657
|
// 4. Fork — 좌우 균형:
|
|
397
658
|
// - 양쪽 모두 *작은 stub* 항상 표시 (crane 이 양쪽 rack 사이 있는 인상)
|
|
398
659
|
// - extension 부호 (+/-) 에 따라 active 쪽이 추가로 신축
|
|
399
660
|
// - 신축 길이 = |forkExtension| (실제 reach 만큼)
|
|
400
661
|
const stubLen = Math.max(2, carriageH * 0.5)
|
|
401
|
-
// 포크 폭 —
|
|
402
|
-
|
|
403
|
-
const forkW = carriageW * 0.8
|
|
662
|
+
// 포크 폭 — deck 의 80%.
|
|
663
|
+
const forkW = deckW * 0.8
|
|
404
664
|
const forkX = cx - forkW / 2
|
|
405
665
|
const extLen = Math.abs(forkExtension)
|
|
406
666
|
const sign = forkExtension >= 0 ? 1 : -1
|
|
@@ -424,26 +684,20 @@ export default class Crane extends Mover(CarrierHolder(ContainerCapacity(Legenda
|
|
|
424
684
|
if (carrying) {
|
|
425
685
|
// 팔레트 — 항상 carriage 의 90% (폭/길이 모두). carriage 가 sizeMul 적용된 폭이라
|
|
426
686
|
// carrier 도 자동으로 sizeMul 반영. 길이는 carriage 길이 고정에 따라 고정.
|
|
427
|
-
const carrW =
|
|
687
|
+
const carrW = deckW * 0.9
|
|
428
688
|
const carrH = carriageH * 0.9
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
? carriageY + carriageH + forkMid
|
|
436
|
-
: carriageY - forkMid
|
|
437
|
-
} else {
|
|
438
|
-
carrCenterY = cy // transit
|
|
439
|
-
}
|
|
689
|
+
// Carrier 2D 위치 — 3D _bladeMidZ 와 동일 공식 (sign * extLen).
|
|
690
|
+
// extLen=0 → carriage 정중앙. extLen 증가 → cell 방향 으로 진출.
|
|
691
|
+
const offset = extLen
|
|
692
|
+
const carrCenterY = sign > 0
|
|
693
|
+
? carriageY + carriageH / 2 + offset
|
|
694
|
+
: carriageY + carriageH / 2 - offset
|
|
440
695
|
const carrX = cx - carrW / 2
|
|
441
696
|
const carrY = carrCenterY - carrH / 2
|
|
442
697
|
|
|
443
698
|
// 들린 상태 — drop shadow 로 *떠 있는* 인상 (forkLift > 0). offset 비율 = lift 강도.
|
|
444
699
|
if (forkLift > 0) {
|
|
445
|
-
const
|
|
446
|
-
const liftMax = Math.max(forkLengthRef * 0.05, 20) // forkLift 정상 범위
|
|
700
|
+
const liftMax = Math.max(num(state.forkLift, 30), 20) // configured 진폭
|
|
447
701
|
const liftRatio = clamp(forkLift / liftMax, 0.3, 1) // 최소 30% 표시 (있다는 것만 보이게)
|
|
448
702
|
const shadowOff = Math.max(2, Math.min(carrW, carrH) * 0.14) * liftRatio
|
|
449
703
|
ctx.fillStyle = 'rgba(0, 0, 0, 0.4)'
|
|
@@ -470,17 +724,90 @@ export default class Crane extends Mover(CarrierHolder(ContainerCapacity(Legenda
|
|
|
470
724
|
|
|
471
725
|
added(parent: any): void {
|
|
472
726
|
super.added?.(parent)
|
|
727
|
+
|
|
728
|
+
// state ↔ visual single source of truth. state 미설정 시 _canonicalDefault
|
|
729
|
+
// 로 명시 초기화 — build / _tween / 외부 read 모두 동일 값.
|
|
730
|
+
const init: Record<string, number> = {}
|
|
731
|
+
for (const k of ['carriagePosition', 'carriageHeight', 'forkExtension', 'forkLiftRT']) {
|
|
732
|
+
if (typeof (this.state as any)[k] !== 'number') {
|
|
733
|
+
init[k] = this._canonicalDefault(k)
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
if (Object.keys(init).length > 0) this.setState(init)
|
|
737
|
+
|
|
738
|
+
// state.simulate === true 명시 시만 자동 시작. mode 연동은 board view layer
|
|
739
|
+
// 책임 — 컴포넌트 자체는 simulate state 만 본다 (sorter/conveyor 등 다른
|
|
740
|
+
// 시뮬 컴포넌트와 동일 패턴).
|
|
741
|
+
if ((this.state as CraneState).simulate !== true) return
|
|
742
|
+
this._startAutoSimulate()
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
/**
|
|
746
|
+
* Actuator state 의 *canonical default*. state 미설정 시 모든 read 경로
|
|
747
|
+
* (build / _tween / 외부) 가 *동일 값* 보도록.
|
|
748
|
+
*
|
|
749
|
+
* single source 패턴 — visual fallback 과 state fallback 이 *분리* 되어
|
|
750
|
+
* 첫 _tween 시 start 가 visual current 와 어긋나는 jump 결함을 근본 차단.
|
|
751
|
+
*/
|
|
752
|
+
_canonicalDefault(key: string): number {
|
|
753
|
+
const state = this.state as any
|
|
754
|
+
const W = numOr(state.width, 100)
|
|
755
|
+
const H = numOr(state.height, 100)
|
|
756
|
+
const D = numOr(state.depth, Math.max(W, H) * 4)
|
|
757
|
+
switch (key) {
|
|
758
|
+
case 'carriagePosition': return W / 2
|
|
759
|
+
case 'carriageHeight': return D * 0.4
|
|
760
|
+
case 'forkExtension':
|
|
761
|
+
case 'forkLiftRT':
|
|
762
|
+
case 'forkLift':
|
|
763
|
+
return 0
|
|
764
|
+
default: return 0
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
private _startAutoSimulate(): void {
|
|
473
769
|
if (this._simStarted) return
|
|
474
770
|
this._simStarted = true
|
|
475
771
|
// 초기 지연 800~2800ms 사이 random — 여러 crane 이 동기적으로 시작하지 않도록.
|
|
476
|
-
// added 에서 시작 → 2D/3D 모드 무관. buildRealObject 는 3D 진입 시만 호출돼 시점이
|
|
477
|
-
// 늦거나 누락될 수 있으므로 부적합.
|
|
478
772
|
const initialDelay = 800 + Math.random() * 2000
|
|
479
773
|
setTimeout(() => {
|
|
774
|
+
if (this._simAbort || (this.state as CraneState).simulate !== true) return
|
|
480
775
|
this.simulate().catch(e => console.error('[Crane] simulate', e))
|
|
481
776
|
}, initialDelay)
|
|
482
777
|
}
|
|
483
778
|
|
|
779
|
+
/**
|
|
780
|
+
* state.simulate 변경 시 자동 simulate 시작/중단. application 이 런타임 toggle
|
|
781
|
+
* 가능 (예: editor 에서 simulate off, view 에서 on).
|
|
782
|
+
*/
|
|
783
|
+
onchange(after: Record<string, unknown>, before: Record<string, unknown>): void {
|
|
784
|
+
super.onchange(after as any, before as any)
|
|
785
|
+
// carriage 관련 state 변경 시 2D render 명시 invalidate. things-scene 의
|
|
786
|
+
// setState → trigger change 가 render queue 에 자동 등록 못 하는 케이스 방어.
|
|
787
|
+
if (
|
|
788
|
+
'carriagePosition' in after ||
|
|
789
|
+
'carriageWidth' in after ||
|
|
790
|
+
'carriageHeight' in after ||
|
|
791
|
+
'forkExtension' in after ||
|
|
792
|
+
'forkLift' in after ||
|
|
793
|
+
'forkLiftRT' in after
|
|
794
|
+
) {
|
|
795
|
+
;(this as any).invalidate?.()
|
|
796
|
+
}
|
|
797
|
+
if ('simulate' in after) {
|
|
798
|
+
if (after.simulate === true) {
|
|
799
|
+
// 명시 true → 자동 시작 (이미 시작 중이면 noop). mode gating 은
|
|
800
|
+
// frameClock (animate 의 simTime) 이 자동 처리 — modeler 면 simTime
|
|
801
|
+
// 안 흐름, _tween step 호출 안 됨.
|
|
802
|
+
this._simAbort = false
|
|
803
|
+
this._simStarted = false
|
|
804
|
+
this._startAutoSimulate()
|
|
805
|
+
} else {
|
|
806
|
+
this._simAbort = true
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
|
|
484
811
|
// ── 3D ───────────────────────────────────────────────────────────────────
|
|
485
812
|
|
|
486
813
|
buildRealObject(): RealObject | undefined {
|
|
@@ -524,17 +851,14 @@ export default class Crane extends Mover(CarrierHolder(ContainerCapacity(Legenda
|
|
|
524
851
|
private _initRailRange(): void {
|
|
525
852
|
this._railMin = this._railMax = NaN
|
|
526
853
|
|
|
527
|
-
//
|
|
528
|
-
//
|
|
529
|
-
|
|
854
|
+
// simulate cycle 의 random pickAndPlace range — crane 의 parent 컨테이너
|
|
855
|
+
// (= 보통 rack 또는 board) 의 bbox 를 기준으로. parent 가 top-level
|
|
856
|
+
// (root/model-layer) 이면 bbox 의미 없음 → skip.
|
|
530
857
|
const root = (this as any).root
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
if (parent && parent !== root && (parent.state as any)?.width > 0) {
|
|
536
|
-
target = parent
|
|
537
|
-
}
|
|
858
|
+
const parent = (this as any).parent
|
|
859
|
+
let target: any = null
|
|
860
|
+
if (parent && parent !== root && (parent.state as any)?.width > 0) {
|
|
861
|
+
target = parent
|
|
538
862
|
}
|
|
539
863
|
if (!target) return
|
|
540
864
|
|
|
@@ -611,20 +935,16 @@ export default class Crane extends Mover(CarrierHolder(ContainerCapacity(Legenda
|
|
|
611
935
|
const H = numOr((this.state as any).height, 100)
|
|
612
936
|
const D = numOr((this.state as any).depth, W * 4)
|
|
613
937
|
const forkLen = numOr((this.state as any).forkLength, H * 0.6)
|
|
938
|
+
const cw = numOr((this.state as any).carriageWidth, W * 0.1)
|
|
614
939
|
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
const pickRail = () => this._railMin + Math.random() * Math.max(0, this._railMax - this._railMin)
|
|
621
|
-
const toLeftTop = (rail: number) => ({
|
|
622
|
-
left: this._railOriginX + rail * this._railCos - W / 2,
|
|
623
|
-
top: this._railOriginY + rail * this._railSin - H / 2
|
|
624
|
-
})
|
|
940
|
+
// carriagePosition 의 valid range — rail 안 [cw/2, W - cw/2].
|
|
941
|
+
// crane 본체 (rail) 는 안 움직임 — *carriage assembly 만* X 슬라이드.
|
|
942
|
+
const minPos = cw / 2
|
|
943
|
+
const maxPos = Math.max(minPos, W - cw / 2)
|
|
944
|
+
const pickPos = () => minPos + Math.random() * (maxPos - minPos)
|
|
625
945
|
|
|
626
|
-
const
|
|
627
|
-
const
|
|
946
|
+
const sourcePos = pickPos()
|
|
947
|
+
const destPos = pickPos()
|
|
628
948
|
|
|
629
949
|
const sourceCH = Math.random() * D * 0.75
|
|
630
950
|
const destCH = Math.random() * D * 0.75
|
|
@@ -636,14 +956,14 @@ export default class Crane extends Mover(CarrierHolder(ContainerCapacity(Legenda
|
|
|
636
956
|
// Tween duration 은 base 의 70~130% 사이 random — 여러 crane 이 같은 타이밍으로 안 보이도록.
|
|
637
957
|
const jitter = (base: number): number => base * (0.7 + Math.random() * 0.6)
|
|
638
958
|
|
|
639
|
-
// 이동:
|
|
640
|
-
await this._tween({ status: 'moving',
|
|
959
|
+
// 이동: carriagePosition 만 변경 (crane.left/top 안 건드림).
|
|
960
|
+
await this._tween({ status: 'moving', carriagePosition: sourcePos, carriageHeight: sourceCH }, jitter(1500))
|
|
641
961
|
await this._tween({ status: 'loading', forkExtension: sideA * forkLen }, jitter(700))
|
|
642
|
-
await this._tween({
|
|
962
|
+
await this._tween({ forkLiftRT: liftH }, jitter(400))
|
|
643
963
|
await this._tween({ forkExtension: 0 }, jitter(700))
|
|
644
|
-
await this._tween({ status: 'moving',
|
|
964
|
+
await this._tween({ status: 'moving', carriagePosition: destPos, carriageHeight: destCH }, jitter(1500))
|
|
645
965
|
await this._tween({ status: 'unloading', forkExtension: sideB * forkLen }, jitter(700))
|
|
646
|
-
await this._tween({
|
|
966
|
+
await this._tween({ forkLiftRT: 0 }, jitter(400))
|
|
647
967
|
await this._tween({ status: 'idle', forkExtension: 0 }, jitter(700))
|
|
648
968
|
|
|
649
969
|
// 사이클 사이 짧은 idle (200~1000ms) — 자연스러운 phase 분산
|
|
@@ -651,15 +971,16 @@ export default class Crane extends Mover(CarrierHolder(ContainerCapacity(Legenda
|
|
|
651
971
|
}
|
|
652
972
|
|
|
653
973
|
/**
|
|
654
|
-
* things-scene 의 `
|
|
655
|
-
* frameClock.simTime
|
|
974
|
+
* frameClock 기반 tween — things-scene 의 표준 `Component.animate()` 사용.
|
|
975
|
+
* frameClock.simTime 진행 (view mode + scene 활성) 시에만 step 호출 → modeler
|
|
976
|
+
* 또는 paused 시 자동 정지. scene.simulationSpeed 도 자동 대응.
|
|
656
977
|
*/
|
|
657
978
|
private _tween(targets: Record<string, any>, duration: number): Promise<void> {
|
|
658
979
|
const start: Record<string, number> = {}
|
|
659
980
|
for (const k of Object.keys(targets)) {
|
|
660
981
|
if (k === 'status') continue
|
|
661
982
|
const v = (this.state as any)[k]
|
|
662
|
-
start[k] = typeof v === 'number' && Number.isFinite(v) ? v :
|
|
983
|
+
start[k] = typeof v === 'number' && Number.isFinite(v) ? v : this._canonicalDefault(k)
|
|
663
984
|
}
|
|
664
985
|
if (typeof targets.status === 'string') {
|
|
665
986
|
this.setState({ status: targets.status })
|
|
@@ -676,10 +997,6 @@ export default class Crane extends Mover(CarrierHolder(ContainerCapacity(Legenda
|
|
|
676
997
|
ease: 'inout',
|
|
677
998
|
delta: 'quad',
|
|
678
999
|
step: (dx: number) => {
|
|
679
|
-
if (this._simAbort) {
|
|
680
|
-
controller.stop?.()
|
|
681
|
-
return finish()
|
|
682
|
-
}
|
|
683
1000
|
const update: Record<string, number> = {}
|
|
684
1001
|
for (const k of Object.keys(start)) {
|
|
685
1002
|
const tgt = targets[k]
|
|
@@ -701,13 +1018,31 @@ function resolveCarrierDepth(c: Component): number {
|
|
|
701
1018
|
return numOr((c as any)?.state?.depth, 0)
|
|
702
1019
|
}
|
|
703
1020
|
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
1021
|
+
|
|
1022
|
+
/**
|
|
1023
|
+
* Target 의 *world bottom Y* — fork blade 가 위치해야 할 높이.
|
|
1024
|
+
*
|
|
1025
|
+
* `getWorldPose(c).position.y` 는 *center Y*. fork 는 carrier 의 *바닥* (pallet
|
|
1026
|
+
* pocket 진입 면) 에 위치해야 함 → center 에서 depth/2 만큼 빼서 bottom.
|
|
1027
|
+
*
|
|
1028
|
+
* `effectiveDepth` 는 RealObject 가 자동 계산한 실제 3D Y 키. 미가용 시 state.
|
|
1029
|
+
* depth fallback.
|
|
1030
|
+
*/
|
|
1031
|
+
function resolveCarrierBottomY(c: Component): number | null {
|
|
1032
|
+
if (!c) return null
|
|
1033
|
+
try {
|
|
1034
|
+
const pose = getWorldPose(c)
|
|
1035
|
+
const centerY = pose?.position?.y
|
|
1036
|
+
if (typeof centerY !== 'number' || !Number.isFinite(centerY)) return null
|
|
1037
|
+
const ro = (c as any)._realObject
|
|
1038
|
+
const eff = ro?.effectiveDepth
|
|
1039
|
+
const depth = typeof eff === 'number' && Number.isFinite(eff)
|
|
1040
|
+
? eff
|
|
1041
|
+
: numOr((c as any)?.state?.depth, 0)
|
|
1042
|
+
return centerY - depth / 2
|
|
1043
|
+
} catch {
|
|
1044
|
+
return null
|
|
1045
|
+
}
|
|
711
1046
|
}
|
|
712
1047
|
|
|
713
1048
|
function numOr(v: unknown, dflt: number): number {
|