@fmsim/machine 1.0.88 → 2.0.0-beta.100
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 +6 -2
- package/dist/agv.js.map +1 -1
- package/dist/buffer.js.map +1 -1
- package/dist/carrier-manager.js +183 -0
- package/dist/carrier-manager.js.map +1 -0
- package/dist/carrier.js +105 -14
- package/dist/carrier.js.map +1 -1
- package/dist/conveyor-join.js +8 -2
- package/dist/conveyor-join.js.map +1 -1
- package/dist/conveyor.js +9 -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 +144 -1
- package/dist/crane.js.map +1 -1
- package/dist/equipment.js.map +1 -1
- package/dist/factories/agv-3d.js +147 -0
- package/dist/factories/agv-3d.js.map +1 -0
- package/dist/factories/conveyor-3d.js +345 -0
- package/dist/factories/conveyor-3d.js.map +1 -0
- package/dist/factories/crane-3d.js +186 -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 +973 -0
- package/dist/factories/machine-3d.js.map +1 -0
- package/dist/factories/oht-3d.js +177 -0
- package/dist/factories/oht-3d.js.map +1 -0
- package/dist/factories/shuttle-3d.js +159 -0
- package/dist/factories/shuttle-3d.js.map +1 -0
- package/dist/factories/z-priority-defaults.js +33 -0
- package/dist/factories/z-priority-defaults.js.map +1 -0
- package/dist/factories/z-priority.js +54 -0
- package/dist/factories/z-priority.js.map +1 -0
- package/dist/features/conveyor-mixin.js.map +1 -1
- package/dist/features/mcs-status-default.js +15 -1
- package/dist/features/mcs-status-default.js.map +1 -1
- package/dist/features/mcs-status-mixin.js +22 -0
- package/dist/features/mcs-status-mixin.js.map +1 -1
- package/dist/features/mcs-zone-mixin.js.map +1 -1
- package/dist/features/parent-object-mixin.js +15 -7
- package/dist/features/parent-object-mixin.js.map +1 -1
- package/dist/floating-container.js +5 -0
- package/dist/floating-container.js.map +1 -1
- package/dist/index.js +58 -2
- package/dist/index.js.map +1 -1
- package/dist/mcs-carrier-holder.js +56 -17
- package/dist/mcs-carrier-holder.js.map +1 -1
- package/dist/mcs-machine.js +39 -0
- package/dist/mcs-machine.js.map +1 -1
- package/dist/mcs-unit.js +20 -0
- package/dist/mcs-unit.js.map +1 -1
- package/dist/mcs-vehicle.js +10 -4
- package/dist/mcs-vehicle.js.map +1 -1
- package/dist/node.js +10 -3
- package/dist/node.js.map +1 -1
- package/dist/oht-line.js.map +1 -1
- package/dist/oht.js +4 -0
- package/dist/oht.js.map +1 -1
- package/dist/port-flow.js +4 -0
- package/dist/port-flow.js.map +1 -1
- package/dist/port.js +4 -0
- package/dist/port.js.map +1 -1
- package/dist/scanner.js +30 -0
- package/dist/scanner.js.map +1 -0
- package/dist/scene/root-container-override.js +160 -96
- package/dist/scene/root-container-override.js.map +1 -1
- package/dist/shelf.js +4 -0
- package/dist/shelf.js.map +1 -1
- package/dist/shuttle.js.map +1 -1
- package/dist/static-container.js +5 -0
- package/dist/static-container.js.map +1 -1
- package/dist/stocker-abnormal-bar.js +184 -0
- package/dist/stocker-abnormal-bar.js.map +1 -0
- package/dist/stocker-capacity-bar.js +253 -6
- package/dist/stocker-capacity-bar.js.map +1 -1
- package/dist/stocker-carrier-bar.js +198 -0
- package/dist/stocker-carrier-bar.js.map +1 -0
- package/dist/stocker.js +7 -1
- package/dist/stocker.js.map +1 -1
- package/dist/templates/backdrop.js +30 -0
- package/dist/templates/backdrop.js.map +1 -0
- package/dist/templates/decal.js +21 -0
- package/dist/templates/decal.js.map +1 -0
- package/dist/templates/index.js +7 -1
- package/dist/templates/index.js.map +1 -1
- package/dist/templates/scanner.js +20 -0
- package/dist/templates/scanner.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/utils/crane-position.js +83 -0
- package/dist/utils/crane-position.js.map +1 -0
- package/dist/utils/data-value.js +21 -0
- package/dist/utils/data-value.js.map +1 -0
- package/dist/utils/fit-gauge-scale.js +28 -0
- package/dist/utils/fit-gauge-scale.js.map +1 -0
- package/dist/utils/get-value-on-ranges.js +2 -4
- package/dist/utils/get-value-on-ranges.js.map +1 -1
- package/dist/utils/stocker-gauge.js +78 -0
- package/dist/utils/stocker-gauge.js.map +1 -0
- package/dist/zone-capacity-bar.js +3 -3
- package/dist/zone-capacity-bar.js.map +1 -1
- package/icons/backdrop.png +0 -0
- package/icons/decal.png +0 -0
- package/jest.config.js +10 -0
- package/package.json +7 -6
- package/src/agv-line.ts +1 -1
- package/src/agv.ts +10 -5
- package/src/buffer.ts +1 -1
- package/src/carrier-manager.ts +223 -0
- package/src/carrier.ts +111 -18
- package/src/conveyor-join.ts +6 -3
- package/src/conveyor.ts +10 -2
- package/src/crane-rail.ts +58 -0
- package/src/crane.ts +182 -2
- package/src/equipment.ts +1 -1
- package/src/factories/agv-3d.ts +160 -0
- package/src/factories/conveyor-3d.ts +398 -0
- package/src/factories/crane-3d.ts +217 -0
- package/src/factories/crane-rail-3d.ts +115 -0
- package/src/factories/machine-3d.ts +1192 -0
- package/src/factories/oht-3d.ts +196 -0
- package/src/factories/shuttle-3d.ts +179 -0
- package/src/factories/z-priority-defaults.ts +38 -0
- package/src/factories/z-priority.ts +60 -0
- package/src/features/conveyor-mixin.ts +1 -1
- package/src/features/mcs-status-default.ts +15 -1
- package/src/features/mcs-status-mixin.ts +26 -4
- package/src/features/mcs-zone-mixin.ts +1 -1
- package/src/features/parent-object-mixin.ts +17 -8
- package/src/floating-container.ts +5 -0
- package/src/index.ts +66 -2
- package/src/mcs-carrier-holder.ts +58 -29
- package/src/mcs-machine.ts +43 -1
- package/src/mcs-unit.ts +25 -2
- package/src/mcs-vehicle.ts +12 -9
- package/src/node.ts +9 -3
- package/src/oht-line.ts +1 -1
- package/src/oht.ts +7 -2
- package/src/port-flow.ts +6 -1
- package/src/port.ts +6 -1
- package/src/scanner.ts +29 -0
- package/src/scene/root-container-override.ts +156 -105
- package/src/shelf.ts +6 -1
- package/src/shuttle.ts +1 -1
- package/src/static-container.ts +5 -0
- package/src/stocker-abnormal-bar.ts +247 -0
- package/src/stocker-capacity-bar.ts +301 -10
- package/src/stocker-carrier-bar.ts +262 -0
- package/src/stocker.ts +10 -3
- package/src/templates/backdrop.ts +30 -0
- package/src/templates/decal.ts +21 -0
- package/src/templates/index.ts +7 -1
- package/src/templates/scanner.ts +20 -0
- package/src/utils/crane-position.ts +105 -0
- package/src/utils/data-value.ts +19 -0
- package/src/utils/fit-gauge-scale.ts +35 -0
- package/src/utils/get-value-on-ranges.ts +2 -4
- package/src/utils/stocker-gauge.ts +111 -0
- package/src/zone-capacity-bar.ts +5 -5
- package/translations/en.json +26 -1
- package/translations/ja.json +26 -1
- package/translations/ko.json +26 -1
- package/translations/ms.json +26 -1
- package/translations/zh.json +26 -1
|
@@ -0,0 +1,398 @@
|
|
|
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
|
+
import { Z_FMSIM } from './z-priority'
|
|
20
|
+
|
|
21
|
+
const FRAME_COLOR = 0x888899
|
|
22
|
+
const ROLLER_COLOR = 0xaaaabc
|
|
23
|
+
const DEFAULT_COLOR = 0x999999
|
|
24
|
+
|
|
25
|
+
export class Conveyor3D extends RealObjectGroup {
|
|
26
|
+
get zPriority(): number { return Z_FMSIM.CONVEYOR }
|
|
27
|
+
|
|
28
|
+
get effectiveDepth(): number {
|
|
29
|
+
const { depth } = this.component.state
|
|
30
|
+
return depth || TRANSPORT_SURFACE_HEIGHT
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
private resolveColor(): number {
|
|
34
|
+
const comp = this.component as any
|
|
35
|
+
|
|
36
|
+
// 1) MCSStatusMixin.statusColor (legend 기반 상태 색상)
|
|
37
|
+
// '#F0F0F0'은 상태 미설정 시 기본 fallback — 무시하고 defaultColor 사용
|
|
38
|
+
try {
|
|
39
|
+
const sc = comp.statusColor
|
|
40
|
+
if (typeof sc === 'string' && sc && sc.toUpperCase() !== '#F0F0F0' && sc !== 'transparent') return new THREE.Color(sc).getHex()
|
|
41
|
+
} catch {
|
|
42
|
+
/* statusColor 미구현 시 무시 */
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// 2) state.fillStyle fallback
|
|
46
|
+
const { fillStyle } = this.component.state
|
|
47
|
+
if (fillStyle && typeof fillStyle === 'string' && fillStyle !== 'transparent') {
|
|
48
|
+
try {
|
|
49
|
+
return new THREE.Color(fillStyle).getHex()
|
|
50
|
+
} catch {
|
|
51
|
+
/* 파싱 실패 시 무시 */
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return DEFAULT_COLOR
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
build() {
|
|
59
|
+
super.build()
|
|
60
|
+
|
|
61
|
+
const { width: rawWidth, height: rawHeight, conveyorType, orientation } = this.component.state
|
|
62
|
+
const depth = this.effectiveDepth
|
|
63
|
+
if (!rawWidth || !rawHeight) return
|
|
64
|
+
|
|
65
|
+
// orientation: 'horizontal' = 롤러가 Z축(세로), 'vertical' = 롤러가 X축(가로)
|
|
66
|
+
// default = 긴 방향에 수직 (긴 축을 따라 이송)
|
|
67
|
+
const isVertical = orientation === 'vertical' ? true
|
|
68
|
+
: orientation === 'horizontal' ? false
|
|
69
|
+
: rawWidth < rawHeight // default: 세로가 길면 롤러는 가로(X축)
|
|
70
|
+
// isVertical=true → 롤러 축이 X(가로), 이송 방향이 Z(세로)
|
|
71
|
+
// isVertical=false → 롤러 축이 Z(세로), 이송 방향이 X(가로)
|
|
72
|
+
const width = rawWidth
|
|
73
|
+
const height = rawHeight
|
|
74
|
+
|
|
75
|
+
// 2D와 동일 로직으로 rollWidth 산출
|
|
76
|
+
const rollWidth = Math.max(10, Math.min(width, height) / 2)
|
|
77
|
+
const rollerDiameter = Math.max(rollWidth, 2)
|
|
78
|
+
const frameH = Math.min(rollerDiameter, depth)
|
|
79
|
+
const legH = Math.max(depth - frameH, 0)
|
|
80
|
+
|
|
81
|
+
// isVertical일 때 이송 방향이 Z축: 레일은 Z축을 따라, 벨트폭은 width
|
|
82
|
+
// !isVertical일 때 이송 방향이 X축: 레일은 X축을 따라, 벨트폭은 height
|
|
83
|
+
const beltWidth = isVertical ? width : height
|
|
84
|
+
const railLen = isVertical ? height : width
|
|
85
|
+
const railW = Math.max(beltWidth * 0.06, 2)
|
|
86
|
+
const legThickness = Math.max(railW * 0.8, 2)
|
|
87
|
+
|
|
88
|
+
const frameMaterial = new THREE.MeshStandardMaterial({
|
|
89
|
+
color: FRAME_COLOR,
|
|
90
|
+
metalness: 0.85,
|
|
91
|
+
roughness: 0.35,
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
// --- Frame: side rails + legs + cross-bracing ---
|
|
95
|
+
// Y 좌표: y=0(바닥) ~ y=depth(상단). 레일 상단이 depth에 위치.
|
|
96
|
+
const frameGeometries: THREE.BufferGeometry[] = []
|
|
97
|
+
const railY = depth - frameH / 2
|
|
98
|
+
|
|
99
|
+
// Side rails — 이송 방향을 따라, 벨트 양측에 배치
|
|
100
|
+
if (isVertical) {
|
|
101
|
+
// 레일이 Z축(세로)을 따라, ±X에 배치
|
|
102
|
+
for (const xSign of [-1, 1]) {
|
|
103
|
+
const rail = new THREE.BoxGeometry(railW, frameH, height)
|
|
104
|
+
rail.translate(xSign * (width / 2 - railW / 2), railY, 0)
|
|
105
|
+
frameGeometries.push(rail)
|
|
106
|
+
}
|
|
107
|
+
} else {
|
|
108
|
+
// 레일이 X축(가로)을 따라, ±Z에 배치
|
|
109
|
+
for (const zSign of [-1, 1]) {
|
|
110
|
+
const rail = new THREE.BoxGeometry(width, frameH, railW)
|
|
111
|
+
rail.translate(0, railY, zSign * (height / 2 - railW / 2))
|
|
112
|
+
frameGeometries.push(rail)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// 4 legs at corners
|
|
117
|
+
if (legH > 0) {
|
|
118
|
+
const legTopY = depth - frameH
|
|
119
|
+
const legCenterY = legTopY - legH / 2
|
|
120
|
+
|
|
121
|
+
// 다리 위치: 레일 안쪽에 정렬
|
|
122
|
+
const legXPos = isVertical ? (width / 2 - railW / 2) : (width / 2 - legThickness / 2)
|
|
123
|
+
const legZPos = isVertical ? (height / 2 - legThickness / 2) : (height / 2 - railW / 2)
|
|
124
|
+
|
|
125
|
+
for (const xSign of [-1, 1]) {
|
|
126
|
+
for (const zSign of [-1, 1]) {
|
|
127
|
+
const leg = new THREE.BoxGeometry(legThickness, legH, legThickness)
|
|
128
|
+
leg.translate(xSign * legXPos, legCenterY, zSign * legZPos)
|
|
129
|
+
frameGeometries.push(leg)
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Cross-bracing
|
|
134
|
+
const braceH = legThickness * 0.6
|
|
135
|
+
const braceW = legThickness * 0.6
|
|
136
|
+
const braceY = legTopY - legH * 0.35
|
|
137
|
+
|
|
138
|
+
// 이송 방향 브레이스 (레일과 평행)
|
|
139
|
+
if (isVertical) {
|
|
140
|
+
for (const xSign of [-1, 1]) {
|
|
141
|
+
const brace = new THREE.BoxGeometry(braceW, braceH, height - legThickness)
|
|
142
|
+
brace.translate(xSign * legXPos, braceY, 0)
|
|
143
|
+
frameGeometries.push(brace)
|
|
144
|
+
}
|
|
145
|
+
} else {
|
|
146
|
+
for (const zSign of [-1, 1]) {
|
|
147
|
+
const brace = new THREE.BoxGeometry(width - legThickness, braceH, braceW)
|
|
148
|
+
brace.translate(0, braceY, zSign * legZPos)
|
|
149
|
+
frameGeometries.push(brace)
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// 벨트폭 방향 브레이스 (레일과 수직)
|
|
154
|
+
if (isVertical) {
|
|
155
|
+
for (const zSign of [-1, 1]) {
|
|
156
|
+
const brace = new THREE.BoxGeometry(width - railW, braceH, braceW)
|
|
157
|
+
brace.translate(0, braceY, zSign * legZPos)
|
|
158
|
+
frameGeometries.push(brace)
|
|
159
|
+
}
|
|
160
|
+
} else {
|
|
161
|
+
for (const xSign of [-1, 1]) {
|
|
162
|
+
const brace = new THREE.BoxGeometry(braceW, braceH, height - railW)
|
|
163
|
+
brace.translate(xSign * legXPos, braceY, 0)
|
|
164
|
+
frameGeometries.push(brace)
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const frameMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(frameGeometries), frameMaterial)
|
|
170
|
+
frameMesh.castShadow = true
|
|
171
|
+
frameMesh.receiveShadow = true
|
|
172
|
+
this.components3D.add(frameMesh)
|
|
173
|
+
|
|
174
|
+
// --- Rollers, Belt, or Rails ---
|
|
175
|
+
if (conveyorType === 'rail') {
|
|
176
|
+
this.buildRails(width, height, depth, frameH, railW, isVertical)
|
|
177
|
+
} else if (conveyorType === 'belt') {
|
|
178
|
+
this.buildBelt(width, height, depth, frameH, railW, isVertical)
|
|
179
|
+
} else {
|
|
180
|
+
this.buildRollers(width, height, depth, frameH, railW, rollWidth, isVertical)
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
private buildRollers(
|
|
185
|
+
width: number,
|
|
186
|
+
height: number,
|
|
187
|
+
depth: number,
|
|
188
|
+
frameH: number,
|
|
189
|
+
railW: number,
|
|
190
|
+
rollWidth: number,
|
|
191
|
+
isVertical: boolean = false
|
|
192
|
+
) {
|
|
193
|
+
const rollerRadius = Math.max(rollWidth / 8, 1)
|
|
194
|
+
const diameter = rollerRadius * 2
|
|
195
|
+
const step = diameter * 1.05
|
|
196
|
+
const rollerY = depth - rollerRadius
|
|
197
|
+
const color = this.resolveColor()
|
|
198
|
+
|
|
199
|
+
const rollerGeometries: THREE.BufferGeometry[] = []
|
|
200
|
+
|
|
201
|
+
if (isVertical) {
|
|
202
|
+
// 롤러 축 = X축(가로), 이송 방향 = Z축(세로)
|
|
203
|
+
const rollerLength = width - railW * 2 - 0.5
|
|
204
|
+
const count = Math.max(1, Math.floor(height / step))
|
|
205
|
+
const totalSpan = (count - 1) * step
|
|
206
|
+
const startZ = -totalSpan / 2
|
|
207
|
+
|
|
208
|
+
for (let i = 0; i < count; i++) {
|
|
209
|
+
const z = startZ + i * step
|
|
210
|
+
const roller = new THREE.CylinderGeometry(rollerRadius, rollerRadius, rollerLength, 16)
|
|
211
|
+
roller.rotateZ(Math.PI / 2)
|
|
212
|
+
roller.translate(0, rollerY, z)
|
|
213
|
+
rollerGeometries.push(roller)
|
|
214
|
+
}
|
|
215
|
+
} else {
|
|
216
|
+
// 롤러 축 = Z축(세로), 이송 방향 = X축(가로) — 기존 동작
|
|
217
|
+
const rollerLength = height - railW * 2 - 0.5
|
|
218
|
+
const count = Math.max(1, Math.floor(width / step))
|
|
219
|
+
const totalSpan = (count - 1) * step
|
|
220
|
+
const startX = -totalSpan / 2
|
|
221
|
+
|
|
222
|
+
for (let i = 0; i < count; i++) {
|
|
223
|
+
const x = startX + i * step
|
|
224
|
+
const roller = new THREE.CylinderGeometry(rollerRadius, rollerRadius, rollerLength, 16)
|
|
225
|
+
roller.rotateX(Math.PI / 2)
|
|
226
|
+
roller.translate(x, rollerY, 0)
|
|
227
|
+
rollerGeometries.push(roller)
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (rollerGeometries.length > 0) {
|
|
232
|
+
const rollerMesh = new THREE.Mesh(
|
|
233
|
+
BufferGeometryUtils.mergeGeometries(rollerGeometries),
|
|
234
|
+
new THREE.MeshStandardMaterial({ color, metalness: 0.9, roughness: 0.2 })
|
|
235
|
+
)
|
|
236
|
+
rollerMesh.castShadow = true
|
|
237
|
+
this.components3D.add(rollerMesh)
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
private buildBelt(width: number, height: number, depth: number, frameH: number, railW: number, isVertical: boolean = false) {
|
|
242
|
+
const drumRadius = frameH * 0.38
|
|
243
|
+
const beltY = depth - drumRadius
|
|
244
|
+
const color = this.resolveColor()
|
|
245
|
+
|
|
246
|
+
// 이송 방향 길이와 벨트폭
|
|
247
|
+
const transportLen = isVertical ? height : width
|
|
248
|
+
const beltWidth = isVertical ? width : height
|
|
249
|
+
const drumLength = beltWidth - railW * 2
|
|
250
|
+
|
|
251
|
+
// Two end drums
|
|
252
|
+
const drumGeometries: THREE.BufferGeometry[] = []
|
|
253
|
+
const drumInset = drumRadius * 1.5
|
|
254
|
+
const drum1 = -transportLen / 2 + drumInset
|
|
255
|
+
const drum2 = transportLen / 2 - drumInset
|
|
256
|
+
|
|
257
|
+
for (const dp of [drum1, drum2]) {
|
|
258
|
+
const drum = new THREE.CylinderGeometry(drumRadius, drumRadius, drumLength, 16)
|
|
259
|
+
if (isVertical) {
|
|
260
|
+
drum.rotateZ(Math.PI / 2)
|
|
261
|
+
drum.translate(0, beltY, dp)
|
|
262
|
+
} else {
|
|
263
|
+
drum.rotateX(Math.PI / 2)
|
|
264
|
+
drum.translate(dp, beltY, 0)
|
|
265
|
+
}
|
|
266
|
+
drumGeometries.push(drum)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const drumMesh = new THREE.Mesh(
|
|
270
|
+
BufferGeometryUtils.mergeGeometries(drumGeometries),
|
|
271
|
+
new THREE.MeshStandardMaterial({ color: ROLLER_COLOR, metalness: 0.9, roughness: 0.2 })
|
|
272
|
+
)
|
|
273
|
+
drumMesh.castShadow = true
|
|
274
|
+
this.components3D.add(drumMesh)
|
|
275
|
+
|
|
276
|
+
// Belt surface
|
|
277
|
+
const beltThickness = drumRadius * 0.12
|
|
278
|
+
const beltMaterial = new THREE.MeshStandardMaterial({
|
|
279
|
+
color: color,
|
|
280
|
+
metalness: 0.0,
|
|
281
|
+
roughness: 0.9,
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
// Flat belt top surface spanning between drums
|
|
285
|
+
const flatLen = drum2 - drum1
|
|
286
|
+
const flatGeo = isVertical
|
|
287
|
+
? new THREE.BoxGeometry(drumLength, beltThickness, flatLen)
|
|
288
|
+
: new THREE.BoxGeometry(flatLen, beltThickness, drumLength)
|
|
289
|
+
const flatMesh = new THREE.Mesh(flatGeo, beltMaterial)
|
|
290
|
+
flatMesh.position.set(0, beltY + drumRadius, 0)
|
|
291
|
+
flatMesh.castShadow = true
|
|
292
|
+
this.components3D.add(flatMesh)
|
|
293
|
+
|
|
294
|
+
// Belt wrap around drums (segmented half-cylinder)
|
|
295
|
+
const wrapSegments = 12
|
|
296
|
+
for (const [dp, angleStart] of [
|
|
297
|
+
[drum1, Math.PI / 2],
|
|
298
|
+
[drum2, -Math.PI / 2],
|
|
299
|
+
] as [number, number][]) {
|
|
300
|
+
const wrapGeometries: THREE.BufferGeometry[] = []
|
|
301
|
+
for (let i = 0; i < wrapSegments; i++) {
|
|
302
|
+
const a0 = angleStart + (Math.PI * i) / wrapSegments
|
|
303
|
+
const a1 = angleStart + (Math.PI * (i + 1)) / wrapSegments
|
|
304
|
+
const r = drumRadius + beltThickness / 2
|
|
305
|
+
|
|
306
|
+
const p0 = dp + Math.cos(a0) * r
|
|
307
|
+
const y0 = beltY + Math.sin(a0) * r
|
|
308
|
+
const p1 = dp + Math.cos(a1) * r
|
|
309
|
+
const y1 = beltY + Math.sin(a1) * r
|
|
310
|
+
|
|
311
|
+
const segLen = Math.sqrt((p1 - p0) ** 2 + (y1 - y0) ** 2) * 1.05
|
|
312
|
+
const segAngle = Math.atan2(y1 - y0, p1 - p0)
|
|
313
|
+
|
|
314
|
+
if (isVertical) {
|
|
315
|
+
const seg = new THREE.BoxGeometry(drumLength, beltThickness, segLen)
|
|
316
|
+
seg.rotateX(-segAngle)
|
|
317
|
+
seg.translate(0, (y0 + y1) / 2, (p0 + p1) / 2)
|
|
318
|
+
wrapGeometries.push(seg)
|
|
319
|
+
} else {
|
|
320
|
+
const seg = new THREE.BoxGeometry(segLen, beltThickness, drumLength)
|
|
321
|
+
seg.rotateZ(segAngle)
|
|
322
|
+
seg.translate((p0 + p1) / 2, (y0 + y1) / 2, 0)
|
|
323
|
+
wrapGeometries.push(seg)
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
const wrapMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(wrapGeometries), beltMaterial)
|
|
327
|
+
wrapMesh.castShadow = true
|
|
328
|
+
this.components3D.add(wrapMesh)
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Flat belt bottom (return path)
|
|
332
|
+
const bottomGeo = isVertical
|
|
333
|
+
? new THREE.BoxGeometry(drumLength * 0.85, beltThickness, flatLen)
|
|
334
|
+
: new THREE.BoxGeometry(flatLen, beltThickness, drumLength * 0.85)
|
|
335
|
+
const bottomMesh = new THREE.Mesh(bottomGeo, beltMaterial)
|
|
336
|
+
bottomMesh.position.set(0, beltY - drumRadius, 0)
|
|
337
|
+
this.components3D.add(bottomMesh)
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
private buildRails(width: number, height: number, depth: number, frameH: number, railW: number, isVertical: boolean = false) {
|
|
341
|
+
const color = this.resolveColor()
|
|
342
|
+
const railHeight = frameH * 0.3
|
|
343
|
+
const railY = depth - railHeight / 2
|
|
344
|
+
const beltWidth = isVertical ? width : height
|
|
345
|
+
const transportLen = isVertical ? height : width
|
|
346
|
+
const innerWidth = beltWidth - railW * 2
|
|
347
|
+
// 두 레일 간격: 내부 폭의 60%
|
|
348
|
+
const railSpacing = innerWidth * 0.6
|
|
349
|
+
|
|
350
|
+
const railMaterial = new THREE.MeshStandardMaterial({
|
|
351
|
+
color,
|
|
352
|
+
metalness: 0.7,
|
|
353
|
+
roughness: 0.3,
|
|
354
|
+
})
|
|
355
|
+
|
|
356
|
+
const railGeometries: THREE.BufferGeometry[] = []
|
|
357
|
+
|
|
358
|
+
// Two parallel rails running along transport direction
|
|
359
|
+
if (isVertical) {
|
|
360
|
+
for (const xSign of [-1, 1]) {
|
|
361
|
+
const rail = new THREE.BoxGeometry(railW * 0.8, railHeight, transportLen - railW * 2)
|
|
362
|
+
rail.translate(xSign * (railSpacing / 2), railY, 0)
|
|
363
|
+
railGeometries.push(rail)
|
|
364
|
+
}
|
|
365
|
+
} else {
|
|
366
|
+
for (const zSign of [-1, 1]) {
|
|
367
|
+
const rail = new THREE.BoxGeometry(transportLen - railW * 2, railHeight, railW * 0.8)
|
|
368
|
+
rail.translate(0, railY, zSign * (railSpacing / 2))
|
|
369
|
+
railGeometries.push(rail)
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const railMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(railGeometries), railMaterial)
|
|
374
|
+
railMesh.castShadow = true
|
|
375
|
+
this.components3D.add(railMesh)
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
updateDimension() {}
|
|
379
|
+
updateAlpha() {}
|
|
380
|
+
|
|
381
|
+
onchange(after: Record<string, unknown>, before: Record<string, unknown>) {
|
|
382
|
+
if (
|
|
383
|
+
'conveyorType' in after ||
|
|
384
|
+
'orientation' in after ||
|
|
385
|
+
'width' in after ||
|
|
386
|
+
'height' in after ||
|
|
387
|
+
'depth' in after ||
|
|
388
|
+
'fillStyle' in after ||
|
|
389
|
+
'strokeStyle' in after
|
|
390
|
+
) {
|
|
391
|
+
this.update()
|
|
392
|
+
return
|
|
393
|
+
}
|
|
394
|
+
super.onchange(after, before)
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
registerRealObjectFactory('Conveyor', (component: Component) => new Conveyor3D(component))
|
|
@@ -0,0 +1,217 @@
|
|
|
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
|
+
/** 캐리지 포크 group — Crane.state.forkZ (0~100) 가 변하면 position.y 만 lerp.
|
|
26
|
+
* build() 시 inner 에 add 되고, *_mastBaseY 와 mastH 를 함께 보관* 해서
|
|
27
|
+
* setForkZ 가 비율 계산에 사용. */
|
|
28
|
+
private forkGroup?: THREE.Group
|
|
29
|
+
private forkMastBaseY = 0
|
|
30
|
+
private forkMastH = 0
|
|
31
|
+
|
|
32
|
+
get effectiveDepth(): number {
|
|
33
|
+
const { depth } = this.component.state
|
|
34
|
+
return depth || 5 * TRANSPORT_SURFACE_HEIGHT - 2
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* 0~100 정규화 z (= state.forkZ) 를 mast 높이 비율로 변환해서 포크 group 의
|
|
39
|
+
* three-up y 위치 갱신. build() 가 안 끝났으면 noop — build 가 forkZ 를 다시
|
|
40
|
+
* 반영하면서 끝난다.
|
|
41
|
+
*/
|
|
42
|
+
applyForkZ(z: number): void {
|
|
43
|
+
if (!this.forkGroup) return
|
|
44
|
+
const clamped = Math.max(0, Math.min(100, Number(z) || 0))
|
|
45
|
+
this.forkGroup.position.y = this.forkMastBaseY + (clamped / 100) * this.forkMastH
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
private resolveColor(): number {
|
|
49
|
+
const comp = this.component as any
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
const sc = comp.statusColor
|
|
53
|
+
if (typeof sc === 'string' && sc && sc.toUpperCase() !== '#F0F0F0') return new THREE.Color(sc).getHex()
|
|
54
|
+
} catch {
|
|
55
|
+
/* statusColor 미구현 시 무시 */
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const { fillStyle } = this.component.state
|
|
59
|
+
if (fillStyle && typeof fillStyle === 'string') {
|
|
60
|
+
try {
|
|
61
|
+
return new THREE.Color(fillStyle).getHex()
|
|
62
|
+
} catch {
|
|
63
|
+
/* 파싱 실패 시 무시 */
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return DEFAULT_BODY_COLOR
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
build() {
|
|
71
|
+
super.build()
|
|
72
|
+
|
|
73
|
+
const { width = 10, height = 10 } = this.component.state
|
|
74
|
+
const w = Math.abs(width)
|
|
75
|
+
const h = Math.abs(height)
|
|
76
|
+
const d = this.effectiveDepth
|
|
77
|
+
if (!w || !h) return
|
|
78
|
+
|
|
79
|
+
const inner = this.components3D
|
|
80
|
+
|
|
81
|
+
// 높이 배분: 하단 클램프 4%, 마스트 92%, 상단 클램프 4%
|
|
82
|
+
const clampH = d * 0.04
|
|
83
|
+
const mastH = d * 0.92
|
|
84
|
+
|
|
85
|
+
const pillarThick = Math.min(w, h) * 0.12
|
|
86
|
+
|
|
87
|
+
const clampMat = new THREE.MeshStandardMaterial({
|
|
88
|
+
color: CLAMP_COLOR,
|
|
89
|
+
roughness: 0.5,
|
|
90
|
+
metalness: 0.5,
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
// ── 1. 하단 클램프 (바닥 레일에 물리는 바퀴부) ──
|
|
94
|
+
const clampW = w * 0.7
|
|
95
|
+
const clampD = h * 0.5
|
|
96
|
+
const bottomClampGeo = new THREE.BoxGeometry(clampW, clampH, clampD)
|
|
97
|
+
const bottomClamp = new THREE.Mesh(bottomClampGeo, clampMat)
|
|
98
|
+
bottomClamp.position.y = clampH / 2
|
|
99
|
+
bottomClamp.castShadow = true
|
|
100
|
+
inner.add(bottomClamp)
|
|
101
|
+
|
|
102
|
+
// ── 2. 수직 마스트 (좌우 기둥 + 상·하 가로 연결 바) ──
|
|
103
|
+
const mastBaseY = clampH
|
|
104
|
+
const bodyColor = this.resolveColor()
|
|
105
|
+
const mastMat = new THREE.MeshStandardMaterial({
|
|
106
|
+
color: bodyColor,
|
|
107
|
+
roughness: 0.3,
|
|
108
|
+
metalness: 0.6,
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
const mastGeometries: THREE.BufferGeometry[] = []
|
|
112
|
+
|
|
113
|
+
// 좌우 수직 기둥
|
|
114
|
+
for (const xSign of [-1, 1]) {
|
|
115
|
+
const pillar = new THREE.BoxGeometry(pillarThick, mastH, pillarThick)
|
|
116
|
+
pillar.translate(xSign * (w / 2 - pillarThick / 2), mastBaseY + mastH / 2, 0)
|
|
117
|
+
mastGeometries.push(pillar)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// 하단 가로 연결 바
|
|
121
|
+
const crossW = w - pillarThick
|
|
122
|
+
const crossH = pillarThick * 0.5
|
|
123
|
+
const lowerCross = new THREE.BoxGeometry(crossW, crossH, pillarThick)
|
|
124
|
+
lowerCross.translate(0, mastBaseY + crossH / 2, 0)
|
|
125
|
+
mastGeometries.push(lowerCross)
|
|
126
|
+
|
|
127
|
+
// 상단 가로 연결 바
|
|
128
|
+
const upperCross = new THREE.BoxGeometry(crossW, crossH, pillarThick)
|
|
129
|
+
upperCross.translate(0, mastBaseY + mastH - crossH / 2, 0)
|
|
130
|
+
mastGeometries.push(upperCross)
|
|
131
|
+
|
|
132
|
+
const mastMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(mastGeometries), mastMat)
|
|
133
|
+
mastMesh.castShadow = true
|
|
134
|
+
inner.add(mastMesh)
|
|
135
|
+
|
|
136
|
+
// ── 3. 캐리지 포크 (별도 group — state.forkZ 변경 시 group.position.y lerp) ──
|
|
137
|
+
// 포크 geometry 는 *_mastBaseY 기준 local 0 에 둠* — 즉 group.position.y = 0
|
|
138
|
+
// 일 때 mast 바닥 자리에. forkZ 0~100 에 따라 group.position.y 만 변경하면
|
|
139
|
+
// mast 따라 위/아래 이동. mesh 재구축 불필요.
|
|
140
|
+
const forkMat = new THREE.MeshStandardMaterial({
|
|
141
|
+
color: FORK_COLOR,
|
|
142
|
+
roughness: 0.4,
|
|
143
|
+
metalness: 0.5,
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
const forkGeometries: THREE.BufferGeometry[] = []
|
|
147
|
+
|
|
148
|
+
const plateW = w * 0.9
|
|
149
|
+
const plateH = d * 0.08
|
|
150
|
+
const plateD = pillarThick * 0.8
|
|
151
|
+
const centerPlate = new THREE.BoxGeometry(plateW, plateH, plateD)
|
|
152
|
+
centerPlate.translate(0, plateH / 2, 0) // local 0 = mast 바닥
|
|
153
|
+
forkGeometries.push(centerPlate)
|
|
154
|
+
|
|
155
|
+
const armW = pillarThick * 0.6
|
|
156
|
+
const armH = plateH * 0.4
|
|
157
|
+
const armD = h * 0.6
|
|
158
|
+
const armSpacing = w * 0.5
|
|
159
|
+
|
|
160
|
+
for (const zSign of [-1, 1]) {
|
|
161
|
+
for (const xSign of [-1, 1]) {
|
|
162
|
+
const arm = new THREE.BoxGeometry(armW, armH, armD)
|
|
163
|
+
arm.translate(xSign * (armSpacing / 2), armH / 2, zSign * (plateD / 2 + armD / 2))
|
|
164
|
+
forkGeometries.push(arm)
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const forkMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(forkGeometries), forkMat)
|
|
169
|
+
forkMesh.castShadow = true
|
|
170
|
+
|
|
171
|
+
const forkGroup = new THREE.Group()
|
|
172
|
+
forkGroup.add(forkMesh)
|
|
173
|
+
inner.add(forkGroup)
|
|
174
|
+
this.forkGroup = forkGroup
|
|
175
|
+
this.forkMastBaseY = mastBaseY
|
|
176
|
+
// forkZ=100 시점에 mast 의 *_위쪽 한계*. 포크 자체 두께 (plateH) 만큼은 마스트
|
|
177
|
+
// 안에 머무르도록 limit 보정 — 그렇지 않으면 forkZ=100 일 때 포크가 상단
|
|
178
|
+
// 클램프와 충돌. mastH * 0.92 로 약간 보수적 (= 마스트 본체 92% 까지만 이동).
|
|
179
|
+
this.forkMastH = mastH * 0.92
|
|
180
|
+
|
|
181
|
+
// 초기 forkZ 반영 — state.forkZ 가 이미 set 되어 있으면 그 위치에 배치
|
|
182
|
+
const initialForkZ = Number((this.component.state as any).forkZ)
|
|
183
|
+
this.applyForkZ(Number.isFinite(initialForkZ) ? initialForkZ : 0)
|
|
184
|
+
|
|
185
|
+
// ── 4. 상단 클램프 (천장 레일에 물리는 가이드부) ──
|
|
186
|
+
const topClampGeo = new THREE.BoxGeometry(clampW, clampH, clampD)
|
|
187
|
+
const topClamp = new THREE.Mesh(topClampGeo, clampMat)
|
|
188
|
+
topClamp.position.y = mastBaseY + mastH + clampH / 2
|
|
189
|
+
topClamp.castShadow = true
|
|
190
|
+
inner.add(topClamp)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
updateDimension() {}
|
|
194
|
+
updateAlpha() {}
|
|
195
|
+
|
|
196
|
+
/** build() 직전 호출. forkGroup 참조를 깨끗이 비워둔다 — 새 build 가 다시 set. */
|
|
197
|
+
clear() {
|
|
198
|
+
this.forkGroup = undefined
|
|
199
|
+
super.clear()
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
onchange(after: Record<string, unknown>, before: Record<string, unknown>) {
|
|
203
|
+
if ('width' in after || 'height' in after || 'depth' in after || 'fillStyle' in after || 'strokeStyle' in after) {
|
|
204
|
+
this.update()
|
|
205
|
+
return
|
|
206
|
+
}
|
|
207
|
+
// forkZ 는 mesh 재구축 없이 group.position.y 만 갱신. 같은 onchange 에서
|
|
208
|
+
// left/top 도 함께 변경될 수 있으므로 super 도 반드시 호출해 본체 transform
|
|
209
|
+
// 갱신 path (state.left/top → obj3d.position.x/z) 가 동작하게 함.
|
|
210
|
+
if ('forkZ' in after) {
|
|
211
|
+
this.applyForkZ(Number(after.forkZ) || 0)
|
|
212
|
+
}
|
|
213
|
+
super.onchange(after, before)
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
registerRealObjectFactory('Crane', (component: Component) => new Crane3D(component))
|