@fmsim/machine 1.0.87 → 2.0.0-beta.2
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/dist/agv-line.js.map +1 -1
- package/dist/agv.js.map +1 -1
- package/dist/buffer.js.map +1 -1
- package/dist/carrier.js +1 -0
- package/dist/carrier.js.map +1 -1
- package/dist/conveyor-join.js.map +1 -1
- package/dist/conveyor.js +1 -1
- package/dist/conveyor.js.map +1 -1
- package/dist/crane-rail.js +53 -0
- package/dist/crane-rail.js.map +1 -0
- package/dist/crane.js.map +1 -1
- package/dist/equipment.js.map +1 -1
- package/dist/factories/agv-3d.js +121 -0
- package/dist/factories/agv-3d.js.map +1 -0
- package/dist/factories/conveyor-3d.js +258 -0
- package/dist/factories/conveyor-3d.js.map +1 -0
- package/dist/factories/crane-3d.js +150 -0
- package/dist/factories/crane-3d.js.map +1 -0
- package/dist/factories/crane-rail-3d.js +95 -0
- package/dist/factories/crane-rail-3d.js.map +1 -0
- package/dist/factories/machine-3d.js +288 -0
- package/dist/factories/machine-3d.js.map +1 -0
- package/dist/factories/oht-3d.js +151 -0
- package/dist/factories/oht-3d.js.map +1 -0
- package/dist/factories/shuttle-3d.js +136 -0
- package/dist/factories/shuttle-3d.js.map +1 -0
- package/dist/index.js +38 -0
- package/dist/index.js.map +1 -1
- package/dist/mcs-carrier-holder.js +6 -1
- package/dist/mcs-carrier-holder.js.map +1 -1
- package/dist/mcs-machine.js.map +1 -1
- package/dist/mcs-unit.js.map +1 -1
- package/dist/mcs-vehicle.js +2 -1
- package/dist/mcs-vehicle.js.map +1 -1
- package/dist/oht-line.js.map +1 -1
- package/dist/oht.js.map +1 -1
- package/dist/port-flow.js.map +1 -1
- package/dist/port.js.map +1 -1
- package/dist/scene/root-container-override.js.map +1 -1
- package/dist/shelf.js.map +1 -1
- package/dist/shuttle.js.map +1 -1
- package/dist/stocker-abnormal-bar.js +180 -0
- package/dist/stocker-abnormal-bar.js.map +1 -0
- package/dist/stocker-capacity-bar.js.map +1 -1
- package/dist/stocker-carrier-bar.js +194 -0
- package/dist/stocker-carrier-bar.js.map +1 -0
- package/dist/stocker.js +1 -1
- package/dist/stocker.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/zone-capacity-bar.js.map +1 -1
- package/package.json +6 -6
- package/src/agv-line.ts +1 -1
- package/src/agv.ts +3 -3
- package/src/buffer.ts +1 -1
- package/src/carrier.ts +8 -8
- package/src/conveyor-join.ts +1 -1
- package/src/conveyor.ts +2 -2
- package/src/crane-rail.ts +58 -0
- package/src/crane.ts +1 -1
- package/src/equipment.ts +1 -1
- package/src/factories/agv-3d.ts +135 -0
- package/src/factories/conveyor-3d.ts +311 -0
- package/src/factories/crane-3d.ts +176 -0
- package/src/factories/crane-rail-3d.ts +114 -0
- package/src/factories/machine-3d.ts +361 -0
- package/src/factories/oht-3d.ts +172 -0
- package/src/factories/shuttle-3d.ts +159 -0
- package/src/index.ts +40 -0
- package/src/mcs-carrier-holder.ts +8 -2
- package/src/mcs-machine.ts +1 -1
- package/src/mcs-unit.ts +1 -1
- package/src/mcs-vehicle.ts +3 -3
- package/src/oht-line.ts +1 -1
- package/src/oht.ts +1 -1
- package/src/port-flow.ts +1 -1
- package/src/port.ts +1 -1
- package/src/scene/root-container-override.ts +2 -2
- package/src/shelf.ts +1 -1
- package/src/shuttle.ts +1 -1
- package/src/stocker-abnormal-bar.ts +242 -0
- package/src/stocker-capacity-bar.ts +3 -3
- package/src/stocker-carrier-bar.ts +257 -0
- package/src/stocker.ts +3 -3
- package/src/zone-capacity-bar.ts +2 -2
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Conveyor 3D Factory
|
|
3
|
+
*
|
|
4
|
+
* 'conveyor' type (default): side rails + cylindrical rollers + 4 legs + cross-bracing
|
|
5
|
+
* 'belt' type: side rails + two end drums + belt wrapping + 4 legs + cross-bracing
|
|
6
|
+
* 'rail' type: side rails + two parallel rails + 4 legs + cross-bracing
|
|
7
|
+
*
|
|
8
|
+
* Based on operato-scene conveyor-3d.ts, adapted for fmsim:
|
|
9
|
+
* - conveyorType is string ('', 'conveyor', 'belt', 'rail') instead of numeric
|
|
10
|
+
* - statusColor from MCSStatusMixin instead of value-based lookup
|
|
11
|
+
* - rollWidth auto-calculated: Math.max(10, Math.min(width, height) / 2)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import * as THREE from 'three'
|
|
15
|
+
import * as BufferGeometryUtils from 'three/examples/jsm/utils/BufferGeometryUtils.js'
|
|
16
|
+
import { RealObjectGroup, registerRealObjectFactory } from '@hatiolab/things-scene'
|
|
17
|
+
import type { Component } from '@hatiolab/things-scene'
|
|
18
|
+
import { TRANSPORT_SURFACE_HEIGHT } from './machine-3d.js'
|
|
19
|
+
|
|
20
|
+
const FRAME_COLOR = 0x888899
|
|
21
|
+
const ROLLER_COLOR = 0xaaaabc
|
|
22
|
+
const DEFAULT_COLOR = 0x999999
|
|
23
|
+
|
|
24
|
+
export class Conveyor3D extends RealObjectGroup {
|
|
25
|
+
get effectiveDepth(): number {
|
|
26
|
+
const { depth } = this.component.state
|
|
27
|
+
return depth || TRANSPORT_SURFACE_HEIGHT
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// group origin = 바닥 (y=0). MCSRealObject3D와 동일 컨벤션.
|
|
31
|
+
// 기하는 build()에서 y=0(바닥)~y=depth(상단) 범위로 배치.
|
|
32
|
+
protected get syncZPosOffset(): number {
|
|
33
|
+
return 0
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
private resolveColor(): number {
|
|
37
|
+
const comp = this.component as any
|
|
38
|
+
|
|
39
|
+
// 1) MCSStatusMixin.statusColor (legend 기반 상태 색상)
|
|
40
|
+
// '#F0F0F0'은 상태 미설정 시 기본 fallback — 무시하고 defaultColor 사용
|
|
41
|
+
try {
|
|
42
|
+
const sc = comp.statusColor
|
|
43
|
+
if (typeof sc === 'string' && sc && sc.toUpperCase() !== '#F0F0F0') return new THREE.Color(sc).getHex()
|
|
44
|
+
} catch {
|
|
45
|
+
/* statusColor 미구현 시 무시 */
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 2) state.fillStyle fallback
|
|
49
|
+
const { fillStyle } = this.component.state
|
|
50
|
+
if (fillStyle && typeof fillStyle === 'string') {
|
|
51
|
+
try {
|
|
52
|
+
return new THREE.Color(fillStyle).getHex()
|
|
53
|
+
} catch {
|
|
54
|
+
/* 파싱 실패 시 무시 */
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return DEFAULT_COLOR
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
build() {
|
|
62
|
+
super.build()
|
|
63
|
+
|
|
64
|
+
const { width, height, conveyorType } = this.component.state
|
|
65
|
+
const depth = this.effectiveDepth
|
|
66
|
+
if (!width || !height) return
|
|
67
|
+
|
|
68
|
+
// 2D와 동일 로직으로 rollWidth 산출
|
|
69
|
+
const rollWidth = Math.max(10, Math.min(width, height) / 2)
|
|
70
|
+
const rollerDiameter = Math.max(rollWidth, 2)
|
|
71
|
+
const frameH = Math.min(rollerDiameter, depth)
|
|
72
|
+
const legH = Math.max(depth - frameH, 0)
|
|
73
|
+
const railW = Math.max(height * 0.06, 2)
|
|
74
|
+
const legThickness = Math.max(railW * 0.8, 2)
|
|
75
|
+
|
|
76
|
+
const frameMaterial = new THREE.MeshStandardMaterial({
|
|
77
|
+
color: FRAME_COLOR,
|
|
78
|
+
metalness: 0.85,
|
|
79
|
+
roughness: 0.35,
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
// --- Frame: side rails + legs + cross-bracing ---
|
|
83
|
+
// Y 좌표: y=0(바닥) ~ y=depth(상단). 레일 상단이 depth에 위치.
|
|
84
|
+
const frameGeometries: THREE.BufferGeometry[] = []
|
|
85
|
+
const railY = depth - frameH / 2
|
|
86
|
+
|
|
87
|
+
// Side rails (along X)
|
|
88
|
+
for (const zSign of [-1, 1]) {
|
|
89
|
+
const rail = new THREE.BoxGeometry(width, frameH, railW)
|
|
90
|
+
rail.translate(0, railY, zSign * (height / 2 - railW / 2))
|
|
91
|
+
frameGeometries.push(rail)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// 4 legs at corners
|
|
95
|
+
if (legH > 0) {
|
|
96
|
+
const legTopY = depth - frameH
|
|
97
|
+
const legCenterY = legTopY - legH / 2
|
|
98
|
+
|
|
99
|
+
for (const xSign of [-1, 1]) {
|
|
100
|
+
for (const zSign of [-1, 1]) {
|
|
101
|
+
const lx = xSign * (width / 2 - legThickness / 2)
|
|
102
|
+
const lz = zSign * (height / 2 - railW / 2)
|
|
103
|
+
const leg = new THREE.BoxGeometry(legThickness, legH, legThickness)
|
|
104
|
+
leg.translate(lx, legCenterY, lz)
|
|
105
|
+
frameGeometries.push(leg)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Cross-bracing: 앞뒤 다리 연결 (Z 방향)
|
|
110
|
+
const braceH = legThickness * 0.6
|
|
111
|
+
const braceW = legThickness * 0.6
|
|
112
|
+
const braceLen = height - railW
|
|
113
|
+
const braceY = legTopY - legH * 0.35
|
|
114
|
+
|
|
115
|
+
for (const xSign of [-1, 1]) {
|
|
116
|
+
const brace = new THREE.BoxGeometry(braceW, braceH, braceLen)
|
|
117
|
+
brace.translate(xSign * (width / 2 - legThickness / 2), braceY, 0)
|
|
118
|
+
frameGeometries.push(brace)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Cross-bracing: 좌우 다리 연결 (X 방향)
|
|
122
|
+
const braceLenX = width - legThickness
|
|
123
|
+
for (const zSign of [-1, 1]) {
|
|
124
|
+
const brace = new THREE.BoxGeometry(braceLenX, braceH, braceW)
|
|
125
|
+
brace.translate(0, braceY, zSign * (height / 2 - railW / 2))
|
|
126
|
+
frameGeometries.push(brace)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const frameMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(frameGeometries), frameMaterial)
|
|
131
|
+
frameMesh.castShadow = true
|
|
132
|
+
frameMesh.receiveShadow = true
|
|
133
|
+
this.object3d.add(frameMesh)
|
|
134
|
+
|
|
135
|
+
// --- Rollers, Belt, or Rails ---
|
|
136
|
+
if (conveyorType === 'rail') {
|
|
137
|
+
this.buildRails(width, height, depth, frameH, railW)
|
|
138
|
+
} else if (conveyorType === 'belt') {
|
|
139
|
+
this.buildBelt(width, height, depth, frameH, railW)
|
|
140
|
+
} else {
|
|
141
|
+
this.buildRollers(width, height, depth, frameH, railW, rollWidth)
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
private buildRollers(
|
|
146
|
+
width: number,
|
|
147
|
+
height: number,
|
|
148
|
+
depth: number,
|
|
149
|
+
frameH: number,
|
|
150
|
+
railW: number,
|
|
151
|
+
rollWidth: number
|
|
152
|
+
) {
|
|
153
|
+
const rollerRadius = Math.max(rollWidth / 8, 1)
|
|
154
|
+
const rollerLength = height - railW * 2 - 0.5
|
|
155
|
+
const diameter = rollerRadius * 2
|
|
156
|
+
// 롤러 간 최소 간격 (z-fighting 방지용 미세 갭만)
|
|
157
|
+
const step = diameter * 1.05
|
|
158
|
+
// 롤러 상단 = 이송 표면 (y=depth)
|
|
159
|
+
const rollerY = depth - rollerRadius
|
|
160
|
+
const color = this.resolveColor()
|
|
161
|
+
|
|
162
|
+
const rollerGeometries: THREE.BufferGeometry[] = []
|
|
163
|
+
const count = Math.max(1, Math.floor(width / step))
|
|
164
|
+
const totalSpan = (count - 1) * step
|
|
165
|
+
const startX = -totalSpan / 2
|
|
166
|
+
|
|
167
|
+
for (let i = 0; i < count; i++) {
|
|
168
|
+
const x = startX + i * step
|
|
169
|
+
const roller = new THREE.CylinderGeometry(rollerRadius, rollerRadius, rollerLength, 16)
|
|
170
|
+
roller.rotateX(Math.PI / 2)
|
|
171
|
+
roller.translate(x, rollerY, 0)
|
|
172
|
+
rollerGeometries.push(roller)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (rollerGeometries.length > 0) {
|
|
176
|
+
const rollerMesh = new THREE.Mesh(
|
|
177
|
+
BufferGeometryUtils.mergeGeometries(rollerGeometries),
|
|
178
|
+
new THREE.MeshStandardMaterial({ color, metalness: 0.9, roughness: 0.2 })
|
|
179
|
+
)
|
|
180
|
+
rollerMesh.castShadow = true
|
|
181
|
+
this.object3d.add(rollerMesh)
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
private buildBelt(width: number, height: number, depth: number, frameH: number, railW: number) {
|
|
186
|
+
const drumRadius = frameH * 0.38
|
|
187
|
+
const drumLength = height - railW * 2
|
|
188
|
+
const beltY = depth - drumRadius
|
|
189
|
+
const color = this.resolveColor()
|
|
190
|
+
|
|
191
|
+
// Two end drums
|
|
192
|
+
const drumGeometries: THREE.BufferGeometry[] = []
|
|
193
|
+
const drumInset = drumRadius * 1.5
|
|
194
|
+
const leftDrumX = -width / 2 + drumInset
|
|
195
|
+
const rightDrumX = width / 2 - drumInset
|
|
196
|
+
|
|
197
|
+
for (const dx of [leftDrumX, rightDrumX]) {
|
|
198
|
+
const drum = new THREE.CylinderGeometry(drumRadius, drumRadius, drumLength, 16)
|
|
199
|
+
drum.rotateX(Math.PI / 2)
|
|
200
|
+
drum.translate(dx, beltY, 0)
|
|
201
|
+
drumGeometries.push(drum)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const drumMesh = new THREE.Mesh(
|
|
205
|
+
BufferGeometryUtils.mergeGeometries(drumGeometries),
|
|
206
|
+
new THREE.MeshStandardMaterial({ color: ROLLER_COLOR, metalness: 0.9, roughness: 0.2 })
|
|
207
|
+
)
|
|
208
|
+
drumMesh.castShadow = true
|
|
209
|
+
this.object3d.add(drumMesh)
|
|
210
|
+
|
|
211
|
+
// Belt surface
|
|
212
|
+
const beltThickness = drumRadius * 0.12
|
|
213
|
+
const beltMaterial = new THREE.MeshStandardMaterial({
|
|
214
|
+
color: color,
|
|
215
|
+
metalness: 0.0,
|
|
216
|
+
roughness: 0.9,
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
// Flat belt top surface spanning between drums
|
|
220
|
+
const flatLen = rightDrumX - leftDrumX
|
|
221
|
+
const flatGeo = new THREE.BoxGeometry(flatLen, beltThickness, drumLength)
|
|
222
|
+
const flatMesh = new THREE.Mesh(flatGeo, beltMaterial)
|
|
223
|
+
flatMesh.position.set(0, beltY + drumRadius, 0)
|
|
224
|
+
flatMesh.castShadow = true
|
|
225
|
+
this.object3d.add(flatMesh)
|
|
226
|
+
|
|
227
|
+
// Belt wrap around drums (segmented half-cylinder)
|
|
228
|
+
const wrapSegments = 12
|
|
229
|
+
for (const [dx, angleStart] of [
|
|
230
|
+
[leftDrumX, Math.PI / 2],
|
|
231
|
+
[rightDrumX, -Math.PI / 2],
|
|
232
|
+
] as [number, number][]) {
|
|
233
|
+
const wrapGeometries: THREE.BufferGeometry[] = []
|
|
234
|
+
for (let i = 0; i < wrapSegments; i++) {
|
|
235
|
+
const a0 = angleStart + (Math.PI * i) / wrapSegments
|
|
236
|
+
const a1 = angleStart + (Math.PI * (i + 1)) / wrapSegments
|
|
237
|
+
const r = drumRadius + beltThickness / 2
|
|
238
|
+
|
|
239
|
+
const x0 = dx + Math.cos(a0) * r
|
|
240
|
+
const y0 = beltY + Math.sin(a0) * r
|
|
241
|
+
const x1 = dx + Math.cos(a1) * r
|
|
242
|
+
const y1 = beltY + Math.sin(a1) * r
|
|
243
|
+
|
|
244
|
+
const segLen = Math.sqrt((x1 - x0) ** 2 + (y1 - y0) ** 2) * 1.05
|
|
245
|
+
const segAngle = Math.atan2(y1 - y0, x1 - x0)
|
|
246
|
+
|
|
247
|
+
const seg = new THREE.BoxGeometry(segLen, beltThickness, drumLength)
|
|
248
|
+
seg.rotateZ(segAngle)
|
|
249
|
+
seg.translate((x0 + x1) / 2, (y0 + y1) / 2, 0)
|
|
250
|
+
wrapGeometries.push(seg)
|
|
251
|
+
}
|
|
252
|
+
const wrapMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(wrapGeometries), beltMaterial)
|
|
253
|
+
wrapMesh.castShadow = true
|
|
254
|
+
this.object3d.add(wrapMesh)
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Flat belt bottom (return path)
|
|
258
|
+
const bottomGeo = new THREE.BoxGeometry(flatLen, beltThickness, drumLength * 0.85)
|
|
259
|
+
const bottomMesh = new THREE.Mesh(bottomGeo, beltMaterial)
|
|
260
|
+
bottomMesh.position.set(0, beltY - drumRadius, 0)
|
|
261
|
+
this.object3d.add(bottomMesh)
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
private buildRails(width: number, height: number, depth: number, frameH: number, railW: number) {
|
|
265
|
+
const color = this.resolveColor()
|
|
266
|
+
const railHeight = frameH * 0.3
|
|
267
|
+
const railY = depth - railHeight / 2
|
|
268
|
+
const innerWidth = height - railW * 2
|
|
269
|
+
// 두 레일 간격: 내부 폭의 60%
|
|
270
|
+
const railSpacing = innerWidth * 0.6
|
|
271
|
+
|
|
272
|
+
const railMaterial = new THREE.MeshStandardMaterial({
|
|
273
|
+
color,
|
|
274
|
+
metalness: 0.7,
|
|
275
|
+
roughness: 0.3,
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
const railGeometries: THREE.BufferGeometry[] = []
|
|
279
|
+
|
|
280
|
+
// Two parallel rails running along X axis
|
|
281
|
+
for (const zSign of [-1, 1]) {
|
|
282
|
+
const rail = new THREE.BoxGeometry(width - railW * 2, railHeight, railW * 0.8)
|
|
283
|
+
rail.translate(0, railY, zSign * (railSpacing / 2))
|
|
284
|
+
railGeometries.push(rail)
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const railMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(railGeometries), railMaterial)
|
|
288
|
+
railMesh.castShadow = true
|
|
289
|
+
this.object3d.add(railMesh)
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
updateDimension() {}
|
|
293
|
+
updateAlpha() {}
|
|
294
|
+
|
|
295
|
+
onchange(after: Record<string, unknown>, before: Record<string, unknown>) {
|
|
296
|
+
if (
|
|
297
|
+
'conveyorType' in after ||
|
|
298
|
+
'width' in after ||
|
|
299
|
+
'height' in after ||
|
|
300
|
+
'depth' in after ||
|
|
301
|
+
'fillStyle' in after ||
|
|
302
|
+
'strokeStyle' in after
|
|
303
|
+
) {
|
|
304
|
+
this.update()
|
|
305
|
+
return
|
|
306
|
+
}
|
|
307
|
+
super.onchange(after, before)
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
registerRealObjectFactory('Conveyor', (component: Component) => new Conveyor3D(component))
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Crane (Stacker Crane) 3D Factory
|
|
3
|
+
*
|
|
4
|
+
* 스토커 내부의 스태커 크레인 — CraneRail 위를 이동하는 차체.
|
|
5
|
+
* 레일 인프라는 CraneRail이 담당하므로 크레인은 이동체만 표현.
|
|
6
|
+
*
|
|
7
|
+
* 구조 (바닥 원점, y=0~depth):
|
|
8
|
+
* - 하단 바퀴/클램프 (4%, 바닥 레일에 물리는 부분)
|
|
9
|
+
* - 수직 마스트 (좌우 기둥 88%, statusColor 적용)
|
|
10
|
+
* - 캐리지 포크 (마스트 40% 높이, 양쪽 포크 암)
|
|
11
|
+
* - 상단 클램프 (4%, 천장 레일에 물리는 부분)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import * as THREE from 'three'
|
|
15
|
+
import * as BufferGeometryUtils from 'three/examples/jsm/utils/BufferGeometryUtils.js'
|
|
16
|
+
import { RealObjectGroup, registerRealObjectFactory } from '@hatiolab/things-scene'
|
|
17
|
+
import type { Component } from '@hatiolab/things-scene'
|
|
18
|
+
import { TRANSPORT_SURFACE_HEIGHT } from './machine-3d.js'
|
|
19
|
+
|
|
20
|
+
const CLAMP_COLOR = 0x444444
|
|
21
|
+
const FORK_COLOR = 0x999999
|
|
22
|
+
const DEFAULT_BODY_COLOR = 0xcc8844
|
|
23
|
+
|
|
24
|
+
export class Crane3D extends RealObjectGroup {
|
|
25
|
+
get effectiveDepth(): number {
|
|
26
|
+
const { depth } = this.component.state
|
|
27
|
+
return depth || 5 * TRANSPORT_SURFACE_HEIGHT - 2
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
protected get syncZPosOffset(): number {
|
|
31
|
+
return 0
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
private resolveColor(): number {
|
|
35
|
+
const comp = this.component as any
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
const sc = comp.statusColor
|
|
39
|
+
if (typeof sc === 'string' && sc && sc.toUpperCase() !== '#F0F0F0') return new THREE.Color(sc).getHex()
|
|
40
|
+
} catch {
|
|
41
|
+
/* statusColor 미구현 시 무시 */
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const { fillStyle } = this.component.state
|
|
45
|
+
if (fillStyle && typeof fillStyle === 'string') {
|
|
46
|
+
try {
|
|
47
|
+
return new THREE.Color(fillStyle).getHex()
|
|
48
|
+
} catch {
|
|
49
|
+
/* 파싱 실패 시 무시 */
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return DEFAULT_BODY_COLOR
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
build() {
|
|
57
|
+
super.build()
|
|
58
|
+
|
|
59
|
+
const { width = 10, height = 10 } = this.component.state
|
|
60
|
+
const w = Math.abs(width)
|
|
61
|
+
const h = Math.abs(height)
|
|
62
|
+
const d = this.effectiveDepth
|
|
63
|
+
if (!w || !h) return
|
|
64
|
+
|
|
65
|
+
// 높이 배분: 하단 클램프 4%, 마스트 92%, 상단 클램프 4%
|
|
66
|
+
const clampH = d * 0.04
|
|
67
|
+
const mastH = d * 0.92
|
|
68
|
+
|
|
69
|
+
const pillarThick = Math.min(w, h) * 0.12
|
|
70
|
+
|
|
71
|
+
const clampMat = new THREE.MeshStandardMaterial({
|
|
72
|
+
color: CLAMP_COLOR,
|
|
73
|
+
roughness: 0.5,
|
|
74
|
+
metalness: 0.5,
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
// ── 1. 하단 클램프 (바닥 레일에 물리는 바퀴부) ──
|
|
78
|
+
const clampW = w * 0.7
|
|
79
|
+
const clampD = h * 0.5
|
|
80
|
+
const bottomClampGeo = new THREE.BoxGeometry(clampW, clampH, clampD)
|
|
81
|
+
const bottomClamp = new THREE.Mesh(bottomClampGeo, clampMat)
|
|
82
|
+
bottomClamp.position.y = clampH / 2
|
|
83
|
+
bottomClamp.castShadow = true
|
|
84
|
+
this.object3d.add(bottomClamp)
|
|
85
|
+
|
|
86
|
+
// ── 2. 수직 마스트 (좌우 기둥 + 상·하 가로 연결 바) ──
|
|
87
|
+
const mastBaseY = clampH
|
|
88
|
+
const bodyColor = this.resolveColor()
|
|
89
|
+
const mastMat = new THREE.MeshStandardMaterial({
|
|
90
|
+
color: bodyColor,
|
|
91
|
+
roughness: 0.3,
|
|
92
|
+
metalness: 0.6,
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
const mastGeometries: THREE.BufferGeometry[] = []
|
|
96
|
+
|
|
97
|
+
// 좌우 수직 기둥
|
|
98
|
+
for (const xSign of [-1, 1]) {
|
|
99
|
+
const pillar = new THREE.BoxGeometry(pillarThick, mastH, pillarThick)
|
|
100
|
+
pillar.translate(xSign * (w / 2 - pillarThick / 2), mastBaseY + mastH / 2, 0)
|
|
101
|
+
mastGeometries.push(pillar)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// 하단 가로 연결 바
|
|
105
|
+
const crossW = w - pillarThick
|
|
106
|
+
const crossH = pillarThick * 0.5
|
|
107
|
+
const lowerCross = new THREE.BoxGeometry(crossW, crossH, pillarThick)
|
|
108
|
+
lowerCross.translate(0, mastBaseY + crossH / 2, 0)
|
|
109
|
+
mastGeometries.push(lowerCross)
|
|
110
|
+
|
|
111
|
+
// 상단 가로 연결 바
|
|
112
|
+
const upperCross = new THREE.BoxGeometry(crossW, crossH, pillarThick)
|
|
113
|
+
upperCross.translate(0, mastBaseY + mastH - crossH / 2, 0)
|
|
114
|
+
mastGeometries.push(upperCross)
|
|
115
|
+
|
|
116
|
+
const mastMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(mastGeometries), mastMat)
|
|
117
|
+
mastMesh.castShadow = true
|
|
118
|
+
this.object3d.add(mastMesh)
|
|
119
|
+
|
|
120
|
+
// ── 3. 캐리지 포크 (마스트 40% 높이, 양쪽으로 뻗는 포크 암) ──
|
|
121
|
+
const forkY = mastBaseY + mastH * 0.4
|
|
122
|
+
const forkMat = new THREE.MeshStandardMaterial({
|
|
123
|
+
color: FORK_COLOR,
|
|
124
|
+
roughness: 0.4,
|
|
125
|
+
metalness: 0.5,
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
const forkGeometries: THREE.BufferGeometry[] = []
|
|
129
|
+
|
|
130
|
+
// 캐리지 중심 플레이트 (마스트에 부착, 양쪽 포크의 베이스)
|
|
131
|
+
const plateW = w * 0.9
|
|
132
|
+
const plateH = d * 0.08
|
|
133
|
+
const plateD = pillarThick * 0.8
|
|
134
|
+
const centerPlate = new THREE.BoxGeometry(plateW, plateH, plateD)
|
|
135
|
+
centerPlate.translate(0, forkY + plateH / 2, 0)
|
|
136
|
+
forkGeometries.push(centerPlate)
|
|
137
|
+
|
|
138
|
+
// 양쪽 포크 암 (좌/우 선반 방향으로 뻗음)
|
|
139
|
+
const armW = pillarThick * 0.6
|
|
140
|
+
const armH = plateH * 0.4
|
|
141
|
+
const armD = h * 0.6
|
|
142
|
+
const armSpacing = w * 0.5
|
|
143
|
+
|
|
144
|
+
for (const zSign of [-1, 1]) {
|
|
145
|
+
for (const xSign of [-1, 1]) {
|
|
146
|
+
const arm = new THREE.BoxGeometry(armW, armH, armD)
|
|
147
|
+
arm.translate(xSign * (armSpacing / 2), forkY + armH / 2, zSign * (plateD / 2 + armD / 2))
|
|
148
|
+
forkGeometries.push(arm)
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const forkMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(forkGeometries), forkMat)
|
|
153
|
+
forkMesh.castShadow = true
|
|
154
|
+
this.object3d.add(forkMesh)
|
|
155
|
+
|
|
156
|
+
// ── 4. 상단 클램프 (천장 레일에 물리는 가이드부) ──
|
|
157
|
+
const topClampGeo = new THREE.BoxGeometry(clampW, clampH, clampD)
|
|
158
|
+
const topClamp = new THREE.Mesh(topClampGeo, clampMat)
|
|
159
|
+
topClamp.position.y = mastBaseY + mastH + clampH / 2
|
|
160
|
+
topClamp.castShadow = true
|
|
161
|
+
this.object3d.add(topClamp)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
updateDimension() {}
|
|
165
|
+
updateAlpha() {}
|
|
166
|
+
|
|
167
|
+
onchange(after: Record<string, unknown>, before: Record<string, unknown>) {
|
|
168
|
+
if ('width' in after || 'height' in after || 'depth' in after || 'fillStyle' in after || 'strokeStyle' in after) {
|
|
169
|
+
this.update()
|
|
170
|
+
return
|
|
171
|
+
}
|
|
172
|
+
super.onchange(after, before)
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
registerRealObjectFactory('Crane', (component: Component) => new Crane3D(component))
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CraneRail 3D Factory
|
|
3
|
+
*
|
|
4
|
+
* 스토커 내부 스태커 크레인의 주행 레일.
|
|
5
|
+
* 바닥 레일 + 천장 가이드 레일로 크레인의 2축 이동(수평 주행 + 수직 승강)을 지원.
|
|
6
|
+
*
|
|
7
|
+
* 구조 (바닥 원점, y=0~depth):
|
|
8
|
+
* - 바닥 레일: 2개의 평행 트랙 (크레인 바퀴 주행)
|
|
9
|
+
* - 천장 가이드 레일: 2개의 평행 트랙 (마스트 상단 안내)
|
|
10
|
+
* - 수직 지지대: 양 끝단 기둥 (바닥~천장 연결)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import * as THREE from 'three'
|
|
14
|
+
import * as BufferGeometryUtils from 'three/examples/jsm/utils/BufferGeometryUtils.js'
|
|
15
|
+
import { RealObjectGroup, registerRealObjectFactory } from '@hatiolab/things-scene'
|
|
16
|
+
import type { Component } from '@hatiolab/things-scene'
|
|
17
|
+
import { TRANSPORT_SURFACE_HEIGHT } from './machine-3d.js'
|
|
18
|
+
|
|
19
|
+
const RAIL_COLOR = 0x666677
|
|
20
|
+
const SUPPORT_COLOR = 0x555566
|
|
21
|
+
|
|
22
|
+
/** 크레인 레일 기본 높이: 스토커와 동일 */
|
|
23
|
+
const DEFAULT_RAIL_DEPTH = 5 * TRANSPORT_SURFACE_HEIGHT
|
|
24
|
+
|
|
25
|
+
export class CraneRail3D extends RealObjectGroup {
|
|
26
|
+
get effectiveDepth(): number {
|
|
27
|
+
const { depth } = this.component.state
|
|
28
|
+
return depth || DEFAULT_RAIL_DEPTH
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
protected get syncZPosOffset(): number {
|
|
32
|
+
return 0
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
build() {
|
|
36
|
+
super.build()
|
|
37
|
+
|
|
38
|
+
const { width = 10, height = 10 } = this.component.state
|
|
39
|
+
const w = Math.abs(width)
|
|
40
|
+
const h = Math.abs(height)
|
|
41
|
+
const d = this.effectiveDepth
|
|
42
|
+
if (!w || !h) return
|
|
43
|
+
|
|
44
|
+
const railH = Math.max(d * 0.02, 2) // 레일 높이 (얇은 트랙)
|
|
45
|
+
const railW = Math.max(h * 0.06, 2) // 레일 폭
|
|
46
|
+
const supportThick = Math.max(Math.min(w, h) * 0.05, 2)
|
|
47
|
+
|
|
48
|
+
const railMat = new THREE.MeshStandardMaterial({
|
|
49
|
+
color: RAIL_COLOR,
|
|
50
|
+
roughness: 0.3,
|
|
51
|
+
metalness: 0.7,
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
const supportMat = new THREE.MeshStandardMaterial({
|
|
55
|
+
color: SUPPORT_COLOR,
|
|
56
|
+
roughness: 0.5,
|
|
57
|
+
metalness: 0.5,
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
// ── 바닥 레일 (2개 평행 트랙) ──
|
|
61
|
+
const bottomGeometries: THREE.BufferGeometry[] = []
|
|
62
|
+
for (const zSign of [-1, 1]) {
|
|
63
|
+
const rail = new THREE.BoxGeometry(w, railH, railW)
|
|
64
|
+
rail.translate(0, railH / 2, zSign * (h / 2 - railW / 2))
|
|
65
|
+
bottomGeometries.push(rail)
|
|
66
|
+
}
|
|
67
|
+
const bottomMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(bottomGeometries), railMat)
|
|
68
|
+
bottomMesh.castShadow = true
|
|
69
|
+
bottomMesh.receiveShadow = true
|
|
70
|
+
this.object3d.add(bottomMesh)
|
|
71
|
+
|
|
72
|
+
// ── 천장 가이드 레일 (2개 평행 트랙) ──
|
|
73
|
+
const topGeometries: THREE.BufferGeometry[] = []
|
|
74
|
+
for (const zSign of [-1, 1]) {
|
|
75
|
+
const rail = new THREE.BoxGeometry(w, railH, railW)
|
|
76
|
+
rail.translate(0, d - railH / 2, zSign * (h / 2 - railW / 2))
|
|
77
|
+
topGeometries.push(rail)
|
|
78
|
+
}
|
|
79
|
+
const topMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(topGeometries), railMat)
|
|
80
|
+
topMesh.castShadow = true
|
|
81
|
+
this.object3d.add(topMesh)
|
|
82
|
+
|
|
83
|
+
// ── 양 끝단 수직 지지대 (바닥~천장 연결) ──
|
|
84
|
+
const supportH = d - railH * 2
|
|
85
|
+
const supportGeometries: THREE.BufferGeometry[] = []
|
|
86
|
+
for (const xSign of [-1, 1]) {
|
|
87
|
+
for (const zSign of [-1, 1]) {
|
|
88
|
+
const support = new THREE.BoxGeometry(supportThick, supportH, supportThick)
|
|
89
|
+
support.translate(
|
|
90
|
+
xSign * (w / 2 - supportThick / 2),
|
|
91
|
+
railH + supportH / 2,
|
|
92
|
+
zSign * (h / 2 - railW / 2)
|
|
93
|
+
)
|
|
94
|
+
supportGeometries.push(support)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const supportMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(supportGeometries), supportMat)
|
|
98
|
+
supportMesh.castShadow = true
|
|
99
|
+
this.object3d.add(supportMesh)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
updateDimension() {}
|
|
103
|
+
updateAlpha() {}
|
|
104
|
+
|
|
105
|
+
onchange(after: Record<string, unknown>, before: Record<string, unknown>) {
|
|
106
|
+
if ('width' in after || 'height' in after || 'depth' in after || 'fillStyle' in after || 'strokeStyle' in after) {
|
|
107
|
+
this.update()
|
|
108
|
+
return
|
|
109
|
+
}
|
|
110
|
+
super.onchange(after, before)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
registerRealObjectFactory('CraneRail', (component: Component) => new CraneRail3D(component))
|