@operato/scene-storage 10.0.0-beta.42 → 10.0.0-beta.44
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 +16 -0
- package/dist/box-3d.d.ts +2 -0
- package/dist/box-3d.js +103 -64
- package/dist/box-3d.js.map +1 -1
- package/dist/pallet-3d.d.ts +2 -0
- package/dist/pallet-3d.js +103 -53
- package/dist/pallet-3d.js.map +1 -1
- package/dist/parcel-3d.js +42 -9
- package/dist/parcel-3d.js.map +1 -1
- package/package.json +2 -2
- package/src/box-3d.ts +121 -68
- package/src/pallet-3d.ts +122 -55
- package/src/parcel-3d.ts +41 -9
- package/tsconfig.tsbuildinfo +1 -1
package/src/pallet-3d.ts
CHANGED
|
@@ -21,6 +21,57 @@ import * as THREE from 'three'
|
|
|
21
21
|
import * as BufferGeometryUtils from 'three/examples/jsm/utils/BufferGeometryUtils.js'
|
|
22
22
|
import { RealObjectGroup } from '@hatiolab/things-scene'
|
|
23
23
|
|
|
24
|
+
// ── Material cache by bodyColor (shared across all Pallet3D instances) ──
|
|
25
|
+
// 동일 bodyColor 의 pallet 수십~수백 개 → GPU material 인스턴스 *1개로 통합*.
|
|
26
|
+
const woodBodyMaterials = new Map<string, THREE.MeshStandardMaterial>()
|
|
27
|
+
const woodStringerMaterials = new Map<string, THREE.MeshStandardMaterial>()
|
|
28
|
+
const plasticDeckMaterials = new Map<string, THREE.MeshStandardMaterial>()
|
|
29
|
+
const plasticFootMaterials = new Map<string, THREE.MeshStandardMaterial>()
|
|
30
|
+
|
|
31
|
+
function getWoodBodyMaterial(bodyColor: string): THREE.MeshStandardMaterial {
|
|
32
|
+
let m = woodBodyMaterials.get(bodyColor)
|
|
33
|
+
if (!m) {
|
|
34
|
+
m = new THREE.MeshStandardMaterial({ color: bodyColor, metalness: 0.0, roughness: 0.85 })
|
|
35
|
+
woodBodyMaterials.set(bodyColor, m)
|
|
36
|
+
}
|
|
37
|
+
return m
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function getWoodStringerMaterial(bodyColor: string): THREE.MeshStandardMaterial {
|
|
41
|
+
let m = woodStringerMaterials.get(bodyColor)
|
|
42
|
+
if (!m) {
|
|
43
|
+
const tint = new THREE.Color(bodyColor).multiplyScalar(0.85)
|
|
44
|
+
m = new THREE.MeshStandardMaterial({ color: tint, metalness: 0.0, roughness: 0.9 })
|
|
45
|
+
woodStringerMaterials.set(bodyColor, m)
|
|
46
|
+
}
|
|
47
|
+
return m
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function getPlasticDeckMaterial(bodyColor: string): THREE.MeshStandardMaterial {
|
|
51
|
+
let m = plasticDeckMaterials.get(bodyColor)
|
|
52
|
+
if (!m) {
|
|
53
|
+
m = new THREE.MeshStandardMaterial({ color: bodyColor, metalness: 0.1, roughness: 0.55 })
|
|
54
|
+
plasticDeckMaterials.set(bodyColor, m)
|
|
55
|
+
}
|
|
56
|
+
return m
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function getPlasticFootMaterial(bodyColor: string): THREE.MeshStandardMaterial {
|
|
60
|
+
let m = plasticFootMaterials.get(bodyColor)
|
|
61
|
+
if (!m) {
|
|
62
|
+
const tint = new THREE.Color(bodyColor).multiplyScalar(0.85)
|
|
63
|
+
m = new THREE.MeshStandardMaterial({ color: tint, metalness: 0.1, roughness: 0.65 })
|
|
64
|
+
plasticFootMaterials.set(bodyColor, m)
|
|
65
|
+
}
|
|
66
|
+
return m
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ── Geometry cache by size (shared across all Pallet3D instances) ──
|
|
70
|
+
// 동일 width × height × depth 의 pallet → merged geometry *1세트*만 GPU 에 업로드.
|
|
71
|
+
// Translation / merge 비용도 동일 size pallet 끼리 *1회*만 발생.
|
|
72
|
+
const woodGeoCache = new Map<string, { top: THREE.BufferGeometry; stringer: THREE.BufferGeometry; bottom: THREE.BufferGeometry }>()
|
|
73
|
+
const plasticGeoCache = new Map<string, { deck: THREE.BufferGeometry; feet: THREE.BufferGeometry; brace: THREE.BufferGeometry }>()
|
|
74
|
+
|
|
24
75
|
export class Pallet3D extends RealObjectGroup {
|
|
25
76
|
build() {
|
|
26
77
|
super.build()
|
|
@@ -38,28 +89,36 @@ export class Pallet3D extends RealObjectGroup {
|
|
|
38
89
|
|
|
39
90
|
/** Wood EUR-style: 7 top slats + 3 stringers + 5 bottom slats. */
|
|
40
91
|
private buildWood(width: number, height: number, depth: number, bodyColor: string) {
|
|
92
|
+
const { top, stringer, bottom } = this.getWoodGeometries(width, height, depth)
|
|
93
|
+
|
|
94
|
+
const woodMaterial = getWoodBodyMaterial(bodyColor)
|
|
95
|
+
const stringerMaterial = getWoodStringerMaterial(bodyColor)
|
|
96
|
+
|
|
97
|
+
const topSlatMesh = new THREE.Mesh(top, woodMaterial)
|
|
98
|
+
topSlatMesh.castShadow = true
|
|
99
|
+
topSlatMesh.receiveShadow = true
|
|
100
|
+
this.object3d.add(topSlatMesh)
|
|
101
|
+
|
|
102
|
+
const stringerMesh = new THREE.Mesh(stringer, stringerMaterial)
|
|
103
|
+
stringerMesh.castShadow = true
|
|
104
|
+
this.object3d.add(stringerMesh)
|
|
105
|
+
|
|
106
|
+
const botSlatMesh = new THREE.Mesh(bottom, woodMaterial)
|
|
107
|
+
botSlatMesh.receiveShadow = true
|
|
108
|
+
this.object3d.add(botSlatMesh)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private getWoodGeometries(width: number, height: number, depth: number) {
|
|
112
|
+
const key = `${width}|${height}|${depth}`
|
|
113
|
+
let cached = woodGeoCache.get(key)
|
|
114
|
+
if (cached) return cached
|
|
115
|
+
|
|
41
116
|
const baseY = -depth / 2
|
|
42
117
|
const slatThickness = depth * 0.15
|
|
43
118
|
const stringerThickness = depth * 0.45
|
|
44
119
|
const bottomSlatThickness = depth * 0.13
|
|
45
120
|
|
|
46
|
-
const woodMaterial = new THREE.MeshStandardMaterial({
|
|
47
|
-
color: bodyColor,
|
|
48
|
-
metalness: 0.0,
|
|
49
|
-
roughness: 0.85
|
|
50
|
-
})
|
|
51
|
-
const stringerColor = new THREE.Color(bodyColor).multiplyScalar(0.85)
|
|
52
|
-
const stringerMaterial = new THREE.MeshStandardMaterial({
|
|
53
|
-
color: stringerColor,
|
|
54
|
-
metalness: 0.0,
|
|
55
|
-
roughness: 0.9
|
|
56
|
-
})
|
|
57
|
-
|
|
58
|
-
// ── Top + bottom slats — same count, same z-positions, paired vertically ─
|
|
59
|
-
// EUR-pallet style: 5 boards on top, 5 below (under the same z ranges so
|
|
60
|
-
// they read as a single skeleton rather than two unrelated grids).
|
|
61
121
|
const slatCount = 5
|
|
62
|
-
const slatW = width
|
|
63
122
|
const slatD = (height * 0.92) / (slatCount + (slatCount - 1) * 0.4)
|
|
64
123
|
const gapD = slatD * 0.4
|
|
65
124
|
const totalSpan = slatCount * slatD + (slatCount - 1) * gapD
|
|
@@ -72,16 +131,12 @@ export class Pallet3D extends RealObjectGroup {
|
|
|
72
131
|
|
|
73
132
|
const topSlatGeos: THREE.BufferGeometry[] = []
|
|
74
133
|
for (const z of slatPositions) {
|
|
75
|
-
const slat = new THREE.BoxGeometry(
|
|
134
|
+
const slat = new THREE.BoxGeometry(width, slatThickness, slatD)
|
|
76
135
|
slat.translate(0, baseY + depth - slatThickness / 2, z)
|
|
77
136
|
topSlatGeos.push(slat)
|
|
78
137
|
}
|
|
79
|
-
const
|
|
80
|
-
topSlatMesh.castShadow = true
|
|
81
|
-
topSlatMesh.receiveShadow = true
|
|
82
|
-
this.object3d.add(topSlatMesh)
|
|
138
|
+
const top = BufferGeometryUtils.mergeGeometries(topSlatGeos)
|
|
83
139
|
|
|
84
|
-
// ── Stringers (3 perpendicular blocks between top and bottom decks) ─
|
|
85
140
|
const stringerCount = 3
|
|
86
141
|
const stringerW = width * 0.07
|
|
87
142
|
const stringerY = baseY + bottomSlatThickness + stringerThickness / 2
|
|
@@ -93,50 +148,59 @@ export class Pallet3D extends RealObjectGroup {
|
|
|
93
148
|
stringer.translate(x, stringerY, 0)
|
|
94
149
|
stringerGeos.push(stringer)
|
|
95
150
|
}
|
|
96
|
-
const
|
|
97
|
-
stringerMesh.castShadow = true
|
|
98
|
-
this.object3d.add(stringerMesh)
|
|
151
|
+
const stringer = BufferGeometryUtils.mergeGeometries(stringerGeos)
|
|
99
152
|
|
|
100
|
-
// ── Bottom slats — same z-positions as top so the deck reads as paired ─
|
|
101
153
|
const botSlatGeos: THREE.BufferGeometry[] = []
|
|
102
154
|
for (const z of slatPositions) {
|
|
103
155
|
const slat = new THREE.BoxGeometry(width, bottomSlatThickness, slatD)
|
|
104
156
|
slat.translate(0, baseY + bottomSlatThickness / 2, z)
|
|
105
157
|
botSlatGeos.push(slat)
|
|
106
158
|
}
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
159
|
+
const bottom = BufferGeometryUtils.mergeGeometries(botSlatGeos)
|
|
160
|
+
|
|
161
|
+
cached = { top, stringer, bottom }
|
|
162
|
+
woodGeoCache.set(key, cached)
|
|
163
|
+
return cached
|
|
110
164
|
}
|
|
111
165
|
|
|
112
166
|
/** Plastic molded: solid top deck + ribbed underside / feet. */
|
|
113
167
|
private buildPlastic(width: number, height: number, depth: number, bodyColor: string) {
|
|
168
|
+
const { deck, feet, brace } = this.getPlasticGeometries(width, height, depth)
|
|
169
|
+
|
|
170
|
+
const deckMaterial = getPlasticDeckMaterial(bodyColor)
|
|
171
|
+
const footMaterial = getPlasticFootMaterial(bodyColor)
|
|
172
|
+
|
|
114
173
|
const baseY = -depth / 2
|
|
115
174
|
const deckThickness = depth * 0.30
|
|
116
|
-
const footH = depth * 0.55
|
|
117
|
-
const footW = width * 0.12
|
|
118
175
|
|
|
119
|
-
const
|
|
120
|
-
color: bodyColor,
|
|
121
|
-
metalness: 0.1,
|
|
122
|
-
roughness: 0.55
|
|
123
|
-
})
|
|
124
|
-
const footColor = new THREE.Color(bodyColor).multiplyScalar(0.85)
|
|
125
|
-
const footMaterial = new THREE.MeshStandardMaterial({
|
|
126
|
-
color: footColor,
|
|
127
|
-
metalness: 0.1,
|
|
128
|
-
roughness: 0.65
|
|
129
|
-
})
|
|
130
|
-
|
|
131
|
-
// ── Solid top deck ───────────────────────────────────────────────
|
|
132
|
-
const deckGeo = new THREE.BoxGeometry(width * 0.98, deckThickness, height * 0.98)
|
|
133
|
-
const deckMesh = new THREE.Mesh(deckGeo, deckMaterial)
|
|
176
|
+
const deckMesh = new THREE.Mesh(deck, deckMaterial)
|
|
134
177
|
deckMesh.position.set(0, baseY + depth - deckThickness / 2, 0)
|
|
135
178
|
deckMesh.castShadow = true
|
|
136
179
|
deckMesh.receiveShadow = true
|
|
137
180
|
this.object3d.add(deckMesh)
|
|
138
181
|
|
|
139
|
-
|
|
182
|
+
const footMesh = new THREE.Mesh(feet, footMaterial)
|
|
183
|
+
footMesh.castShadow = true
|
|
184
|
+
this.object3d.add(footMesh)
|
|
185
|
+
|
|
186
|
+
const braceMesh = new THREE.Mesh(brace, footMaterial)
|
|
187
|
+
this.object3d.add(braceMesh)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
private getPlasticGeometries(width: number, height: number, depth: number) {
|
|
191
|
+
const key = `${width}|${height}|${depth}`
|
|
192
|
+
let cached = plasticGeoCache.get(key)
|
|
193
|
+
if (cached) return cached
|
|
194
|
+
|
|
195
|
+
const baseY = -depth / 2
|
|
196
|
+
const deckThickness = depth * 0.30
|
|
197
|
+
const footH = depth * 0.55
|
|
198
|
+
const footW = width * 0.12
|
|
199
|
+
|
|
200
|
+
// Deck geo — local space; mesh position applied at instantiation.
|
|
201
|
+
const deck = new THREE.BoxGeometry(width * 0.98, deckThickness, height * 0.98)
|
|
202
|
+
|
|
203
|
+
// 9 feet (3×3 grid) → merged geometry with translations baked in.
|
|
140
204
|
const footGeos: THREE.BufferGeometry[] = []
|
|
141
205
|
for (let i = -1; i <= 1; i++) {
|
|
142
206
|
for (let j = -1; j <= 1; j++) {
|
|
@@ -147,18 +211,21 @@ export class Pallet3D extends RealObjectGroup {
|
|
|
147
211
|
footGeos.push(foot)
|
|
148
212
|
}
|
|
149
213
|
}
|
|
150
|
-
const
|
|
151
|
-
footMesh.castShadow = true
|
|
152
|
-
this.object3d.add(footMesh)
|
|
214
|
+
const feet = BufferGeometryUtils.mergeGeometries(footGeos)
|
|
153
215
|
|
|
154
|
-
//
|
|
216
|
+
// 3 braces → merged into single mesh (이전에 mesh 3개 분리 = drawcall 3개).
|
|
155
217
|
const braceH = depth * 0.10
|
|
156
|
-
const
|
|
218
|
+
const braceGeos: THREE.BufferGeometry[] = []
|
|
157
219
|
for (const zSign of [-1, 0, 1]) {
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
|
|
220
|
+
const b = new THREE.BoxGeometry(width * 0.95, braceH, height * 0.04)
|
|
221
|
+
b.translate(0, baseY + footH - braceH / 2, zSign * height * 0.4)
|
|
222
|
+
braceGeos.push(b)
|
|
161
223
|
}
|
|
224
|
+
const brace = BufferGeometryUtils.mergeGeometries(braceGeos)
|
|
225
|
+
|
|
226
|
+
cached = { deck, feet, brace }
|
|
227
|
+
plasticGeoCache.set(key, cached)
|
|
228
|
+
return cached
|
|
162
229
|
}
|
|
163
230
|
|
|
164
231
|
updateDimension() {}
|
package/src/parcel-3d.ts
CHANGED
|
@@ -41,6 +41,34 @@ const PARCEL_LABEL_MATERIAL = new THREE.MeshStandardMaterial({
|
|
|
41
41
|
roughness: 0.4
|
|
42
42
|
})
|
|
43
43
|
|
|
44
|
+
// ── Geometry cache — 같은 (w,h,d) 인 parcel 들 BoxGeometry 공유. 수백 parcel 의
|
|
45
|
+
// GPU memory + setup cost 폭감.
|
|
46
|
+
const _BODY_GEO_CACHE = new Map<string, THREE.BoxGeometry>()
|
|
47
|
+
const _TAPE_GEO_CACHE = new Map<string, THREE.BoxGeometry>()
|
|
48
|
+
const _LABEL_GEO_CACHE = new Map<string, THREE.BoxGeometry>()
|
|
49
|
+
|
|
50
|
+
function _key3(a: number, b: number, c: number): string {
|
|
51
|
+
return `${a.toFixed(1)}-${b.toFixed(1)}-${c.toFixed(1)}`
|
|
52
|
+
}
|
|
53
|
+
function _getBodyGeo(w: number, d: number, h: number): THREE.BoxGeometry {
|
|
54
|
+
const k = _key3(w, d, h)
|
|
55
|
+
let g = _BODY_GEO_CACHE.get(k)
|
|
56
|
+
if (!g) { g = new THREE.BoxGeometry(w, d, h); _BODY_GEO_CACHE.set(k, g) }
|
|
57
|
+
return g
|
|
58
|
+
}
|
|
59
|
+
function _getTapeGeo(w: number, t: number, l: number): THREE.BoxGeometry {
|
|
60
|
+
const k = _key3(w, t, l)
|
|
61
|
+
let g = _TAPE_GEO_CACHE.get(k)
|
|
62
|
+
if (!g) { g = new THREE.BoxGeometry(w, t, l); _TAPE_GEO_CACHE.set(k, g) }
|
|
63
|
+
return g
|
|
64
|
+
}
|
|
65
|
+
function _getLabelGeo(w: number, t: number, h: number): THREE.BoxGeometry {
|
|
66
|
+
const k = _key3(w, t, h)
|
|
67
|
+
let g = _LABEL_GEO_CACHE.get(k)
|
|
68
|
+
if (!g) { g = new THREE.BoxGeometry(w, t, h); _LABEL_GEO_CACHE.set(k, g) }
|
|
69
|
+
return g
|
|
70
|
+
}
|
|
71
|
+
|
|
44
72
|
export class Parcel3D extends RealObjectGroup {
|
|
45
73
|
build() {
|
|
46
74
|
super.build()
|
|
@@ -49,8 +77,7 @@ export class Parcel3D extends RealObjectGroup {
|
|
|
49
77
|
const baseY = -depth / 2
|
|
50
78
|
|
|
51
79
|
// ── Main body ────────────────────────────────────────────────────
|
|
52
|
-
const
|
|
53
|
-
const bodyMesh = new THREE.Mesh(bodyGeo, PARCEL_BODY_MATERIAL)
|
|
80
|
+
const bodyMesh = new THREE.Mesh(_getBodyGeo(width, depth, height), PARCEL_BODY_MATERIAL)
|
|
54
81
|
bodyMesh.position.set(0, 0, 0)
|
|
55
82
|
bodyMesh.castShadow = true
|
|
56
83
|
bodyMesh.receiveShadow = true
|
|
@@ -60,24 +87,29 @@ export class Parcel3D extends RealObjectGroup {
|
|
|
60
87
|
const tapeW = Math.min(width, height) * 0.10
|
|
61
88
|
const tapeT = depth * 0.02
|
|
62
89
|
const tapeAlongLong = width >= height
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
90
|
+
const tapeMesh = new THREE.Mesh(
|
|
91
|
+
tapeAlongLong
|
|
92
|
+
? _getTapeGeo(width * 1.005, tapeT, tapeW)
|
|
93
|
+
: _getTapeGeo(tapeW, tapeT, height * 1.005),
|
|
94
|
+
PARCEL_TAPE_MATERIAL
|
|
95
|
+
)
|
|
67
96
|
tapeMesh.position.set(0, baseY + depth + tapeT / 2 - 0.01, 0)
|
|
97
|
+
// shadow 부담 줄임 — tape 는 얇아 shadow 시각 영향 미미
|
|
68
98
|
this.object3d.add(tapeMesh)
|
|
69
99
|
|
|
70
100
|
// ── Shipping label (small white rectangle on top) ────────────────
|
|
71
101
|
const labelW = Math.min(width, height) * 0.35
|
|
72
102
|
const labelH = labelW * 0.6
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
103
|
+
const labelMesh = new THREE.Mesh(
|
|
104
|
+
_getLabelGeo(labelW, depth * 0.005, labelH),
|
|
105
|
+
PARCEL_LABEL_MATERIAL
|
|
106
|
+
)
|
|
76
107
|
if (tapeAlongLong) {
|
|
77
108
|
labelMesh.position.set(width * 0.2, baseY + depth + depth * 0.0025, -height * 0.15)
|
|
78
109
|
} else {
|
|
79
110
|
labelMesh.position.set(width * 0.15, baseY + depth + depth * 0.0025, height * 0.2)
|
|
80
111
|
}
|
|
112
|
+
// shadow 부담 줄임 — label 도 얇아 shadow 시각 영향 미미
|
|
81
113
|
this.object3d.add(labelMesh)
|
|
82
114
|
}
|
|
83
115
|
|