@fmsim/machine 2.0.0-beta.17 → 2.0.0-beta.19
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/carrier.js +12 -1
- package/dist/carrier.js.map +1 -1
- package/dist/factories/agv-3d.js +115 -1
- package/dist/factories/agv-3d.js.map +1 -1
- package/dist/factories/machine-3d.js +30 -19
- package/dist/factories/machine-3d.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/carrier.ts +7 -1
- package/src/factories/agv-3d.ts +136 -1
- package/src/factories/machine-3d.ts +47 -24
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@fmsim/machine",
|
|
3
3
|
"description": "Layout View를 구성하는 설비(machine)들과 유닛을 구현한 모듈입니다.",
|
|
4
4
|
"author": "heartyoh",
|
|
5
|
-
"version": "2.0.0-beta.
|
|
5
|
+
"version": "2.0.0-beta.19",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"module": "dist/index.js",
|
|
8
8
|
"things-scene": true,
|
|
@@ -55,5 +55,5 @@
|
|
|
55
55
|
"prettier --write"
|
|
56
56
|
]
|
|
57
57
|
},
|
|
58
|
-
"gitHead": "
|
|
58
|
+
"gitHead": "29a0fc9444f6eec0961401d979fd21fdfd34e041"
|
|
59
59
|
}
|
package/src/carrier.ts
CHANGED
|
@@ -163,8 +163,14 @@ export default class Carrier extends MCSStatusMixin(ParentObjectMixin(Shape)) {
|
|
|
163
163
|
}
|
|
164
164
|
|
|
165
165
|
// 캐시에 없으면 파싱하고 저장
|
|
166
|
-
|
|
166
|
+
let theme = (this.root as any)?.style?.[carrierAnimationName]
|
|
167
167
|
if (theme) {
|
|
168
|
+
// theme이 string이면 먼저 파싱
|
|
169
|
+
if (typeof theme === 'string') {
|
|
170
|
+
try { theme = JSON5.parse(theme) } catch { return ANIMATION_DEFAULT }
|
|
171
|
+
}
|
|
172
|
+
if (typeof theme !== 'object' || theme === null) return ANIMATION_DEFAULT
|
|
173
|
+
|
|
168
174
|
// 문자열 형태의 설정들을 미리 파싱
|
|
169
175
|
const parsedTheme = {}
|
|
170
176
|
for (const [status, config] of Object.entries(theme)) {
|
package/src/factories/agv-3d.ts
CHANGED
|
@@ -59,9 +59,19 @@ export class AGV3D extends RealObjectGroup {
|
|
|
59
59
|
return DEFAULT_BODY_COLOR
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
private _dataHandler?: () => void
|
|
63
|
+
|
|
62
64
|
build() {
|
|
63
65
|
super.build()
|
|
64
66
|
|
|
67
|
+
// data 변경 감지 (배터리 잔량 업데이트)
|
|
68
|
+
if (!this._dataHandler) {
|
|
69
|
+
this._dataHandler = (_after: any, _before: any) => {
|
|
70
|
+
if (_after.data !== undefined) this.updateBattery()
|
|
71
|
+
}
|
|
72
|
+
this.component.on('change', this._dataHandler)
|
|
73
|
+
}
|
|
74
|
+
|
|
65
75
|
const { width = 10, height = 10 } = this.component.state
|
|
66
76
|
const w = Math.abs(width)
|
|
67
77
|
const h = Math.abs(height)
|
|
@@ -118,9 +128,127 @@ export class AGV3D extends RealObjectGroup {
|
|
|
118
128
|
plat.receiveShadow = true
|
|
119
129
|
|
|
120
130
|
this._ownMeshes = [bumper, body, plat]
|
|
131
|
+
|
|
132
|
+
// ── 배터리 실린더 (바디 뒷면, BATTERYRATE 데이터가 있을 때만) ──
|
|
133
|
+
if (this.getBatteryRate() > 0) {
|
|
134
|
+
const batteryMeshes = this.buildBattery(w, h, bumperH, bodyH)
|
|
135
|
+
this._ownMeshes.push(...batteryMeshes)
|
|
136
|
+
}
|
|
137
|
+
|
|
121
138
|
for (const m of this._ownMeshes) this.object3d.add(m)
|
|
122
139
|
}
|
|
123
140
|
|
|
141
|
+
private _batteryFillMesh?: THREE.Mesh
|
|
142
|
+
private _batteryLen = 0
|
|
143
|
+
private _batteryY = 0
|
|
144
|
+
private _batteryZ = 0
|
|
145
|
+
|
|
146
|
+
private buildBattery(agvW: number, agvH: number, bodyBottomY: number, bodyH: number): THREE.Mesh[] {
|
|
147
|
+
const radius = Math.min(agvW, agvH) * 0.15
|
|
148
|
+
this._batteryLen = agvW * 0.7
|
|
149
|
+
this._batteryY = bodyBottomY + bodyH * 0.5
|
|
150
|
+
this._batteryZ = -agvH / 2 - radius * 1.5
|
|
151
|
+
|
|
152
|
+
const rate = this.getBatteryRate()
|
|
153
|
+
const batteryColor = this.getBatteryColor(rate)
|
|
154
|
+
const innerRadius = radius * 0.85
|
|
155
|
+
const capWidth = radius * 0.3
|
|
156
|
+
const meshes: THREE.Mesh[] = []
|
|
157
|
+
|
|
158
|
+
// 투명 외장 (유리 쉘, 양단 막힘)
|
|
159
|
+
const shellGeo = new THREE.CylinderGeometry(radius, radius, this._batteryLen, 20)
|
|
160
|
+
shellGeo.rotateZ(Math.PI / 2)
|
|
161
|
+
const shellMat = new THREE.MeshStandardMaterial({
|
|
162
|
+
color: 0xffffff,
|
|
163
|
+
transparent: true,
|
|
164
|
+
opacity: 0.25,
|
|
165
|
+
roughness: 0.1,
|
|
166
|
+
metalness: 0.1,
|
|
167
|
+
side: THREE.DoubleSide,
|
|
168
|
+
depthWrite: false,
|
|
169
|
+
})
|
|
170
|
+
const shell = new THREE.Mesh(shellGeo, shellMat)
|
|
171
|
+
shell.position.set(0, this._batteryY, this._batteryZ)
|
|
172
|
+
meshes.push(shell)
|
|
173
|
+
|
|
174
|
+
// 좌측 캡 (금속)
|
|
175
|
+
const capGeo = new THREE.CylinderGeometry(radius * 0.6, radius, capWidth, 20)
|
|
176
|
+
capGeo.rotateZ(Math.PI / 2)
|
|
177
|
+
const capMat = new THREE.MeshStandardMaterial({
|
|
178
|
+
color: batteryColor, roughness: 0.3, metalness: 0.7,
|
|
179
|
+
emissive: batteryColor, emissiveIntensity: 0.2,
|
|
180
|
+
})
|
|
181
|
+
const leftCap = new THREE.Mesh(capGeo, capMat)
|
|
182
|
+
leftCap.position.set(-this._batteryLen / 2 - capWidth / 2, this._batteryY, this._batteryZ)
|
|
183
|
+
meshes.push(leftCap)
|
|
184
|
+
|
|
185
|
+
// 우측 캡 (금속)
|
|
186
|
+
const rightCapGeo = capGeo.clone()
|
|
187
|
+
const rightCap = new THREE.Mesh(rightCapGeo, capMat)
|
|
188
|
+
rightCap.position.set(this._batteryLen / 2 + capWidth / 2, this._batteryY, this._batteryZ)
|
|
189
|
+
rightCap.rotation.z = Math.PI
|
|
190
|
+
meshes.push(rightCap)
|
|
191
|
+
|
|
192
|
+
// 내부 잔량/빈 부분
|
|
193
|
+
const fillLen = Math.max(this._batteryLen * rate / 100, 0.1)
|
|
194
|
+
const emptyLen = Math.max(this._batteryLen - fillLen, 0.1)
|
|
195
|
+
|
|
196
|
+
// 빈 부분 (짙은 회색, 불투명, 양단 닫힘)
|
|
197
|
+
const emptyGeo = new THREE.CylinderGeometry(innerRadius, innerRadius, emptyLen, 16)
|
|
198
|
+
emptyGeo.rotateZ(Math.PI / 2)
|
|
199
|
+
const emptyMat = new THREE.MeshStandardMaterial({
|
|
200
|
+
color: 0x333333,
|
|
201
|
+
roughness: 0.6,
|
|
202
|
+
metalness: 0.2,
|
|
203
|
+
side: THREE.DoubleSide,
|
|
204
|
+
})
|
|
205
|
+
const emptyMesh = new THREE.Mesh(emptyGeo, emptyMat)
|
|
206
|
+
emptyMesh.position.set(
|
|
207
|
+
(this._batteryLen - emptyLen) / 2,
|
|
208
|
+
this._batteryY, this._batteryZ
|
|
209
|
+
)
|
|
210
|
+
meshes.push(emptyMesh)
|
|
211
|
+
|
|
212
|
+
// 잔량 채움 (상태 컬러)
|
|
213
|
+
const fillGeo = new THREE.CylinderGeometry(innerRadius * 0.98, innerRadius * 0.98, fillLen, 16)
|
|
214
|
+
fillGeo.rotateZ(Math.PI / 2)
|
|
215
|
+
const fillMat = new THREE.MeshStandardMaterial({
|
|
216
|
+
color: batteryColor,
|
|
217
|
+
roughness: 0.3,
|
|
218
|
+
metalness: 0.1,
|
|
219
|
+
emissive: batteryColor,
|
|
220
|
+
emissiveIntensity: 0.4,
|
|
221
|
+
side: THREE.DoubleSide,
|
|
222
|
+
})
|
|
223
|
+
this._batteryFillMesh = new THREE.Mesh(fillGeo, fillMat)
|
|
224
|
+
this._batteryFillMesh.position.set(
|
|
225
|
+
-(this._batteryLen - fillLen) / 2,
|
|
226
|
+
this._batteryY, this._batteryZ
|
|
227
|
+
)
|
|
228
|
+
meshes.push(this._batteryFillMesh)
|
|
229
|
+
|
|
230
|
+
return meshes
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
private getBatteryRate(): number {
|
|
234
|
+
const data = (this.component as any).data
|
|
235
|
+
return typeof data?.BATTERYRATE === 'number' ? data.BATTERYRATE : 0
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
private getBatteryColor(rate: number): number {
|
|
239
|
+
try {
|
|
240
|
+
const comp = this.component as any
|
|
241
|
+
if (comp.getBatteryColor) {
|
|
242
|
+
const color = comp.getBatteryColor(rate)
|
|
243
|
+
if (color) return new THREE.Color(color).getHex()
|
|
244
|
+
}
|
|
245
|
+
} catch { /* fallback */ }
|
|
246
|
+
// 기본: 녹색 > 노랑 > 빨강
|
|
247
|
+
if (rate > 50) return 0x4caf50
|
|
248
|
+
if (rate > 20) return 0xffc107
|
|
249
|
+
return 0xf44336
|
|
250
|
+
}
|
|
251
|
+
|
|
124
252
|
updateDimension() {}
|
|
125
253
|
|
|
126
254
|
update() {
|
|
@@ -150,12 +278,19 @@ export class AGV3D extends RealObjectGroup {
|
|
|
150
278
|
|
|
151
279
|
onchange(after: Record<string, unknown>, before: Record<string, unknown>) {
|
|
152
280
|
if ('width' in after || 'height' in after || 'depth' in after ||
|
|
153
|
-
'fillStyle' in after || 'strokeStyle' in after || 'id' in after
|
|
281
|
+
'fillStyle' in after || 'strokeStyle' in after || 'id' in after ||
|
|
282
|
+
'EMPTYTYPE' in after || 'CARRIERSTATUS' in after) {
|
|
154
283
|
this.update()
|
|
155
284
|
return
|
|
156
285
|
}
|
|
286
|
+
|
|
157
287
|
super.onchange(after, before)
|
|
158
288
|
}
|
|
289
|
+
|
|
290
|
+
private updateBattery() {
|
|
291
|
+
// 전체 rebuild로 정확한 geometry 생성
|
|
292
|
+
this.update()
|
|
293
|
+
}
|
|
159
294
|
}
|
|
160
295
|
|
|
161
296
|
registerRealObjectFactory('AGV', (component: Component) => new AGV3D(component))
|
|
@@ -30,7 +30,7 @@ interface MCS3DConfig {
|
|
|
30
30
|
/** statusColor 미사용 시 기본 색상 */
|
|
31
31
|
defaultColor: number
|
|
32
32
|
/** 'box' | 'hexagon' */
|
|
33
|
-
shape: 'box' | 'hexagon'
|
|
33
|
+
shape: 'box' | 'hexagon' | 'plane'
|
|
34
34
|
/** mesh 생성 전 가시성 판정 — false 반환 시 mesh 미생성 */
|
|
35
35
|
isVisible?: (component: Component) => boolean
|
|
36
36
|
/** 부모 대비 크기 비율 (기본 1.0). < 1이면 부모 중심에 배치. */
|
|
@@ -41,6 +41,10 @@ interface MCS3DConfig {
|
|
|
41
41
|
matteTop?: boolean
|
|
42
42
|
/** 상판에 2D 렌더링 텍스처 적용 */
|
|
43
43
|
texturedTop?: boolean
|
|
44
|
+
/** 자체 발광 강도 (0~1). 조명 약한 면에서도 색상 유지 */
|
|
45
|
+
emissiveIntensity?: number
|
|
46
|
+
/** 바닥면 유지 (기본 false — 바닥면 제거) */
|
|
47
|
+
keepBottomFace?: boolean
|
|
44
48
|
}
|
|
45
49
|
|
|
46
50
|
/**
|
|
@@ -163,7 +167,7 @@ class MCSRealObject3D extends RealObjectGroup {
|
|
|
163
167
|
|
|
164
168
|
private buildBoxGeometry(w: number, d: number, h: number): THREE.BufferGeometry {
|
|
165
169
|
const geo = new THREE.BoxGeometry(w, d, h)
|
|
166
|
-
return this.removeBottomFace(geo)
|
|
170
|
+
return this._config.keepBottomFace ? geo : this.removeBottomFace(geo)
|
|
167
171
|
}
|
|
168
172
|
|
|
169
173
|
private buildHexagonGeometry(w: number, d: number, h: number): THREE.BufferGeometry {
|
|
@@ -194,6 +198,12 @@ class MCSRealObject3D extends RealObjectGroup {
|
|
|
194
198
|
return this.removeBottomFace(geo)
|
|
195
199
|
}
|
|
196
200
|
|
|
201
|
+
private buildPlaneGeometry(w: number, h: number): THREE.BufferGeometry {
|
|
202
|
+
const geo = new THREE.PlaneGeometry(w, h)
|
|
203
|
+
geo.rotateX(-Math.PI / 2)
|
|
204
|
+
return geo
|
|
205
|
+
}
|
|
206
|
+
|
|
197
207
|
// ── Build ──
|
|
198
208
|
|
|
199
209
|
protected _ownMesh?: THREE.Mesh
|
|
@@ -217,19 +227,31 @@ class MCSRealObject3D extends RealObjectGroup {
|
|
|
217
227
|
const { opacity, metalness, roughness, shape } = this._config
|
|
218
228
|
|
|
219
229
|
const geometry =
|
|
220
|
-
shape === '
|
|
221
|
-
? this.
|
|
222
|
-
:
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
230
|
+
shape === 'plane'
|
|
231
|
+
? this.buildPlaneGeometry(w, h)
|
|
232
|
+
: shape === 'hexagon'
|
|
233
|
+
? this.buildHexagonGeometry(w, depth, h)
|
|
234
|
+
: this.buildBoxGeometry(w, depth, h)
|
|
235
|
+
|
|
236
|
+
const isPlane = shape === 'plane'
|
|
237
|
+
const material = isPlane
|
|
238
|
+
? new THREE.MeshBasicMaterial({
|
|
239
|
+
color,
|
|
240
|
+
opacity,
|
|
241
|
+
transparent: opacity < 1,
|
|
242
|
+
side: THREE.DoubleSide,
|
|
243
|
+
depthWrite: opacity >= 0.9,
|
|
244
|
+
})
|
|
245
|
+
: new THREE.MeshStandardMaterial({
|
|
246
|
+
color,
|
|
247
|
+
metalness,
|
|
248
|
+
roughness,
|
|
249
|
+
opacity,
|
|
250
|
+
transparent: opacity < 1,
|
|
251
|
+
side: opacity < 1 ? THREE.DoubleSide : THREE.FrontSide,
|
|
252
|
+
depthWrite: opacity >= 0.9,
|
|
253
|
+
...(this._config.emissiveIntensity ? { emissive: color, emissiveIntensity: this._config.emissiveIntensity } : {}),
|
|
254
|
+
})
|
|
233
255
|
// z-fighting 방지: 바닥면(floor grid)보다 항상 앞에 렌더링
|
|
234
256
|
material.polygonOffset = true
|
|
235
257
|
material.polygonOffsetFactor = -1
|
|
@@ -254,9 +276,9 @@ class MCSRealObject3D extends RealObjectGroup {
|
|
|
254
276
|
}
|
|
255
277
|
|
|
256
278
|
this._ownMesh = new THREE.Mesh(geometry, meshMaterial)
|
|
257
|
-
// sizeRatio < 1이면 부모 중심에 배치 (rawDepth/2),
|
|
258
|
-
this._ownMesh.position.y = ratio < 1 ? rawDepth / 2 : depth / 2
|
|
259
|
-
this._ownMesh.castShadow =
|
|
279
|
+
// sizeRatio < 1이면 부모 중심에 배치 (rawDepth/2), plane은 살짝 띄움
|
|
280
|
+
this._ownMesh.position.y = isPlane ? 1 : ratio < 1 ? rawDepth / 2 : depth / 2
|
|
281
|
+
this._ownMesh.castShadow = !isPlane
|
|
260
282
|
this._ownMesh.receiveShadow = true
|
|
261
283
|
this.object3d.add(this._ownMesh)
|
|
262
284
|
}
|
|
@@ -610,16 +632,16 @@ registerRealObjectFactory('STOCKER', (component: Component) => new StockerRealOb
|
|
|
610
632
|
// Unit (이동체 · 홀더) — 낮은 depth, 불투명
|
|
611
633
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
612
634
|
|
|
613
|
-
// Port:
|
|
635
|
+
// Port: 바닥판 (PlaneGeometry)
|
|
614
636
|
const PORT_CONFIG: MCS3DConfig = {
|
|
615
637
|
...DEFAULTS,
|
|
616
|
-
depthRatio: 0.
|
|
638
|
+
depthRatio: 0.01,
|
|
617
639
|
defaultZPosFromParent: true,
|
|
618
|
-
opacity:
|
|
619
|
-
metalness: 0.
|
|
620
|
-
roughness: 0.
|
|
640
|
+
opacity: 1,
|
|
641
|
+
metalness: 0.3,
|
|
642
|
+
roughness: 0.8,
|
|
621
643
|
defaultColor: 0xb0a090,
|
|
622
|
-
|
|
644
|
+
shape: 'plane',
|
|
623
645
|
}
|
|
624
646
|
registerRealObjectFactory('Port', (component: Component) => new MCSRealObject3D(component, PORT_CONFIG))
|
|
625
647
|
|
|
@@ -640,6 +662,7 @@ const CARRIER_CONFIG: MCS3DConfig = {
|
|
|
640
662
|
isVisible: (c: Component) => !!(c.state as any).id,
|
|
641
663
|
sizeRatio: 0.8,
|
|
642
664
|
texturedTop: true,
|
|
665
|
+
emissiveIntensity: 0.4,
|
|
643
666
|
}
|
|
644
667
|
|
|
645
668
|
class CarrierRealObject3D extends MCSRealObject3D {
|