@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-3d.ts
CHANGED
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
*/
|
|
32
32
|
|
|
33
33
|
import * as THREE from 'three'
|
|
34
|
-
import { RealObjectGroup } from '@hatiolab/things-scene'
|
|
34
|
+
import { RealObject, RealObjectGroup } from '@hatiolab/things-scene'
|
|
35
35
|
|
|
36
36
|
const MAST_COLOR = 0xff7a00 // mast — orange
|
|
37
37
|
const TROLLEY_COLOR = 0x3a4048 // base / top — dark charcoal
|
|
@@ -43,15 +43,33 @@ const LAMP_OFF = 0x222222
|
|
|
43
43
|
|
|
44
44
|
export class Crane3D extends RealObjectGroup {
|
|
45
45
|
private _forkGroup?: THREE.Group
|
|
46
|
-
private
|
|
46
|
+
private _carrierBaseY: number = 0
|
|
47
47
|
private _bladeMidZ: number = 0
|
|
48
|
+
/** floor rail 만 제외한 나머지 (trolley + masts + carriage + fork) 의 movable parent. */
|
|
49
|
+
private _trolleyGroup?: THREE.Group
|
|
50
|
+
/** carriage + fork 의 lift parent (carriageHeight + forkLiftRT 변경 시 Y 만 update). */
|
|
51
|
+
private _carriageLiftGroup?: THREE.Group
|
|
52
|
+
/** Fork active extension mesh — scale.z 와 position.z 로 lerp (rebuild 없이). */
|
|
53
|
+
private _extLeftMesh?: THREE.Mesh
|
|
54
|
+
private _extRightMesh?: THREE.Mesh
|
|
55
|
+
private _extBaseParams?: { bladeSpacing: number; carriageZ: number; stubL: number }
|
|
56
|
+
/** Fork mesh 의 group-local Y center (carriage 위 + bladeH/2). _applyForkExtensionMeshes 가 ext mesh Y 결정. */
|
|
57
|
+
private _forkOffsetY: number = 0
|
|
58
|
+
/** liftGroup.position.y 재계산용 base parameters. */
|
|
59
|
+
private _liftBaseParams?: { baseTrolleyY: number; baseH: number; carriageH: number; bladeH: number }
|
|
48
60
|
|
|
49
61
|
build() {
|
|
50
62
|
super.build()
|
|
51
63
|
|
|
52
64
|
this._forkGroup = undefined
|
|
53
|
-
this.
|
|
65
|
+
this._carrierBaseY = 0
|
|
54
66
|
this._bladeMidZ = 0
|
|
67
|
+
this._trolleyGroup = undefined
|
|
68
|
+
this._carriageLiftGroup = undefined
|
|
69
|
+
this._extLeftMesh = undefined
|
|
70
|
+
this._extRightMesh = undefined
|
|
71
|
+
this._extBaseParams = undefined
|
|
72
|
+
this._liftBaseParams = undefined
|
|
55
73
|
|
|
56
74
|
const { width, height, depth } = this.component.state
|
|
57
75
|
const emissiveColor = (this.component.state.lampEmissive as string) || '#222222'
|
|
@@ -60,13 +78,15 @@ export class Crane3D extends RealObjectGroup {
|
|
|
60
78
|
|
|
61
79
|
// Actuators
|
|
62
80
|
const D = numOr(depth, Math.max(width, height) * 4)
|
|
63
|
-
const carriageRaw = numOr((this.component.state as any).carriageHeight, D * 0.4)
|
|
81
|
+
const carriageRaw = numOr((this.component.state as any).carriageHeight, (this.component as any)._canonicalDefault?.('carriageHeight') ?? D * 0.4)
|
|
64
82
|
const carriageHeight = Math.max(0, Math.min(carriageRaw, D * 0.85))
|
|
65
83
|
|
|
66
84
|
const forkLength = numOr((this.component.state as any).forkLength, height * 0.6)
|
|
67
85
|
const forkExtensionRaw = numOr((this.component.state as any).forkExtension, 0)
|
|
68
86
|
const forkExtension = Math.max(-forkLength, Math.min(forkLength, forkExtensionRaw))
|
|
69
|
-
|
|
87
|
+
// forkLiftRT — 시뮬 runtime current 들림. state.forkLift 는 *configured 진폭*
|
|
88
|
+
// (사용자 설정) 라 안 건드림. 3D carriage Y 는 runtime 값 사용.
|
|
89
|
+
const forkLift = numOr((this.component.state as any).forkLiftRT, 0)
|
|
70
90
|
|
|
71
91
|
// ── Axis convention (FIXED): ─────────────────────────────────────
|
|
72
92
|
// Rail = X (state.left = 2D X = 3D X). Crane 좌우 이동.
|
|
@@ -80,18 +100,24 @@ export class Crane3D extends RealObjectGroup {
|
|
|
80
100
|
const topFrameH = S * 0.1
|
|
81
101
|
const topGuideH = S * 0.1
|
|
82
102
|
const carriageH = S * 0.12
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
const
|
|
103
|
+
// Carriage assembly 크기 — state.carriageWidth 기반 (rail 길이 = crane.width
|
|
104
|
+
// 와 독립). 미명시 시 rail 의 10%.
|
|
105
|
+
const carriageAssemblyW = numOr((this.component.state as any).carriageWidth, width * 0.1)
|
|
106
|
+
const mastW = carriageAssemblyW * 0.15 // mast X 단면 (along rail)
|
|
107
|
+
const mastD = height * 0.25 // mast Z 단면 (cross-rail)
|
|
108
|
+
const mastSpacing = carriageAssemblyW * 0.85 // 두 mast X 간격
|
|
86
109
|
const bladeW = S * 0.1
|
|
87
|
-
|
|
110
|
+
// bladeH — fork 두께. carriage 보다 얇게 (실 fork prong 의 가는 모양).
|
|
111
|
+
// carriage 와 *같은 Y center (0)* 이고 두께만 carriageH * 0.35.
|
|
112
|
+
const bladeH = carriageH * 0.35
|
|
88
113
|
const bladeL = forkLength
|
|
89
114
|
const bladeSpacing = mastSpacing * 0.45
|
|
90
115
|
const carriageW = mastSpacing - mastW * 0.2
|
|
91
116
|
const carriageZ = height * 0.55
|
|
92
|
-
|
|
93
|
-
const
|
|
94
|
-
const
|
|
117
|
+
// Cabinet — 존재감만. 작게.
|
|
118
|
+
const cabW = S * 0.18
|
|
119
|
+
const cabH = S * 0.18
|
|
120
|
+
const cabD = S * 0.15
|
|
95
121
|
|
|
96
122
|
const baseY = -D / 2
|
|
97
123
|
const mastH = Math.max(D - railH * 2 - baseH - topFrameH - topGuideH, S * 0.5)
|
|
@@ -104,37 +130,52 @@ export class Crane3D extends RealObjectGroup {
|
|
|
104
130
|
const forkMat = new THREE.MeshStandardMaterial({ color: FORK_COLOR, metalness: 0.85, roughness: 0.3 })
|
|
105
131
|
const railMat = new THREE.MeshStandardMaterial({ color: RAIL_COLOR, metalness: 0.9, roughness: 0.3 })
|
|
106
132
|
|
|
107
|
-
// ── Floor rail
|
|
133
|
+
// ── Floor rail (고정 — crane 본체 안 움직임). 폭 = crane.width (overhang 제거).
|
|
108
134
|
{
|
|
109
|
-
const
|
|
135
|
+
const railThin = railH * 0.5
|
|
136
|
+
const geo = new THREE.BoxGeometry(width, railThin, height * 0.15)
|
|
110
137
|
const mesh = new THREE.Mesh(geo, railMat)
|
|
111
|
-
mesh.position.set(0, baseY +
|
|
138
|
+
mesh.position.set(0, baseY + railThin / 2, 0)
|
|
112
139
|
mesh.receiveShadow = true
|
|
113
140
|
this.object3d.add(mesh)
|
|
114
141
|
}
|
|
115
142
|
|
|
143
|
+
// ── Trolley group — carriage assembly (rail 위 X 만 이동) ─────────
|
|
144
|
+
// 모든 movable 부품 (base trolley, masts, carriage, fork, cabinet, lamp)
|
|
145
|
+
// 의 parent. carriagePosition 변경 시 group 의 local X 만 변경.
|
|
146
|
+
const trolleyGroup = new THREE.Group()
|
|
147
|
+
this._trolleyGroup = trolleyGroup
|
|
148
|
+
this.object3d.add(trolleyGroup)
|
|
149
|
+
// 초기 carriagePosition 적용 — rail-local X (0 ~ width) → object3d-local X (-W/2 ~ +W/2)
|
|
150
|
+
const carriagePos = numOr((this.component.state as any).carriagePosition, (this.component as any)._canonicalDefault?.('carriagePosition') ?? width / 2)
|
|
151
|
+
trolleyGroup.position.x = carriagePos - width / 2
|
|
152
|
+
|
|
116
153
|
// ── Base trolley ──────────────────────────────────────────────────
|
|
154
|
+
// Cabinet 이 mast 바깥쪽에 자연스럽게 놓이도록 *trolley 폭을 mast + cabinet
|
|
155
|
+
// padding 까지 확장*. carriage assembly width 보다 양 옆으로 (cabW + gap) × 2.
|
|
156
|
+
const trolleyPad = cabW + S * 0.04
|
|
157
|
+
const baseTrolleyW = carriageAssemblyW + trolleyPad * 2
|
|
117
158
|
const baseTrolleyY = baseY + railH + baseH / 2
|
|
118
159
|
{
|
|
119
|
-
const geo = new THREE.BoxGeometry(
|
|
160
|
+
const geo = new THREE.BoxGeometry(baseTrolleyW, baseH, height * 0.7)
|
|
120
161
|
const mesh = new THREE.Mesh(geo, trolleyMat)
|
|
121
162
|
mesh.position.set(0, baseTrolleyY, 0)
|
|
122
163
|
mesh.castShadow = true
|
|
123
164
|
mesh.receiveShadow = true
|
|
124
|
-
|
|
165
|
+
trolleyGroup.add(mesh)
|
|
125
166
|
}
|
|
126
167
|
|
|
127
|
-
// ── Control cabinet
|
|
168
|
+
// ── Control cabinet — mast 바깥쪽 (trolley 의 *확장 padding 영역* 위) ─────
|
|
128
169
|
{
|
|
129
170
|
const geo = new THREE.BoxGeometry(cabW, cabH, cabD)
|
|
130
171
|
const cab = new THREE.Mesh(geo, cabinetMat)
|
|
131
172
|
cab.position.set(
|
|
132
|
-
-
|
|
173
|
+
-(carriageAssemblyW / 2 + cabW / 2 + S * 0.02), // mast 왼쪽 바깥
|
|
133
174
|
baseTrolleyY + baseH / 2 + cabH / 2,
|
|
134
175
|
-height * 0.25 + cabD / 2
|
|
135
176
|
)
|
|
136
177
|
cab.castShadow = true
|
|
137
|
-
|
|
178
|
+
trolleyGroup.add(cab)
|
|
138
179
|
}
|
|
139
180
|
|
|
140
181
|
// ── Status lamp ───────────────────────────────────────────────────
|
|
@@ -150,8 +191,8 @@ export class Crane3D extends RealObjectGroup {
|
|
|
150
191
|
})
|
|
151
192
|
const geo = new THREE.CylinderGeometry(lampR, lampR * 0.8, lampH, 12)
|
|
152
193
|
const lamp = new THREE.Mesh(geo, lampMat)
|
|
153
|
-
lamp.position.set(
|
|
154
|
-
|
|
194
|
+
lamp.position.set(carriageAssemblyW / 2 + lampR * 1.5 + S * 0.02, baseTrolleyY + baseH / 2 + lampH / 2, 0)
|
|
195
|
+
trolleyGroup.add(lamp)
|
|
155
196
|
}
|
|
156
197
|
|
|
157
198
|
// ── Twin masts ────────────────────────────────────────────────────
|
|
@@ -162,118 +203,131 @@ export class Crane3D extends RealObjectGroup {
|
|
|
162
203
|
mesh.position.set(xOff, mastY, 0)
|
|
163
204
|
mesh.castShadow = true
|
|
164
205
|
mesh.receiveShadow = true
|
|
165
|
-
|
|
206
|
+
trolleyGroup.add(mesh)
|
|
166
207
|
}
|
|
167
208
|
|
|
168
|
-
// ── Carriage + Fork
|
|
169
|
-
//
|
|
170
|
-
//
|
|
171
|
-
//
|
|
172
|
-
const
|
|
209
|
+
// ── Carriage + Fork lift group (carriageHeight + forkLiftRT 따라 Y 이동) ─
|
|
210
|
+
// _carriageLiftGroup 안에 carriage + forkGroup. forkLiftRT / carriageHeight
|
|
211
|
+
// 변경 시 *그룹 Y 만 update* (mesh rebuild X). _forkGroup 의 child carrier 가
|
|
212
|
+
// *dispose 없이 그대로* 함께 따라 움직임.
|
|
213
|
+
const stubL = Math.min(carriageZ * 0.2, Math.max(bladeL * 0.05, 6))
|
|
214
|
+
const liftGroup = new THREE.Group()
|
|
215
|
+
this._carriageLiftGroup = liftGroup
|
|
216
|
+
this._liftBaseParams = { baseTrolleyY, baseH, carriageH, bladeH }
|
|
217
|
+
liftGroup.position.set(0, this._computeLiftGroupY(carriageHeight, forkLift), 0)
|
|
218
|
+
trolleyGroup.add(liftGroup)
|
|
219
|
+
|
|
220
|
+
// Carriage — liftGroup local center
|
|
173
221
|
{
|
|
174
222
|
const geo = new THREE.BoxGeometry(carriageW, carriageH, carriageZ)
|
|
175
223
|
const mesh = new THREE.Mesh(geo, carriageMat)
|
|
176
|
-
mesh.position.set(0,
|
|
224
|
+
mesh.position.set(0, 0, 0)
|
|
177
225
|
mesh.castShadow = true
|
|
178
226
|
mesh.receiveShadow = true
|
|
179
|
-
|
|
227
|
+
liftGroup.add(mesh)
|
|
180
228
|
}
|
|
181
229
|
|
|
182
|
-
// ── Two-prong forks
|
|
183
|
-
//
|
|
184
|
-
//
|
|
185
|
-
//
|
|
186
|
-
const forkY = carriageY // carriage 중심 Y (embed)
|
|
187
|
-
const stubL = Math.min(carriageZ * 0.2, Math.max(bladeL * 0.05, 6))
|
|
230
|
+
// ── Two-prong forks ───────────────────────────────────────────────
|
|
231
|
+
// stub (4 box, fixed) + active extension (2 box, scale.z 로 lerp).
|
|
232
|
+
// active mesh 는 *unit-length* 로 생성. _applyForkExtension 이 scale.z 와
|
|
233
|
+
// position.z 로 길이/방향 update — rebuild 없이 매 frame 부드러운 변형.
|
|
188
234
|
const absExt = Math.abs(forkExtension)
|
|
189
235
|
const sign = forkExtension >= 0 ? 1 : -1
|
|
190
236
|
{
|
|
191
237
|
const group = new THREE.Group()
|
|
192
|
-
|
|
238
|
+
// _forkGroup 은 liftGroup-local center (0,0,0) — frame 일치 단순화.
|
|
239
|
+
// attach localPosition (carrier 자식) 도 *group-local = liftGroup-local* 동일 frame.
|
|
240
|
+
// Fork mesh 자체가 group-local 안에서 carriage *위* 로 (mesh.position.y).
|
|
241
|
+
group.position.set(0, 0, 0)
|
|
242
|
+
|
|
243
|
+
// Fork mesh 가 carriage 와 *수평 (같은 Y 평면)* — carriage 안에 embed.
|
|
244
|
+
// fork mesh group-local Y center = 0 (carriage center 와 같음).
|
|
245
|
+
// fork blade *bottom* 면 group-local Y = -bladeH/2.
|
|
246
|
+
// fork blade *top* 면 group-local Y = +bladeH/2.
|
|
247
|
+
const forkOffsetY = 0
|
|
193
248
|
|
|
194
249
|
// 양옆 stub — 두 prong × 두 측면 = 4 box
|
|
195
250
|
const stubGeo = new THREE.BoxGeometry(bladeW, bladeH, stubL)
|
|
196
251
|
for (const xOff of [-bladeSpacing / 2, +bladeSpacing / 2]) {
|
|
197
252
|
for (const zSide of [-1, +1]) {
|
|
198
253
|
const mesh = new THREE.Mesh(stubGeo, forkMat)
|
|
199
|
-
mesh.position.set(xOff,
|
|
254
|
+
mesh.position.set(xOff, forkOffsetY, zSide * (carriageZ / 2 + stubL / 2))
|
|
200
255
|
mesh.castShadow = true
|
|
201
256
|
mesh.receiveShadow = true
|
|
202
257
|
group.add(mesh)
|
|
203
258
|
}
|
|
204
259
|
}
|
|
205
260
|
|
|
206
|
-
// Active
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
const palletL = Math.max(bladeL * 0.3, carriageZ * 0.6)
|
|
227
|
-
const geo = new THREE.BoxGeometry(palletW, palletH, palletL)
|
|
228
|
-
const pallet = new THREE.Mesh(geo, palletMat)
|
|
229
|
-
const palletZ = absExt < 0.5 ? 0 : sign * (carriageZ / 2 + absExt / 2)
|
|
230
|
-
pallet.position.set(0, carriageH / 2 + palletH / 2, palletZ)
|
|
231
|
-
pallet.castShadow = true
|
|
232
|
-
pallet.receiveShadow = true
|
|
233
|
-
group.add(pallet)
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
this.object3d.add(group)
|
|
261
|
+
// Active extension — unit length, scale.z + position.z 로 변형
|
|
262
|
+
const extGeo = new THREE.BoxGeometry(bladeW, bladeH, 1)
|
|
263
|
+
const extLeft = new THREE.Mesh(extGeo, forkMat)
|
|
264
|
+
const extRight = new THREE.Mesh(extGeo, forkMat)
|
|
265
|
+
this._forkOffsetY = forkOffsetY
|
|
266
|
+
extLeft.castShadow = true
|
|
267
|
+
extLeft.receiveShadow = true
|
|
268
|
+
extRight.castShadow = true
|
|
269
|
+
extRight.receiveShadow = true
|
|
270
|
+
group.add(extLeft)
|
|
271
|
+
group.add(extRight)
|
|
272
|
+
this._extLeftMesh = extLeft
|
|
273
|
+
this._extRightMesh = extRight
|
|
274
|
+
this._extBaseParams = { bladeSpacing, carriageZ, stubL }
|
|
275
|
+
this._applyForkExtensionMeshes(absExt, sign)
|
|
276
|
+
|
|
277
|
+
// carrier 의 초기 Z = sign * absExt (= _applyForkExtensionMeshes 의 공식 동일).
|
|
278
|
+
const carrierZ = sign * absExt
|
|
279
|
+
|
|
280
|
+
liftGroup.add(group)
|
|
237
281
|
this._forkGroup = group
|
|
238
|
-
|
|
239
|
-
|
|
282
|
+
// Carrier 외부 bottom 정렬점 (liftGroup-local Y) = fork blade *bottom* =
|
|
283
|
+
// -bladeH/2 (fork mesh group-local center = 0, 두께 bladeH).
|
|
284
|
+
// 사용자 모델: "fork 의 아랫면 ≈ carrier 의 아랫면" — fork blade 가 carrier
|
|
285
|
+
// 의 bottom 부분 안으로 *찔러 들어감* (겹친 자세).
|
|
286
|
+
this._carrierBaseY = -bladeH / 2
|
|
287
|
+
this._bladeMidZ = carrierZ
|
|
240
288
|
}
|
|
241
289
|
|
|
242
|
-
// ── Top frame (connects mast tops)
|
|
290
|
+
// ── Top frame (connects mast tops) — trolley 함께 이동 ─────────────
|
|
243
291
|
const topFrameY = mastY + mastH / 2 + topFrameH / 2
|
|
244
292
|
{
|
|
245
293
|
const geo = new THREE.BoxGeometry(mastSpacing + mastW, topFrameH, height * 0.35)
|
|
246
294
|
const mesh = new THREE.Mesh(geo, trolleyMat)
|
|
247
295
|
mesh.position.set(0, topFrameY, 0)
|
|
248
296
|
mesh.castShadow = true
|
|
249
|
-
|
|
297
|
+
trolleyGroup.add(mesh)
|
|
250
298
|
}
|
|
251
299
|
|
|
252
|
-
// ── Top guide trolley
|
|
300
|
+
// ── Top guide trolley — trolley 함께 이동 (ceiling rail 위 미끄러짐) ─
|
|
253
301
|
const topGuideY = topFrameY + topFrameH / 2 + topGuideH / 2
|
|
254
302
|
{
|
|
255
303
|
const geo = new THREE.BoxGeometry(mastSpacing + mastW * 2, topGuideH, height * 0.3)
|
|
256
304
|
const mesh = new THREE.Mesh(geo, trolleyMat)
|
|
257
305
|
mesh.position.set(0, topGuideY, 0)
|
|
258
306
|
mesh.castShadow = true
|
|
259
|
-
|
|
307
|
+
trolleyGroup.add(mesh)
|
|
260
308
|
}
|
|
261
309
|
|
|
262
|
-
//
|
|
263
|
-
|
|
264
|
-
const geo = new THREE.BoxGeometry(width * 1.1, railH, height * 0.3)
|
|
265
|
-
const mesh = new THREE.Mesh(geo, railMat)
|
|
266
|
-
mesh.position.set(0, topGuideY + topGuideH / 2 + railH / 2, 0)
|
|
267
|
-
this.object3d.add(mesh)
|
|
268
|
-
}
|
|
310
|
+
// Ceiling rail 생략 — 상단은 top guide trolley 만으로 충분. 사용자 의도.
|
|
311
|
+
|
|
269
312
|
}
|
|
270
313
|
|
|
271
314
|
getCarriageFrame(): THREE.Object3D | undefined {
|
|
272
|
-
|
|
315
|
+
// Fallback chain — carrier 가 *carriage transform (X 이동, Y lift)* 따라오도록.
|
|
316
|
+
// _forkGroup 미존재 시 _carriageLiftGroup (X+Y 따라옴) → _trolleyGroup (X 만) →
|
|
317
|
+
// root (no follow). 절대 root 로 떨어지지 않도록 lift/trolley 우선.
|
|
318
|
+
return this._forkGroup ?? this._carriageLiftGroup ?? this._trolleyGroup ?? this.object3d
|
|
273
319
|
}
|
|
274
320
|
|
|
275
|
-
|
|
276
|
-
|
|
321
|
+
/**
|
|
322
|
+
* Fork blade *bottom* 의 liftGroup-local Y. *carrier 외부 bottom 정렬점*.
|
|
323
|
+
*
|
|
324
|
+
* 모델: carrier 의 외부 bottom 과 fork blade 의 bottom 이 *거의 일치*. fork 가
|
|
325
|
+
* carrier 의 bottom 부분을 *찔러 들어가* carrier 와 *겹친 자세* (pallet pocket
|
|
326
|
+
* 안 fork 진입). attachPointFor 가 `carrierBaseY + carrier.depth/2` 로 carrier
|
|
327
|
+
* center 를 정렬 → carrier bottom = fork blade bottom.
|
|
328
|
+
*/
|
|
329
|
+
get carrierBaseY(): number {
|
|
330
|
+
return this._carrierBaseY
|
|
277
331
|
}
|
|
278
332
|
|
|
279
333
|
get bladeMidZ(): number {
|
|
@@ -283,24 +337,135 @@ export class Crane3D extends RealObjectGroup {
|
|
|
283
337
|
updateDimension() {}
|
|
284
338
|
|
|
285
339
|
onchange(after: Record<string, unknown>, before: Record<string, unknown>) {
|
|
286
|
-
|
|
340
|
+
// carriagePosition — trolleyGroup.position.x (mesh-level update).
|
|
341
|
+
if ('carriagePosition' in after && this._trolleyGroup) {
|
|
342
|
+
const W = numOr(this.component.state.width, 100)
|
|
343
|
+
const pos = numOr(after.carriagePosition, W / 2)
|
|
344
|
+
this._trolleyGroup.position.x = pos - W / 2
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Mesh-level updates — fork extension / lift / carriage height. *rebuild 없이*
|
|
348
|
+
// mesh 의 scale / position 만 변경. _forkGroup 의 child carrier 가 dispose
|
|
349
|
+
// 없이 그대로 따라 움직임 (fork 작업 시 시각 자연스러움).
|
|
350
|
+
//
|
|
351
|
+
// status / bodyColor / lampEmissive 는 *cosmetic* — full rebuild 회피. 별도
|
|
352
|
+
// 처리 없음 시 status 변경 시 carrier dispose → 사라짐 결함의 원인. 향후
|
|
353
|
+
// material color 만 update 하는 path 추가 가능.
|
|
354
|
+
const needsFullRebuild =
|
|
287
355
|
'width' in after ||
|
|
288
356
|
'height' in after ||
|
|
289
357
|
'depth' in after ||
|
|
290
|
-
'carriageHeight' in after ||
|
|
291
358
|
'forkLength' in after ||
|
|
292
|
-
'
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
'
|
|
297
|
-
|
|
359
|
+
'carriageWidth' in after
|
|
360
|
+
|
|
361
|
+
if (!needsFullRebuild) {
|
|
362
|
+
let meshUpdated = false
|
|
363
|
+
if (('carriageHeight' in after || 'forkLiftRT' in after) && this._carriageLiftGroup) {
|
|
364
|
+
const state = this.component.state as any
|
|
365
|
+
const D = numOr(state.depth, Math.max(numOr(state.width, 100), numOr(state.height, 100)) * 4)
|
|
366
|
+
const carriageRaw = numOr(state.carriageHeight, D * 0.4)
|
|
367
|
+
const carriageHeight = Math.max(0, Math.min(carriageRaw, D * 0.85))
|
|
368
|
+
const forkLift = numOr(state.forkLiftRT, 0)
|
|
369
|
+
this._carriageLiftGroup.position.y = this._computeLiftGroupY(carriageHeight, forkLift)
|
|
370
|
+
meshUpdated = true
|
|
371
|
+
}
|
|
372
|
+
if ('forkExtension' in after && this._extLeftMesh && this._extRightMesh) {
|
|
373
|
+
const state = this.component.state as any
|
|
374
|
+
const forkLength = numOr(state.forkLength, numOr(state.height, 100) * 0.6)
|
|
375
|
+
const forkExtensionRaw = numOr(state.forkExtension, 0)
|
|
376
|
+
const forkExtension = Math.max(-forkLength, Math.min(forkLength, forkExtensionRaw))
|
|
377
|
+
const absExt = Math.abs(forkExtension)
|
|
378
|
+
const sign = forkExtension >= 0 ? 1 : -1
|
|
379
|
+
this._applyForkExtensionMeshes(absExt, sign)
|
|
380
|
+
meshUpdated = true
|
|
381
|
+
}
|
|
382
|
+
if (meshUpdated) return
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
if (needsFullRebuild) {
|
|
298
386
|
this.update()
|
|
299
387
|
return
|
|
300
388
|
}
|
|
301
389
|
super.onchange(after, before)
|
|
302
390
|
}
|
|
303
391
|
|
|
392
|
+
/** carriageHeight + forkLiftRT 를 liftGroup.position.y 로 변환. */
|
|
393
|
+
private _computeLiftGroupY(carriageHeight: number, forkLift: number): number {
|
|
394
|
+
const p = this._liftBaseParams
|
|
395
|
+
if (!p) return 0
|
|
396
|
+
return p.baseTrolleyY + p.baseH / 2 + carriageHeight + forkLift + p.carriageH / 2
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Carrier 외부 bottom 의 world Y → carriageHeight state 값 inverse-solve.
|
|
401
|
+
*
|
|
402
|
+
* Forward 공식 (build):
|
|
403
|
+
* liftGroup.y crane-local = baseTrolleyY + baseH/2 + carriageHeight + forkLift + carriageH/2
|
|
404
|
+
* carrier 외부 bottom crane-local = liftGroup.y + carrierBaseY (= -bladeH/2)
|
|
405
|
+
* = baseTrolleyY + baseH/2 + carriageH/2 - bladeH/2 + carriageHeight + forkLift
|
|
406
|
+
*
|
|
407
|
+
* Inverse:
|
|
408
|
+
* carriageHeight = worldY − craneCenterY − (baseTrolleyY + baseH/2 + carriageH/2 − bladeH/2) − forkLift
|
|
409
|
+
*/
|
|
410
|
+
solveCarriageHeightForCarrierBaseWorldY(worldY: number, forkLift: number = 0): number {
|
|
411
|
+
const p = this._liftBaseParams
|
|
412
|
+
if (!p) return 0
|
|
413
|
+
this.object3d.updateWorldMatrix(true, false)
|
|
414
|
+
const v = new THREE.Vector3()
|
|
415
|
+
this.object3d.matrixWorld.decompose(v, new THREE.Quaternion(), new THREE.Vector3())
|
|
416
|
+
const craneCenterWorldY = v.y
|
|
417
|
+
return worldY - craneCenterWorldY - (p.baseTrolleyY + p.baseH / 2 + p.carriageH / 2 - p.bladeH / 2) - forkLift
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* target 의 *crane-local Z* → fork extension 값 inverse-solve.
|
|
422
|
+
*
|
|
423
|
+
* Forward (_applyForkExtensionMeshes): `_bladeMidZ = sign * absExt`
|
|
424
|
+
* (carrier 가 ext 만큼 fork 따라 진출). Inverse: `ext = |localZ|, sign = sign(localZ)`.
|
|
425
|
+
*
|
|
426
|
+
* forkLength 로 clamp — localZ 가 forkLength 보다 멀면 carrier 가 fork tip 까지만.
|
|
427
|
+
*/
|
|
428
|
+
solveForkExtensionForLocalZ(localZ: number): number {
|
|
429
|
+
const sign = localZ >= 0 ? 1 : -1
|
|
430
|
+
const state = this.component.state as any
|
|
431
|
+
const forkLen = numOr(state.forkLength, numOr(state.height, 100) * 0.6)
|
|
432
|
+
const clamped = Math.max(0, Math.min(forkLen, Math.abs(localZ)))
|
|
433
|
+
return sign * clamped
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/** Fork active extension mesh 의 scale.z + position.z + visibility update. */
|
|
437
|
+
private _applyForkExtensionMeshes(absExt: number, sign: number) {
|
|
438
|
+
if (!this._extLeftMesh || !this._extRightMesh || !this._extBaseParams) return
|
|
439
|
+
const { bladeSpacing, carriageZ, stubL } = this._extBaseParams
|
|
440
|
+
const visible = absExt > 0.5
|
|
441
|
+
const len = Math.max(0.001, absExt)
|
|
442
|
+
const posZ = sign * (carriageZ / 2 + stubL + absExt / 2)
|
|
443
|
+
this._extLeftMesh.scale.z = len
|
|
444
|
+
this._extRightMesh.scale.z = len
|
|
445
|
+
this._extLeftMesh.position.set(-bladeSpacing / 2, this._forkOffsetY, posZ)
|
|
446
|
+
this._extRightMesh.position.set(+bladeSpacing / 2, this._forkOffsetY, posZ)
|
|
447
|
+
this._extLeftMesh.visible = visible
|
|
448
|
+
this._extRightMesh.visible = visible
|
|
449
|
+
|
|
450
|
+
// _bladeMidZ = carrier 의 Z 위치 = sign * absExt (= fork extension 만큼 직접).
|
|
451
|
+
// ext=0 → 0 (carriage 정중앙 — retract 끝 자세)
|
|
452
|
+
// ext=L → ±L (fork 가 L 만큼 진출한 위치 = carrier 도 그 위치)
|
|
453
|
+
// 단순 linear — solveForkExtensionForLocalZ 의 inverse 도 단순 (ext = |localZ|).
|
|
454
|
+
this._bladeMidZ = sign * absExt
|
|
455
|
+
|
|
456
|
+
// _forkGroup 의 child carrier 의 Z 도 동기 — fork tip 위치 따라 carrier 가
|
|
457
|
+
// 함께 끌려와야 retract 시각 자연.
|
|
458
|
+
if (this._forkGroup) {
|
|
459
|
+
for (const child of this._forkGroup.children) {
|
|
460
|
+
const ctx = (child as any).userData?.context
|
|
461
|
+
if (ctx && ctx !== this && ctx instanceof RealObject) {
|
|
462
|
+
child.position.z = this._bladeMidZ
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
}
|
|
468
|
+
|
|
304
469
|
updateAlpha() {}
|
|
305
470
|
}
|
|
306
471
|
|