@operato/scene-storage 10.0.0-beta.48 → 10.0.0-beta.53
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 +36 -0
- package/dist/box.js +2 -2
- package/dist/box.js.map +1 -1
- package/dist/index.d.ts +9 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -1
- package/dist/pallet.js +2 -2
- package/dist/pallet.js.map +1 -1
- package/dist/parcel.js +2 -2
- package/dist/parcel.js.map +1 -1
- package/dist/picking-station-3d.d.ts +20 -0
- package/dist/picking-station-3d.js +162 -0
- package/dist/picking-station-3d.js.map +1 -0
- package/dist/picking-station.d.ts +50 -0
- package/dist/picking-station.js +186 -0
- package/dist/picking-station.js.map +1 -0
- package/dist/rack-capability.d.ts +11 -0
- package/dist/rack-capability.js +25 -0
- package/dist/rack-capability.js.map +1 -0
- package/dist/rack-grid.d.ts +4 -22
- package/dist/rack-grid.js +23 -115
- package/dist/rack-grid.js.map +1 -1
- package/dist/spot.d.ts +1 -0
- package/dist/spot.js +6 -2
- package/dist/spot.js.map +1 -1
- package/dist/stockpile-3d.d.ts +55 -0
- package/dist/stockpile-3d.js +387 -0
- package/dist/stockpile-3d.js.map +1 -0
- package/dist/stockpile-grid-3d.d.ts +30 -0
- package/dist/stockpile-grid-3d.js +301 -0
- package/dist/stockpile-grid-3d.js.map +1 -0
- package/dist/stockpile-grid.d.ts +85 -0
- package/dist/stockpile-grid.js +361 -0
- package/dist/stockpile-grid.js.map +1 -0
- package/dist/stockpile.d.ts +116 -0
- package/dist/stockpile.js +345 -0
- package/dist/stockpile.js.map +1 -0
- package/dist/storage-rack.d.ts +39 -44
- package/dist/storage-rack.js +71 -146
- package/dist/storage-rack.js.map +1 -1
- package/dist/templates/index.d.ts +80 -0
- package/dist/templates/index.js +7 -1
- package/dist/templates/index.js.map +1 -1
- package/dist/templates/picking-station.d.ts +20 -0
- package/dist/templates/picking-station.js +22 -0
- package/dist/templates/picking-station.js.map +1 -0
- package/dist/templates/stockpile-grid.d.ts +37 -0
- package/dist/templates/stockpile-grid.js +38 -0
- package/dist/templates/stockpile-grid.js.map +1 -0
- package/dist/templates/stockpile.d.ts +29 -0
- package/dist/templates/stockpile.js +31 -0
- package/dist/templates/stockpile.js.map +1 -0
- package/package.json +3 -3
- package/src/box.ts +2 -1
- package/src/index.ts +14 -0
- package/src/pallet.ts +2 -1
- package/src/parcel.ts +2 -1
- package/src/picking-station-3d.ts +164 -0
- package/src/picking-station.ts +220 -0
- package/src/rack-capability.ts +26 -0
- package/src/rack-grid.ts +24 -108
- package/src/spot.ts +15 -1
- package/src/stockpile-3d.ts +412 -0
- package/src/stockpile-grid-3d.ts +327 -0
- package/src/stockpile-grid.ts +408 -0
- package/src/stockpile.ts +427 -0
- package/src/storage-rack.ts +82 -137
- package/src/templates/index.ts +7 -1
- package/src/templates/picking-station.ts +23 -0
- package/src/templates/stockpile-grid.ts +39 -0
- package/src/templates/stockpile.ts +32 -0
- package/test/test-rack-capability.ts +51 -0
- package/translations/en.json +23 -6
- package/translations/ja.json +23 -6
- package/translations/ko.json +22 -5
- package/translations/ms.json +23 -6
- package/translations/zh.json +22 -5
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright © HatioLab Inc. All rights reserved.
|
|
3
|
+
*
|
|
4
|
+
* Stockpile3D — 평치 영역의 3D 시각화.
|
|
5
|
+
*
|
|
6
|
+
* - floor pad (얇은 box, 영역 footprint).
|
|
7
|
+
* - records.length 만큼 가상 carrier mesh 를 *_stackPattern_* 으로 자동 배치.
|
|
8
|
+
* - mesh geometry / material 은 *_carrierPreset_* 별.
|
|
9
|
+
*
|
|
10
|
+
* 핵심 — 실제 carrier 컴포넌트를 자식으로 두지 않고 *_가상 mesh_* 만 그려서 다수
|
|
11
|
+
* 적치를 가볍게 표현. records 변경 시 update() 가 호출되어 mesh 가 재배치된다.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import * as THREE from 'three'
|
|
15
|
+
import { RealObjectGroup } from '@hatiolab/things-scene'
|
|
16
|
+
|
|
17
|
+
import type Stockpile from './stockpile.js'
|
|
18
|
+
import type { StackPattern, CarrierPreset } from './stockpile.js'
|
|
19
|
+
|
|
20
|
+
const PAD_DEPTH = 2
|
|
21
|
+
|
|
22
|
+
/** carrierPreset 별 default 색 (state.carrierWidth/Height/Depth 미지정 시 크기와 함께). */
|
|
23
|
+
const PRESET_COLOR: Record<CarrierPreset, number> = {
|
|
24
|
+
box: 0xb87333, // kraft brown
|
|
25
|
+
pallet: 0x8b6f47, // 짙은 나무
|
|
26
|
+
drum: 0x556679, // 금속/blue-grey
|
|
27
|
+
sack: 0xd4c89a, // beige 천
|
|
28
|
+
crate: 0x9a7548, // 나무 상자
|
|
29
|
+
bale: 0xa89968 // 압축 베일
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const PRESET_DEFAULT_SIZE: Record<CarrierPreset, { w: number; h: number; d: number }> = {
|
|
33
|
+
box: { w: 26, h: 26, d: 20 },
|
|
34
|
+
pallet: { w: 40, h: 30, d: 15 },
|
|
35
|
+
drum: { w: 24, h: 24, d: 30 },
|
|
36
|
+
sack: { w: 30, h: 28, d: 18 },
|
|
37
|
+
crate: { w: 30, h: 30, d: 22 },
|
|
38
|
+
bale: { w: 32, h: 30, d: 18 }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export class Stockpile3D extends RealObjectGroup {
|
|
42
|
+
private _carriersGroup?: THREE.Group
|
|
43
|
+
private _padMesh?: THREE.Mesh
|
|
44
|
+
/** pad 의 외곽선 — strokeStyle 표현 (2D dashed outline 의 3D 대응). */
|
|
45
|
+
private _padOutline?: THREE.LineSegments
|
|
46
|
+
/** record.id → 해당 carrier mesh 매핑 — popup anchor / tether 의 대상으로 사용. */
|
|
47
|
+
private _meshByRecordId: Map<string, THREE.Mesh> = new Map()
|
|
48
|
+
/** Legend 매핑된 색을 material 로 캐시 — 같은 색 record 들은 material 공유 (drawcall ↓). */
|
|
49
|
+
private _materialByColor: Map<number, THREE.MeshStandardMaterial> = new Map()
|
|
50
|
+
|
|
51
|
+
private _getMaterialForColor(colorHex: number): THREE.MeshStandardMaterial {
|
|
52
|
+
let m = this._materialByColor.get(colorHex)
|
|
53
|
+
if (!m) {
|
|
54
|
+
m = new THREE.MeshStandardMaterial({ color: colorHex, roughness: 0.7 })
|
|
55
|
+
this._materialByColor.set(colorHex, m)
|
|
56
|
+
}
|
|
57
|
+
return m
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
private _disposeMaterialCache(): void {
|
|
61
|
+
for (const m of this._materialByColor.values()) m.dispose()
|
|
62
|
+
this._materialByColor.clear()
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
build() {
|
|
66
|
+
super.build()
|
|
67
|
+
const state = this.component.state as any
|
|
68
|
+
const w = state.width ?? 100
|
|
69
|
+
const h = state.height ?? 100
|
|
70
|
+
|
|
71
|
+
// ── floor pad — 영역 표시 (얇은 box) ──────────────────────────────
|
|
72
|
+
const padGeom = new THREE.BoxGeometry(w, PAD_DEPTH, h)
|
|
73
|
+
const padColor = this._padColor()
|
|
74
|
+
const padMat = new THREE.MeshStandardMaterial({
|
|
75
|
+
color: padColor, roughness: 0.85, transparent: true, opacity: 0.55,
|
|
76
|
+
emissive: padColor, emissiveIntensity: 0.05
|
|
77
|
+
})
|
|
78
|
+
this._padMesh = new THREE.Mesh(padGeom, padMat)
|
|
79
|
+
this._padMesh.position.y = PAD_DEPTH / 2
|
|
80
|
+
this._padMesh.receiveShadow = true
|
|
81
|
+
this.object3d.add(this._padMesh)
|
|
82
|
+
|
|
83
|
+
// pad 외곽선 — strokeStyle 3D 표현. 2D 의 dashed outline 과 같은 의도.
|
|
84
|
+
const outlineColor = this._strokeColor()
|
|
85
|
+
const outlineMat = new THREE.LineBasicMaterial({ color: outlineColor })
|
|
86
|
+
this._padOutline = new THREE.LineSegments(new THREE.EdgesGeometry(padGeom), outlineMat)
|
|
87
|
+
this._padOutline.position.y = PAD_DEPTH / 2
|
|
88
|
+
this.object3d.add(this._padOutline)
|
|
89
|
+
|
|
90
|
+
// ── carriers group — records 기반 자동 적치 ─────────────────────
|
|
91
|
+
this._carriersGroup = new THREE.Group()
|
|
92
|
+
this._carriersGroup.position.y = PAD_DEPTH
|
|
93
|
+
this.object3d.add(this._carriersGroup)
|
|
94
|
+
this._rebuildCarriers()
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
update() {
|
|
98
|
+
super.update()
|
|
99
|
+
this._rebuildCarriers()
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* carrier attach point.
|
|
104
|
+
* - slotId 가 record.id 와 매칭되면 그 record 의 mesh 반환 → popup tether 가 정확히
|
|
105
|
+
* 해당 stock 에 연결.
|
|
106
|
+
* - 아니면 pad 반환 (단일 'pile' slot, mover place 의 default anchor).
|
|
107
|
+
*/
|
|
108
|
+
getAttachFrame(slotId?: string): THREE.Object3D | undefined {
|
|
109
|
+
if (slotId) {
|
|
110
|
+
const mesh = this._meshByRecordId.get(slotId)
|
|
111
|
+
if (mesh) return mesh
|
|
112
|
+
}
|
|
113
|
+
return this._padMesh
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** state.fillStyle → THREE.Color. 잘못된 값/없으면 default. capacity 점유율에 따른
|
|
117
|
+
* 색 변경은 강요하지 않음 — 필요하면 사용자가 mapping(eval)으로 fillStyle 을 조정. */
|
|
118
|
+
private _padColor(): THREE.Color {
|
|
119
|
+
const raw = (this.component.state as any)?.fillStyle
|
|
120
|
+
if (typeof raw === 'string' && raw.length > 0) {
|
|
121
|
+
try { return new THREE.Color(raw) } catch { /* fallthrough */ }
|
|
122
|
+
}
|
|
123
|
+
return new THREE.Color(0xc89c5c)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** state.strokeStyle → THREE.Color (pad 외곽선 색). 미설정 시 fillStyle 따라가되
|
|
127
|
+
* 대비 위해 약간 어둡게 */
|
|
128
|
+
private _strokeColor(): THREE.Color {
|
|
129
|
+
const raw = (this.component.state as any)?.strokeStyle
|
|
130
|
+
if (typeof raw === 'string' && raw.length > 0) {
|
|
131
|
+
try { return new THREE.Color(raw) } catch { /* fallthrough */ }
|
|
132
|
+
}
|
|
133
|
+
return this._padColor().clone().multiplyScalar(0.6)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
private _applyStrokeColor(): void {
|
|
137
|
+
if (!this._padOutline) return
|
|
138
|
+
const m = this._padOutline.material as THREE.LineBasicMaterial
|
|
139
|
+
m.color.copy(this._strokeColor())
|
|
140
|
+
m.needsUpdate = true
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** state.fillStyle 변경 즉시 반영 — pad material 의 color + emissive 갱신. */
|
|
144
|
+
private _applyPadColor(): void {
|
|
145
|
+
if (!this._padMesh) return
|
|
146
|
+
const m = this._padMesh.material as THREE.MeshStandardMaterial
|
|
147
|
+
const c = this._padColor()
|
|
148
|
+
m.color.copy(c)
|
|
149
|
+
m.emissive.copy(c)
|
|
150
|
+
m.needsUpdate = true
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
private _rebuildCarriers(): void {
|
|
154
|
+
const g = this._carriersGroup
|
|
155
|
+
if (!g) return
|
|
156
|
+
// 기존 mesh 모두 제거 (geometry/material 은 공유돼 dispose 안 함)
|
|
157
|
+
while (g.children.length > 0) g.remove(g.children[0])
|
|
158
|
+
|
|
159
|
+
const stockpile = this.component as unknown as Stockpile
|
|
160
|
+
const count = stockpile.records.length
|
|
161
|
+
if (count === 0) return
|
|
162
|
+
|
|
163
|
+
const state = stockpile.state as any
|
|
164
|
+
const areaW = state.width ?? 100
|
|
165
|
+
const areaH = state.height ?? 100
|
|
166
|
+
const pattern = (state.stackPattern as StackPattern) ?? 'row'
|
|
167
|
+
const preset = (state.carrierPreset as CarrierPreset) ?? 'box'
|
|
168
|
+
|
|
169
|
+
// carrier 크기 — 명시값 우선, 없으면 preset default
|
|
170
|
+
const def = PRESET_DEFAULT_SIZE[preset] ?? PRESET_DEFAULT_SIZE.box
|
|
171
|
+
const cw = state.carrierWidth ?? def.w
|
|
172
|
+
const ch = state.carrierHeight ?? def.h
|
|
173
|
+
const cd = state.carrierDepth ?? def.d
|
|
174
|
+
const gap = state.carrierGap ?? 10 // 평면(xz) cell 간격 — 0 이면 딱 붙음. carrier 크기 대비 충분히 분리되도록 default 10.
|
|
175
|
+
|
|
176
|
+
const geom = this._carrierGeometry(preset, cw, ch, cd)
|
|
177
|
+
// record 별로 색이 다를 수 있어(Legend 매핑) material 을 record 별로 생성.
|
|
178
|
+
// 같은 색은 material 캐시로 공유해 dispose 비용 / drawcall 최적화.
|
|
179
|
+
const presetDefaultColor = PRESET_COLOR[preset] ?? PRESET_COLOR.box
|
|
180
|
+
this._disposeMaterialCache()
|
|
181
|
+
const resolveColor = (record: any): number => {
|
|
182
|
+
const legendColor = (stockpile as any).resolveLegendColor?.(record)
|
|
183
|
+
if (typeof legendColor === 'string' && legendColor.length > 0) {
|
|
184
|
+
try { return new THREE.Color(legendColor).getHex() } catch { /* fallthrough */ }
|
|
185
|
+
}
|
|
186
|
+
return presetDefaultColor
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const positions = this._computeStackPositions(pattern, count, areaW, areaH, cw, ch, cd, gap, state.stackHeightLimit)
|
|
190
|
+
this._meshByRecordId.clear()
|
|
191
|
+
const records = stockpile.records
|
|
192
|
+
for (let i = 0; i < positions.length; i++) {
|
|
193
|
+
const p = positions[i]
|
|
194
|
+
const record = records[i]
|
|
195
|
+
const colorHex = resolveColor(record)
|
|
196
|
+
const mat = this._getMaterialForColor(colorHex)
|
|
197
|
+
const mesh = new THREE.Mesh(geom, mat)
|
|
198
|
+
mesh.position.set(p.x, p.y, p.z)
|
|
199
|
+
mesh.castShadow = true
|
|
200
|
+
mesh.receiveShadow = true
|
|
201
|
+
// raycast hit → record 식별을 위해 userData 에 recordId 저장 + Map 갱신.
|
|
202
|
+
const rid = records[i]?.id
|
|
203
|
+
if (rid != null) {
|
|
204
|
+
mesh.userData.recordId = String(rid)
|
|
205
|
+
this._meshByRecordId.set(String(rid), mesh)
|
|
206
|
+
}
|
|
207
|
+
g.add(mesh)
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** record.id 로 해당 carrier mesh 직접 조회 — popup tether anchor 용. */
|
|
212
|
+
getMeshByRecordId(recordId: string): THREE.Mesh | undefined {
|
|
213
|
+
return this._meshByRecordId.get(recordId)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* stackPattern 별 각 carrier 의 local 좌표(영역 중심 기준). y 는 pad 위에 쌓이는
|
|
218
|
+
* 높이(cd 단위), x/z 는 영역 평면 위 분포.
|
|
219
|
+
*/
|
|
220
|
+
private _computeStackPositions(
|
|
221
|
+
pattern: StackPattern,
|
|
222
|
+
count: number,
|
|
223
|
+
areaW: number,
|
|
224
|
+
areaH: number,
|
|
225
|
+
cw: number,
|
|
226
|
+
ch: number,
|
|
227
|
+
cd: number,
|
|
228
|
+
gap: number,
|
|
229
|
+
heightLimit?: number
|
|
230
|
+
): Array<{ x: number; y: number; z: number }> {
|
|
231
|
+
const out: Array<{ x: number; y: number; z: number }> = []
|
|
232
|
+
|
|
233
|
+
// column — 한 자리에 수직 적층
|
|
234
|
+
if (pattern === 'column') {
|
|
235
|
+
const layers = heightLimit ?? count
|
|
236
|
+
for (let i = 0; i < count; i++) {
|
|
237
|
+
const layer = Math.min(i, layers - 1)
|
|
238
|
+
out.push({ x: 0, y: layer * cd + cd / 2, z: 0 })
|
|
239
|
+
}
|
|
240
|
+
return out
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// 격자 cell — 행/열 수. cell stride = carrier + gap (인접 carrier 사이 분리).
|
|
244
|
+
const stridX = cw + gap
|
|
245
|
+
const stridZ = ch + gap
|
|
246
|
+
const cols = Math.max(1, Math.floor((areaW + gap) / stridX))
|
|
247
|
+
const rows = Math.max(1, Math.floor((areaH + gap) / stridZ))
|
|
248
|
+
const cellsPerLayer = cols * rows
|
|
249
|
+
|
|
250
|
+
// pyramid — 위로 갈수록 행/열 -1 씩 (사다리꼴)
|
|
251
|
+
if (pattern === 'pyramid') {
|
|
252
|
+
let remaining = count
|
|
253
|
+
let layer = 0
|
|
254
|
+
while (remaining > 0) {
|
|
255
|
+
const lcols = Math.max(1, cols - layer)
|
|
256
|
+
const lrows = Math.max(1, rows - layer)
|
|
257
|
+
const cellsThisLayer = lcols * lrows
|
|
258
|
+
const take = Math.min(remaining, cellsThisLayer)
|
|
259
|
+
for (let i = 0; i < take; i++) {
|
|
260
|
+
const c = i % lcols
|
|
261
|
+
const r = Math.floor(i / lcols)
|
|
262
|
+
// 가운데 정렬 — 줄어든 만큼 offset
|
|
263
|
+
const offsetCol = (cols - lcols) / 2
|
|
264
|
+
const offsetRow = (rows - lrows) / 2
|
|
265
|
+
const x = -areaW / 2 + (c + offsetCol + 0.5) * stridX
|
|
266
|
+
const z = -areaH / 2 + (r + offsetRow + 0.5) * stridZ
|
|
267
|
+
out.push({ x, y: layer * cd + cd / 2, z })
|
|
268
|
+
}
|
|
269
|
+
remaining -= take
|
|
270
|
+
layer++
|
|
271
|
+
if (lcols === 1 && lrows === 1) {
|
|
272
|
+
// 정상에 도달 — 남은 건 column 처럼 위로
|
|
273
|
+
for (let i = 0; i < remaining; i++) {
|
|
274
|
+
out.push({ x: 0, y: (layer + i) * cd + cd / 2, z: 0 })
|
|
275
|
+
}
|
|
276
|
+
break
|
|
277
|
+
}
|
|
278
|
+
if (heightLimit && layer >= heightLimit) break
|
|
279
|
+
}
|
|
280
|
+
return out
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// pile — 자연 더미. row 와 비슷하되 가장자리는 약간 옆으로 흘러내림 효과
|
|
284
|
+
// (간단 구현 — 단마다 약간 random offset).
|
|
285
|
+
if (pattern === 'pile') {
|
|
286
|
+
// pseudo-random (인덱스 기반, deterministic)
|
|
287
|
+
const rng = (i: number) => ((Math.sin(i * 12.9898) * 43758.5453) % 1 + 1) % 1
|
|
288
|
+
for (let i = 0; i < count; i++) {
|
|
289
|
+
const layer = Math.floor(i / cellsPerLayer)
|
|
290
|
+
const idx = i % cellsPerLayer
|
|
291
|
+
const c = idx % cols
|
|
292
|
+
const r = Math.floor(idx / cols)
|
|
293
|
+
const jitterX = (rng(i * 2) - 0.5) * cw * 0.2
|
|
294
|
+
const jitterZ = (rng(i * 2 + 1) - 0.5) * ch * 0.2
|
|
295
|
+
const x = -areaW / 2 + (c + 0.5) * stridX + jitterX
|
|
296
|
+
const z = -areaH / 2 + (r + 0.5) * stridZ + jitterZ
|
|
297
|
+
out.push({ x, y: layer * cd + cd / 2, z })
|
|
298
|
+
if (heightLimit && layer >= heightLimit - 1 && idx === cellsPerLayer - 1) break
|
|
299
|
+
}
|
|
300
|
+
return out
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// row (default) / staggered — 격자 적층
|
|
304
|
+
for (let i = 0; i < count; i++) {
|
|
305
|
+
const layer = Math.floor(i / cellsPerLayer)
|
|
306
|
+
if (heightLimit && layer >= heightLimit) break
|
|
307
|
+
const idx = i % cellsPerLayer
|
|
308
|
+
const c = idx % cols
|
|
309
|
+
const r = Math.floor(idx / cols)
|
|
310
|
+
let x = -areaW / 2 + (c + 0.5) * stridX
|
|
311
|
+
const z = -areaH / 2 + (r + 0.5) * stridZ
|
|
312
|
+
// staggered — 홀수 layer 는 1/2 cell offset (벽돌식)
|
|
313
|
+
if (pattern === 'staggered' && layer % 2 === 1) {
|
|
314
|
+
x += stridX / 2
|
|
315
|
+
}
|
|
316
|
+
out.push({ x, y: layer * cd + cd / 2, z })
|
|
317
|
+
}
|
|
318
|
+
return out
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/** carrierPreset 별 geometry. drum 은 cylinder, 나머지는 box. */
|
|
322
|
+
private _carrierGeometry(preset: CarrierPreset, w: number, h: number, d: number): THREE.BufferGeometry {
|
|
323
|
+
if (preset === 'drum') {
|
|
324
|
+
const radius = Math.min(w, h) / 2
|
|
325
|
+
return new THREE.CylinderGeometry(radius, radius, d, 16)
|
|
326
|
+
}
|
|
327
|
+
// sack/bale — 약간 작게 (자연스러운 형태)
|
|
328
|
+
if (preset === 'sack' || preset === 'bale') {
|
|
329
|
+
return new THREE.BoxGeometry(w * 0.92, d * 0.92, h * 0.92)
|
|
330
|
+
}
|
|
331
|
+
return new THREE.BoxGeometry(w, d, h)
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* state.alpha 변경 시 pad + carriers 의 material opacity 즉시 반영.
|
|
336
|
+
* pad 는 원래 0.55 base opacity 위에 alpha 곱 (영역 표시가 너무 진해지지 않도록).
|
|
337
|
+
* carriers 는 alpha 그대로 (단순한 곱은 fully opaque 이 1, 반투명이 0).
|
|
338
|
+
*/
|
|
339
|
+
updateAlpha(): void {
|
|
340
|
+
const state = this.component.state as any
|
|
341
|
+
const alpha = typeof state.alpha === 'number' ? state.alpha : 1
|
|
342
|
+
if (this._padMesh) {
|
|
343
|
+
const m = this._padMesh.material as THREE.MeshStandardMaterial
|
|
344
|
+
m.opacity = 0.55 * alpha
|
|
345
|
+
m.transparent = m.opacity < 1
|
|
346
|
+
m.needsUpdate = true
|
|
347
|
+
}
|
|
348
|
+
if (this._carriersGroup) {
|
|
349
|
+
for (const child of this._carriersGroup.children) {
|
|
350
|
+
const mesh = child as THREE.Mesh
|
|
351
|
+
const m = mesh.material as THREE.MeshStandardMaterial
|
|
352
|
+
if (!m) continue
|
|
353
|
+
m.opacity = alpha
|
|
354
|
+
m.transparent = alpha < 1
|
|
355
|
+
m.needsUpdate = true
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
updateDimension(): void {
|
|
361
|
+
// width/height 변경 시 pad geometry + outline + carriers 재계산
|
|
362
|
+
if (this._padMesh) {
|
|
363
|
+
const state = this.component.state as any
|
|
364
|
+
const w = state.width ?? 100
|
|
365
|
+
const h = state.height ?? 100
|
|
366
|
+
this._padMesh.geometry.dispose()
|
|
367
|
+
this._padMesh.geometry = new THREE.BoxGeometry(w, PAD_DEPTH, h)
|
|
368
|
+
if (this._padOutline) {
|
|
369
|
+
this._padOutline.geometry.dispose()
|
|
370
|
+
this._padOutline.geometry = new THREE.EdgesGeometry(this._padMesh.geometry)
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
this._rebuildCarriers()
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* state 변경 시 즉시 3D 갱신 — property panel 의 수정이 바로 반영되도록.
|
|
378
|
+
* - width/height → pad geometry 재생성 + carriers 재계산 (updateDimension)
|
|
379
|
+
* - 적치/외형 관련 키 → carriers 만 재계산 (_rebuildCarriers)
|
|
380
|
+
*/
|
|
381
|
+
onchange(after: Record<string, unknown>, before: Record<string, unknown>): void {
|
|
382
|
+
if ('width' in after || 'height' in after) {
|
|
383
|
+
this.updateDimension()
|
|
384
|
+
return
|
|
385
|
+
}
|
|
386
|
+
if ('alpha' in after) {
|
|
387
|
+
this.updateAlpha()
|
|
388
|
+
return
|
|
389
|
+
}
|
|
390
|
+
if ('fillStyle' in after) {
|
|
391
|
+
this._applyPadColor()
|
|
392
|
+
// fillStyle 변경 시 strokeStyle 미설정이면 outline default 도 같이 갱신
|
|
393
|
+
if ((this.component.state as any).strokeStyle == null) this._applyStrokeColor()
|
|
394
|
+
return
|
|
395
|
+
}
|
|
396
|
+
if ('strokeStyle' in after) {
|
|
397
|
+
this._applyStrokeColor()
|
|
398
|
+
return
|
|
399
|
+
}
|
|
400
|
+
const rebuildKeys = [
|
|
401
|
+
'data',
|
|
402
|
+
'stackPattern', 'carrierPreset',
|
|
403
|
+
'carrierWidth', 'carrierHeight', 'carrierDepth', 'carrierGap',
|
|
404
|
+
'capacity', 'stackHeightLimit', 'pickPolicy'
|
|
405
|
+
]
|
|
406
|
+
if (rebuildKeys.some(k => k in after)) {
|
|
407
|
+
this._rebuildCarriers()
|
|
408
|
+
return
|
|
409
|
+
}
|
|
410
|
+
super.onchange?.(after, before)
|
|
411
|
+
}
|
|
412
|
+
}
|