@fmsim/machine 2.0.0-beta.5 → 2.0.0-beta.6
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 +2 -2
- package/dist/carrier.js.map +1 -1
- package/dist/factories/agv-3d.js +33 -6
- package/dist/factories/agv-3d.js.map +1 -1
- package/dist/factories/machine-3d.js +229 -29
- package/dist/factories/machine-3d.js.map +1 -1
- package/dist/factories/oht-3d.js +34 -8
- package/dist/factories/oht-3d.js.map +1 -1
- package/dist/factories/shuttle-3d.js +32 -6
- package/dist/factories/shuttle-3d.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -3
- package/src/carrier.ts +2 -2
- package/src/factories/agv-3d.ts +32 -6
- package/src/factories/machine-3d.ts +259 -15
- package/src/factories/oht-3d.ts +32 -8
- package/src/factories/shuttle-3d.ts +30 -6
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.6",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"module": "dist/index.js",
|
|
8
8
|
"things-scene": true,
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"migration": "things-factory-migration"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@fmsim/api": "^2.0.0-beta.
|
|
28
|
+
"@fmsim/api": "^2.0.0-beta.6",
|
|
29
29
|
"@hatiolab/things-scene": "^10.0.0-beta.1"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
@@ -54,5 +54,5 @@
|
|
|
54
54
|
"prettier --write"
|
|
55
55
|
]
|
|
56
56
|
},
|
|
57
|
-
"gitHead": "
|
|
57
|
+
"gitHead": "879225fef10fca7f9084e4c27d23744337eaa06b"
|
|
58
58
|
}
|
package/src/carrier.ts
CHANGED
|
@@ -134,11 +134,11 @@ export default class Carrier extends MCSStatusMixin(ParentObjectMixin(Shape)) {
|
|
|
134
134
|
// 문자열 형태의 설정들을 미리 파싱
|
|
135
135
|
const parsedTheme = {}
|
|
136
136
|
for (const [status, config] of Object.entries(theme)) {
|
|
137
|
-
if (typeof config === 'string') {
|
|
137
|
+
if (typeof config === 'string' && config.trim()) {
|
|
138
138
|
try {
|
|
139
139
|
parsedTheme[status] = JSON5.parse(config)
|
|
140
140
|
} catch (e) {
|
|
141
|
-
console.error('JSON5 parse error:', e)
|
|
141
|
+
console.error('JSON5 parse error:', e, 'input:', JSON.stringify(config), 'status:', status, 'theme:', carrierAnimationName)
|
|
142
142
|
parsedTheme[status] = config
|
|
143
143
|
}
|
|
144
144
|
} else {
|
package/src/factories/agv-3d.ts
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
import * as THREE from 'three'
|
|
17
17
|
import { RoundedBoxGeometry } from 'three/examples/jsm/geometries/RoundedBoxGeometry.js'
|
|
18
|
-
import { RealObjectGroup, registerRealObjectFactory } from '@hatiolab/things-scene'
|
|
18
|
+
import { RealObjectGroup, registerRealObjectFactory, disposeObject3D } from '@hatiolab/things-scene'
|
|
19
19
|
import type { Component } from '@hatiolab/things-scene'
|
|
20
20
|
import { TRANSPORT_SURFACE_HEIGHT } from './machine-3d.js'
|
|
21
21
|
|
|
@@ -24,6 +24,7 @@ const PLATFORM_COLOR = 0x333333
|
|
|
24
24
|
const DEFAULT_BODY_COLOR = 0x6688cc
|
|
25
25
|
|
|
26
26
|
export class AGV3D extends RealObjectGroup {
|
|
27
|
+
private _ownMeshes: THREE.Mesh[] = []
|
|
27
28
|
get effectiveDepth(): number {
|
|
28
29
|
const { depth } = this.component.state
|
|
29
30
|
return depth || TRANSPORT_SURFACE_HEIGHT
|
|
@@ -88,7 +89,6 @@ export class AGV3D extends RealObjectGroup {
|
|
|
88
89
|
bumper.position.y = bumperH / 2
|
|
89
90
|
bumper.castShadow = true
|
|
90
91
|
bumper.receiveShadow = true
|
|
91
|
-
this.object3d.add(bumper)
|
|
92
92
|
|
|
93
93
|
// ── 본체 (둥근 모서리, statusColor 적용) ──
|
|
94
94
|
const bodyColor = this.resolveColor()
|
|
@@ -102,7 +102,6 @@ export class AGV3D extends RealObjectGroup {
|
|
|
102
102
|
body.position.y = bumperH + bodyH / 2
|
|
103
103
|
body.castShadow = true
|
|
104
104
|
body.receiveShadow = true
|
|
105
|
-
this.object3d.add(body)
|
|
106
105
|
|
|
107
106
|
// ── 상단 플랫폼 (본체보다 안쪽, 어두운색) ──
|
|
108
107
|
const platInset = Math.min(w, h) * 0.04
|
|
@@ -117,14 +116,41 @@ export class AGV3D extends RealObjectGroup {
|
|
|
117
116
|
plat.position.y = bumperH + bodyH + gapH + platH / 2
|
|
118
117
|
plat.castShadow = true
|
|
119
118
|
plat.receiveShadow = true
|
|
120
|
-
|
|
119
|
+
|
|
120
|
+
this._ownMeshes = [bumper, body, plat]
|
|
121
|
+
for (const m of this._ownMeshes) this.object3d.add(m)
|
|
121
122
|
}
|
|
122
123
|
|
|
123
124
|
updateDimension() {}
|
|
124
|
-
|
|
125
|
+
|
|
126
|
+
update() {
|
|
127
|
+
// 자신의 mesh만 제거 (Carrier 등 자식 컴포넌트 3D 보존)
|
|
128
|
+
for (const m of this._ownMeshes) {
|
|
129
|
+
this.object3d.remove(m)
|
|
130
|
+
disposeObject3D(m)
|
|
131
|
+
}
|
|
132
|
+
this._ownMeshes = []
|
|
133
|
+
|
|
134
|
+
this.build()
|
|
135
|
+
this.updateTransform()
|
|
136
|
+
this.updateAlpha()
|
|
137
|
+
this.updateHidden()
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
updateAlpha() {
|
|
141
|
+
const { alpha = 1 } = this.component.state
|
|
142
|
+
for (const m of this._ownMeshes) {
|
|
143
|
+
const materials = Array.isArray(m.material) ? m.material : [m.material]
|
|
144
|
+
for (const mat of materials) {
|
|
145
|
+
;(mat as THREE.MeshStandardMaterial).opacity = alpha
|
|
146
|
+
;(mat as THREE.MeshStandardMaterial).transparent = alpha < 1
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
125
150
|
|
|
126
151
|
onchange(after: Record<string, unknown>, before: Record<string, unknown>) {
|
|
127
|
-
if ('width' in after || 'height' in after || 'depth' in after ||
|
|
152
|
+
if ('width' in after || 'height' in after || 'depth' in after ||
|
|
153
|
+
'fillStyle' in after || 'strokeStyle' in after || 'id' in after) {
|
|
128
154
|
this.update()
|
|
129
155
|
return
|
|
130
156
|
}
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import * as THREE from 'three'
|
|
12
|
-
import { RealObjectGroup, registerRealObjectFactory } from '@hatiolab/things-scene'
|
|
12
|
+
import { RealObjectGroup, registerRealObjectFactory, disposeObject3D } from '@hatiolab/things-scene'
|
|
13
13
|
import type { Component } from '@hatiolab/things-scene'
|
|
14
14
|
import { LEGEND_CAPACITY } from '../features/mcs-status-default'
|
|
15
15
|
import { getVaueOnRanges } from '../utils/get-value-on-ranges'
|
|
@@ -35,6 +35,12 @@ interface MCS3DConfig {
|
|
|
35
35
|
isVisible?: (component: Component) => boolean
|
|
36
36
|
/** 부모 대비 크기 비율 (기본 1.0). < 1이면 부모 중심에 배치. */
|
|
37
37
|
sizeRatio?: number
|
|
38
|
+
/** 상부 면 제거 (5면 개방 박스) */
|
|
39
|
+
openTop?: boolean
|
|
40
|
+
/** 상판 무반사 (MeshBasicMaterial) */
|
|
41
|
+
matteTop?: boolean
|
|
42
|
+
/** 상판에 2D 렌더링 텍스처 적용 */
|
|
43
|
+
texturedTop?: boolean
|
|
38
44
|
}
|
|
39
45
|
|
|
40
46
|
/**
|
|
@@ -94,7 +100,7 @@ class MCSRealObject3D extends RealObjectGroup {
|
|
|
94
100
|
}
|
|
95
101
|
|
|
96
102
|
// 컴포넌트의 상태 색상(statusColor) 또는 fillStyle → Three.js 색상 변환
|
|
97
|
-
|
|
103
|
+
protected resolveColor(): number {
|
|
98
104
|
const comp = this.component as any
|
|
99
105
|
|
|
100
106
|
// 1) MCSStatusMixin.statusColor (legend 기반 상태 색상)
|
|
@@ -190,6 +196,8 @@ class MCSRealObject3D extends RealObjectGroup {
|
|
|
190
196
|
|
|
191
197
|
// ── Build ──
|
|
192
198
|
|
|
199
|
+
protected _ownMesh?: THREE.Mesh
|
|
200
|
+
|
|
193
201
|
build() {
|
|
194
202
|
super.build()
|
|
195
203
|
|
|
@@ -227,16 +235,36 @@ class MCSRealObject3D extends RealObjectGroup {
|
|
|
227
235
|
material.polygonOffsetFactor = -1
|
|
228
236
|
material.polygonOffsetUnit = -1
|
|
229
237
|
|
|
230
|
-
|
|
238
|
+
// BoxGeometry material groups: +x(0), -x(1), +y(2), -y(3), +z(4), -z(5)
|
|
239
|
+
let meshMaterial: THREE.Material | THREE.Material[] = material
|
|
240
|
+
if (shape === 'box') {
|
|
241
|
+
if (this._config.openTop) {
|
|
242
|
+
// 상단면 제거 → 5면 개방 박스
|
|
243
|
+
const invisible = new THREE.MeshBasicMaterial({ visible: false })
|
|
244
|
+
meshMaterial = [material, material, invisible, material, material, material]
|
|
245
|
+
} else if (this._config.texturedTop) {
|
|
246
|
+
// 상판에 2D 렌더링 텍스처
|
|
247
|
+
const topMat = this.buildTopTexture(w, h)
|
|
248
|
+
meshMaterial = [material, material, topMat, material, material, material]
|
|
249
|
+
} else if (this._config.matteTop) {
|
|
250
|
+
// 상판만 무반사
|
|
251
|
+
const matte = new THREE.MeshBasicMaterial({ color, opacity, transparent: opacity < 1 })
|
|
252
|
+
meshMaterial = [material, material, matte, material, material, material]
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
this._ownMesh = new THREE.Mesh(geometry, meshMaterial)
|
|
231
257
|
// sizeRatio < 1이면 부모 중심에 배치 (rawDepth/2), 아니면 자체 중심 (depth/2)
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
this.object3d.add(
|
|
258
|
+
this._ownMesh.position.y = ratio < 1 ? rawDepth / 2 : depth / 2
|
|
259
|
+
this._ownMesh.castShadow = true
|
|
260
|
+
this._ownMesh.receiveShadow = true
|
|
261
|
+
this.object3d.add(this._ownMesh)
|
|
236
262
|
}
|
|
237
263
|
|
|
238
264
|
// ── Change handling ──
|
|
239
265
|
|
|
266
|
+
private _updatePending = false
|
|
267
|
+
|
|
240
268
|
onchange(after: Record<string, unknown>, before: Record<string, unknown>) {
|
|
241
269
|
if (
|
|
242
270
|
'width' in after ||
|
|
@@ -244,18 +272,117 @@ class MCSRealObject3D extends RealObjectGroup {
|
|
|
244
272
|
'depth' in after ||
|
|
245
273
|
'fillStyle' in after ||
|
|
246
274
|
'strokeStyle' in after ||
|
|
247
|
-
'id' in after
|
|
275
|
+
'id' in after ||
|
|
276
|
+
'EMPTYTYPE' in after ||
|
|
277
|
+
'CARRIERSTATUS' in after
|
|
248
278
|
) {
|
|
249
|
-
|
|
279
|
+
// 연속 setState에 의한 다중 rebuild 방지: 한 프레임에 한 번만 실행
|
|
280
|
+
if (!this._updatePending) {
|
|
281
|
+
this._updatePending = true
|
|
282
|
+
queueMicrotask(() => {
|
|
283
|
+
this._updatePending = false
|
|
284
|
+
this.update()
|
|
285
|
+
})
|
|
286
|
+
}
|
|
250
287
|
return
|
|
251
288
|
}
|
|
252
289
|
|
|
253
290
|
super.onchange(after, before)
|
|
254
291
|
}
|
|
255
292
|
|
|
256
|
-
|
|
293
|
+
/**
|
|
294
|
+
* 상판 텍스처: 컴포넌트의 2D render()를 캔버스에 그려서 텍스처로 사용
|
|
295
|
+
*/
|
|
296
|
+
protected buildTopTexture(w: number, h: number): THREE.MeshBasicMaterial {
|
|
297
|
+
const scale = 4
|
|
298
|
+
const cw = this.nextPow2(w * scale)
|
|
299
|
+
const ch = this.nextPow2(h * scale)
|
|
300
|
+
const canvas = document.createElement('canvas')
|
|
301
|
+
canvas.width = cw
|
|
302
|
+
canvas.height = ch
|
|
303
|
+
const ctx = canvas.getContext('2d')!
|
|
304
|
+
|
|
305
|
+
const statusColor = '#' + this.resolveColor().toString(16).padStart(6, '0')
|
|
306
|
+
const emptyType = (this.component.state as any)?.EMPTYTYPE || ''
|
|
307
|
+
const text = emptyType === 'FULL' ? 'F' : emptyType === 'EMPTY' ? 'E' : emptyType === 'EMPTYEMPTY' ? 'X' : ''
|
|
308
|
+
|
|
309
|
+
// 실제 사용 영역 (nextPow2 이전 크기)
|
|
310
|
+
const usedW = w * scale
|
|
311
|
+
const usedH = h * scale
|
|
312
|
+
|
|
313
|
+
// 바탕: 캐리어 상태 색상 (사용 영역만)
|
|
314
|
+
ctx.fillStyle = statusColor
|
|
315
|
+
ctx.fillRect(0, 0, usedW, usedH)
|
|
316
|
+
|
|
317
|
+
// 텍스트 (사용 영역 중앙)
|
|
318
|
+
if (text) {
|
|
319
|
+
const fontSize = Math.round(Math.min(usedW, usedH) * 0.7)
|
|
320
|
+
ctx.font = `bold ${fontSize}px Arial`
|
|
321
|
+
ctx.textAlign = 'center'
|
|
322
|
+
ctx.textBaseline = 'middle'
|
|
323
|
+
ctx.fillStyle = '#000000'
|
|
324
|
+
ctx.fillText(text, usedW / 2, usedH / 2)
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const texture = new THREE.CanvasTexture(canvas)
|
|
328
|
+
texture.offset.set(0, 1 - usedH / ch)
|
|
329
|
+
texture.repeat.set(usedW / cw, usedH / ch)
|
|
330
|
+
texture.colorSpace = THREE.SRGBColorSpace
|
|
331
|
+
|
|
332
|
+
return new THREE.MeshBasicMaterial({
|
|
333
|
+
map: texture,
|
|
334
|
+
transparent: true,
|
|
335
|
+
side: THREE.DoubleSide,
|
|
336
|
+
})
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
private nextPow2(n: number): number {
|
|
340
|
+
if (!(n > 0)) return 1
|
|
341
|
+
let v = Math.ceil(n)
|
|
342
|
+
v--
|
|
343
|
+
v |= v >> 1; v |= v >> 2; v |= v >> 4; v |= v >> 8; v |= v >> 16
|
|
344
|
+
return v + 1
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* 자신의 mesh만 제거 후 rebuild.
|
|
349
|
+
* 기본 update()는 clear()로 object3d의 모든 children을 제거하는데,
|
|
350
|
+
* 컨테이너(Port 등)의 경우 자식 컴포넌트(Carrier 등)의 3D 오브젝트도 함께 제거됨.
|
|
351
|
+
* 이를 방지하기 위해 자신이 생성한 mesh만 제거한다.
|
|
352
|
+
*/
|
|
353
|
+
update() {
|
|
354
|
+
// 자신의 mesh만 제거 (자식 컴포넌트 3D 오브젝트 보존)
|
|
355
|
+
if (this._ownMesh) {
|
|
356
|
+
this.object3d.remove(this._ownMesh)
|
|
357
|
+
disposeObject3D(this._ownMesh)
|
|
358
|
+
this._ownMesh = undefined
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
this.build()
|
|
362
|
+
this.updateTransform()
|
|
363
|
+
this.updateAlpha()
|
|
364
|
+
this.updateHidden()
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// RealObjectGroup 기본 동작 비활성화
|
|
257
368
|
updateDimension() {}
|
|
258
|
-
|
|
369
|
+
|
|
370
|
+
updateAlpha() {
|
|
371
|
+
const { alpha = 1 } = this.component.state
|
|
372
|
+
const configOpacity = this._config.opacity
|
|
373
|
+
const effective = configOpacity * alpha
|
|
374
|
+
|
|
375
|
+
this.object3d.traverse((o: any) => {
|
|
376
|
+
if (!o.material) return
|
|
377
|
+
const materials = Array.isArray(o.material) ? o.material : [o.material]
|
|
378
|
+
for (const m of materials) {
|
|
379
|
+
if (!m.visible) continue // openTop invisible face 무시
|
|
380
|
+
if (m.map) continue // 텍스처 material은 자체 transparent 유지
|
|
381
|
+
m.opacity = effective
|
|
382
|
+
m.transparent = effective < 1
|
|
383
|
+
}
|
|
384
|
+
})
|
|
385
|
+
}
|
|
259
386
|
}
|
|
260
387
|
|
|
261
388
|
// ── 팩토리 등록 헬퍼 ─────────────────────────────────────
|
|
@@ -483,14 +610,18 @@ registerRealObjectFactory('STOCKER', (component: Component) => new StockerRealOb
|
|
|
483
610
|
// Unit (이동체 · 홀더) — 낮은 depth, 불투명
|
|
484
611
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
485
612
|
|
|
486
|
-
|
|
613
|
+
// Port: 상부 개방 5면 박스
|
|
614
|
+
const PORT_CONFIG: MCS3DConfig = {
|
|
615
|
+
...DEFAULTS,
|
|
487
616
|
depthRatio: 0.5,
|
|
488
617
|
defaultZPosFromParent: true,
|
|
489
618
|
opacity: 0.5,
|
|
490
619
|
metalness: 0.4,
|
|
491
620
|
roughness: 0.6,
|
|
492
621
|
defaultColor: 0xb0a090,
|
|
493
|
-
|
|
622
|
+
openTop: true,
|
|
623
|
+
}
|
|
624
|
+
registerRealObjectFactory('Port', (component: Component) => new MCSRealObject3D(component, PORT_CONFIG))
|
|
494
625
|
|
|
495
626
|
register('Shelf', {
|
|
496
627
|
depthRatio: 0.25,
|
|
@@ -499,14 +630,127 @@ register('Shelf', {
|
|
|
499
630
|
defaultColor: 0xaabbcc,
|
|
500
631
|
})
|
|
501
632
|
|
|
502
|
-
|
|
633
|
+
// Carrier: CARRIERSTATUS에 따른 3D 애니메이션 지원
|
|
634
|
+
const CARRIER_CONFIG: MCS3DConfig = {
|
|
635
|
+
...DEFAULTS,
|
|
503
636
|
depthRatio: 0.5,
|
|
504
637
|
metalness: 0.2,
|
|
505
638
|
roughness: 0.8,
|
|
506
639
|
defaultColor: 0x66bb66,
|
|
507
640
|
isVisible: (c: Component) => !!(c.state as any).id,
|
|
508
641
|
sizeRatio: 0.8,
|
|
509
|
-
|
|
642
|
+
texturedTop: true,
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
class CarrierRealObject3D extends MCSRealObject3D {
|
|
646
|
+
private _animRaf: number | null = null
|
|
647
|
+
private _animStartTime = 0
|
|
648
|
+
|
|
649
|
+
constructor(component: Component) {
|
|
650
|
+
super(component, CARRIER_CONFIG)
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
build() {
|
|
654
|
+
super.build()
|
|
655
|
+
this.startCarrierAnimation()
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
/**
|
|
659
|
+
* CARRIERSTATUS의 애니메이션 설정을 읽어 3D에서 직접 실행.
|
|
660
|
+
* things-scene의 delta 시스템과 독립적으로 동작.
|
|
661
|
+
*/
|
|
662
|
+
private startCarrierAnimation() {
|
|
663
|
+
this.stopCarrierAnimation()
|
|
664
|
+
|
|
665
|
+
const comp = this.component as any
|
|
666
|
+
if (!comp.app?.isViewMode) return
|
|
667
|
+
|
|
668
|
+
const carrierStatus = comp.state?.CARRIERSTATUS
|
|
669
|
+
if (!carrierStatus) return
|
|
670
|
+
|
|
671
|
+
// 애니메이션 설정 가져오기
|
|
672
|
+
const config = comp.getAnimationConfig?.(carrierStatus)
|
|
673
|
+
if (!config?.animation) return
|
|
674
|
+
|
|
675
|
+
const { type, duration = 2000, scale: animScale = 1.3 } = config.animation
|
|
676
|
+
if (!type) return
|
|
677
|
+
|
|
678
|
+
this._animStartTime = performance.now()
|
|
679
|
+
|
|
680
|
+
// 애니메이션 대상: group(object3d) — mesh 개별 변환 대신 group 전체를 변환하여
|
|
681
|
+
// 회전/스케일 중심이 component의 3D 위치(= 2D 중심)와 정확히 일치하도록 함
|
|
682
|
+
const obj = this.object3d
|
|
683
|
+
|
|
684
|
+
const animate = () => {
|
|
685
|
+
if (!this._ownMesh) return
|
|
686
|
+
|
|
687
|
+
const elapsed = performance.now() - this._animStartTime
|
|
688
|
+
const progress = (elapsed % duration) / duration // 0~1 반복
|
|
689
|
+
|
|
690
|
+
switch (type) {
|
|
691
|
+
case 'heartbeat': {
|
|
692
|
+
const t = progress < 0.5 ? progress * 2 : (1 - progress) * 2
|
|
693
|
+
const s = 1 + (animScale - 1) * t
|
|
694
|
+
obj.scale.set(s, 1, s)
|
|
695
|
+
break
|
|
696
|
+
}
|
|
697
|
+
case 'rotation': {
|
|
698
|
+
obj.rotation.y = progress * Math.PI * 2
|
|
699
|
+
break
|
|
700
|
+
}
|
|
701
|
+
case 'vibration': {
|
|
702
|
+
const theta = config.animation.theta ?? 0.26
|
|
703
|
+
const current = Math.sin(progress * Math.PI * 2) * theta
|
|
704
|
+
obj.rotation.y = current
|
|
705
|
+
break
|
|
706
|
+
}
|
|
707
|
+
case 'fade': {
|
|
708
|
+
const alpha = 0.3 + 0.7 * (progress < 0.5 ? progress * 2 : (1 - progress) * 2)
|
|
709
|
+
obj.traverse((o: any) => {
|
|
710
|
+
if (o.material && !o.material.map) {
|
|
711
|
+
o.material.opacity = alpha
|
|
712
|
+
o.material.transparent = true
|
|
713
|
+
}
|
|
714
|
+
})
|
|
715
|
+
break
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
this._animRaf = requestAnimationFrame(animate)
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
this._animRaf = requestAnimationFrame(animate)
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
// 자체 애니메이션이 실행 중이면 things-scene의 syncAnimationDelta 비활성화
|
|
726
|
+
syncAnimationDelta() {
|
|
727
|
+
if (!this._animRaf) {
|
|
728
|
+
super.syncAnimationDelta()
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
private stopCarrierAnimation() {
|
|
733
|
+
if (this._animRaf) {
|
|
734
|
+
cancelAnimationFrame(this._animRaf)
|
|
735
|
+
this._animRaf = null
|
|
736
|
+
}
|
|
737
|
+
// group 스케일/회전 초기화
|
|
738
|
+
this.object3d.scale.set(1, 1, 1)
|
|
739
|
+
this.object3d.rotation.y = 0
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
update() {
|
|
743
|
+
this.stopCarrierAnimation()
|
|
744
|
+
super.update()
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
clear() {
|
|
748
|
+
this.stopCarrierAnimation()
|
|
749
|
+
return super.clear()
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
registerRealObjectFactory('Carrier', (component: Component) => new CarrierRealObject3D(component))
|
|
510
754
|
|
|
511
755
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
512
756
|
// Gauge (용량 게이지) — 얇은 바
|
package/src/factories/oht-3d.ts
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
import * as THREE from 'three'
|
|
13
13
|
import * as BufferGeometryUtils from 'three/examples/jsm/utils/BufferGeometryUtils.js'
|
|
14
14
|
import { RoundedBoxGeometry } from 'three/examples/jsm/geometries/RoundedBoxGeometry.js'
|
|
15
|
-
import { RealObjectGroup, registerRealObjectFactory } from '@hatiolab/things-scene'
|
|
15
|
+
import { RealObjectGroup, registerRealObjectFactory, disposeObject3D } from '@hatiolab/things-scene'
|
|
16
16
|
import type { Component } from '@hatiolab/things-scene'
|
|
17
17
|
import { TRANSPORT_SURFACE_HEIGHT } from './machine-3d.js'
|
|
18
18
|
|
|
@@ -24,6 +24,7 @@ const DEFAULT_BODY_COLOR = 0x6688cc
|
|
|
24
24
|
const OHT_RAIL_HEIGHT = 6 * TRANSPORT_SURFACE_HEIGHT
|
|
25
25
|
|
|
26
26
|
export class OHT3D extends RealObjectGroup {
|
|
27
|
+
private _ownMeshes: THREE.Mesh[] = []
|
|
27
28
|
get effectiveDepth(): number {
|
|
28
29
|
const { depth } = this.component.state
|
|
29
30
|
return depth || TRANSPORT_SURFACE_HEIGHT
|
|
@@ -89,7 +90,6 @@ export class OHT3D extends RealObjectGroup {
|
|
|
89
90
|
const gripperMesh = new THREE.Mesh(gripperGeo, frameMat)
|
|
90
91
|
gripperMesh.position.y = gripperBarH / 2
|
|
91
92
|
gripperMesh.castShadow = true
|
|
92
|
-
this.object3d.add(gripperMesh)
|
|
93
93
|
|
|
94
94
|
// ── 2. 호이스트 암 (좌우 수직 기둥 + 하단 가로연결) ──
|
|
95
95
|
const armBaseY = gripperBarH
|
|
@@ -111,7 +111,6 @@ export class OHT3D extends RealObjectGroup {
|
|
|
111
111
|
|
|
112
112
|
const armMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(armGeometries), frameMat)
|
|
113
113
|
armMesh.castShadow = true
|
|
114
|
-
this.object3d.add(armMesh)
|
|
115
114
|
|
|
116
115
|
// ── 3. 본체 하우징 (statusColor 적용) ──
|
|
117
116
|
const bodyBaseY = armBaseY + armH
|
|
@@ -126,7 +125,6 @@ export class OHT3D extends RealObjectGroup {
|
|
|
126
125
|
bodyMesh.position.y = bodyBaseY + bodyH / 2
|
|
127
126
|
bodyMesh.castShadow = true
|
|
128
127
|
bodyMesh.receiveShadow = true
|
|
129
|
-
this.object3d.add(bodyMesh)
|
|
130
128
|
|
|
131
129
|
// ── 4. 상단 트롤리 플레이트 (납작하고 넓은 레일 클램프) ──
|
|
132
130
|
const trolleyBaseY = bodyBaseY + bodyH + gap
|
|
@@ -141,27 +139,53 @@ export class OHT3D extends RealObjectGroup {
|
|
|
141
139
|
const trolleyMesh = new THREE.Mesh(trolleyGeo, trolleyMat)
|
|
142
140
|
trolleyMesh.position.y = trolleyBaseY + trolleyH / 2
|
|
143
141
|
trolleyMesh.castShadow = true
|
|
144
|
-
this.object3d.add(trolleyMesh)
|
|
145
142
|
|
|
146
143
|
// ── 5. 좌우 바퀴 (트롤리 위, 레일을 타는 원통형 바퀴) ──
|
|
147
144
|
const wheelBaseY = trolleyBaseY + trolleyH
|
|
148
145
|
const wheelRadius = wheelH / 2
|
|
149
146
|
const wheelDepth = h * 0.2
|
|
147
|
+
const wheelMeshes: THREE.Mesh[] = []
|
|
150
148
|
for (const xSign of [-1, 1]) {
|
|
151
149
|
const wheelGeo = new THREE.CylinderGeometry(wheelRadius, wheelRadius, wheelDepth, 12)
|
|
152
150
|
wheelGeo.rotateX(Math.PI / 2)
|
|
153
151
|
const wheelMesh = new THREE.Mesh(wheelGeo, trolleyMat)
|
|
154
152
|
wheelMesh.position.set(xSign * (trolleyW * 0.35), wheelBaseY + wheelRadius, 0)
|
|
155
153
|
wheelMesh.castShadow = true
|
|
156
|
-
|
|
154
|
+
wheelMeshes.push(wheelMesh)
|
|
157
155
|
}
|
|
156
|
+
|
|
157
|
+
this._ownMeshes = [gripperMesh, armMesh, bodyMesh, trolleyMesh, ...wheelMeshes]
|
|
158
|
+
for (const m of this._ownMeshes) this.object3d.add(m)
|
|
158
159
|
}
|
|
159
160
|
|
|
160
161
|
updateDimension() {}
|
|
161
|
-
|
|
162
|
+
|
|
163
|
+
update() {
|
|
164
|
+
for (const m of this._ownMeshes) {
|
|
165
|
+
this.object3d.remove(m)
|
|
166
|
+
disposeObject3D(m)
|
|
167
|
+
}
|
|
168
|
+
this._ownMeshes = []
|
|
169
|
+
this.build()
|
|
170
|
+
this.updateTransform()
|
|
171
|
+
this.updateAlpha()
|
|
172
|
+
this.updateHidden()
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
updateAlpha() {
|
|
176
|
+
const { alpha = 1 } = this.component.state
|
|
177
|
+
for (const m of this._ownMeshes) {
|
|
178
|
+
const materials = Array.isArray(m.material) ? m.material : [m.material]
|
|
179
|
+
for (const mat of materials) {
|
|
180
|
+
;(mat as THREE.MeshStandardMaterial).opacity = alpha
|
|
181
|
+
;(mat as THREE.MeshStandardMaterial).transparent = alpha < 1
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
162
185
|
|
|
163
186
|
onchange(after: Record<string, unknown>, before: Record<string, unknown>) {
|
|
164
|
-
if ('width' in after || 'height' in after || 'depth' in after ||
|
|
187
|
+
if ('width' in after || 'height' in after || 'depth' in after ||
|
|
188
|
+
'fillStyle' in after || 'strokeStyle' in after || 'id' in after) {
|
|
165
189
|
this.update()
|
|
166
190
|
return
|
|
167
191
|
}
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
import * as THREE from 'three'
|
|
14
14
|
import * as BufferGeometryUtils from 'three/examples/jsm/utils/BufferGeometryUtils.js'
|
|
15
|
-
import { RealObjectGroup, registerRealObjectFactory } from '@hatiolab/things-scene'
|
|
15
|
+
import { RealObjectGroup, registerRealObjectFactory, disposeObject3D } from '@hatiolab/things-scene'
|
|
16
16
|
import type { Component } from '@hatiolab/things-scene'
|
|
17
17
|
import { TRANSPORT_SURFACE_HEIGHT } from './machine-3d.js'
|
|
18
18
|
|
|
@@ -21,6 +21,7 @@ const FORK_COLOR = 0x777777
|
|
|
21
21
|
const DEFAULT_BODY_COLOR = 0x6688cc
|
|
22
22
|
|
|
23
23
|
export class Shuttle3D extends RealObjectGroup {
|
|
24
|
+
private _ownMeshes: THREE.Mesh[] = []
|
|
24
25
|
get effectiveDepth(): number {
|
|
25
26
|
const { depth } = this.component.state
|
|
26
27
|
return depth || TRANSPORT_SURFACE_HEIGHT
|
|
@@ -80,7 +81,6 @@ export class Shuttle3D extends RealObjectGroup {
|
|
|
80
81
|
baseMesh.position.y = baseH / 2
|
|
81
82
|
baseMesh.castShadow = true
|
|
82
83
|
baseMesh.receiveShadow = true
|
|
83
|
-
this.object3d.add(baseMesh)
|
|
84
84
|
|
|
85
85
|
// ── 2. 좌우 마스트 기둥 (statusColor 적용) ──
|
|
86
86
|
const mastBaseY = baseH
|
|
@@ -109,7 +109,6 @@ export class Shuttle3D extends RealObjectGroup {
|
|
|
109
109
|
|
|
110
110
|
const mastMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(mastGeometries), mastMat)
|
|
111
111
|
mastMesh.castShadow = true
|
|
112
|
-
this.object3d.add(mastMesh)
|
|
113
112
|
|
|
114
113
|
// ── 3. 상단 포크 (2개의 평행 암 + 백 플레이트) ──
|
|
115
114
|
const forkBaseY = mastBaseY + mastH
|
|
@@ -141,14 +140,39 @@ export class Shuttle3D extends RealObjectGroup {
|
|
|
141
140
|
|
|
142
141
|
const forkMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(forkGeometries), forkMat)
|
|
143
142
|
forkMesh.castShadow = true
|
|
144
|
-
|
|
143
|
+
|
|
144
|
+
this._ownMeshes = [baseMesh, mastMesh, forkMesh]
|
|
145
|
+
for (const m of this._ownMeshes) this.object3d.add(m)
|
|
145
146
|
}
|
|
146
147
|
|
|
147
148
|
updateDimension() {}
|
|
148
|
-
|
|
149
|
+
|
|
150
|
+
update() {
|
|
151
|
+
for (const m of this._ownMeshes) {
|
|
152
|
+
this.object3d.remove(m)
|
|
153
|
+
disposeObject3D(m)
|
|
154
|
+
}
|
|
155
|
+
this._ownMeshes = []
|
|
156
|
+
this.build()
|
|
157
|
+
this.updateTransform()
|
|
158
|
+
this.updateAlpha()
|
|
159
|
+
this.updateHidden()
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
updateAlpha() {
|
|
163
|
+
const { alpha = 1 } = this.component.state
|
|
164
|
+
for (const m of this._ownMeshes) {
|
|
165
|
+
const materials = Array.isArray(m.material) ? m.material : [m.material]
|
|
166
|
+
for (const mat of materials) {
|
|
167
|
+
;(mat as THREE.MeshStandardMaterial).opacity = alpha
|
|
168
|
+
;(mat as THREE.MeshStandardMaterial).transparent = alpha < 1
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
149
172
|
|
|
150
173
|
onchange(after: Record<string, unknown>, before: Record<string, unknown>) {
|
|
151
|
-
if ('width' in after || 'height' in after || 'depth' in after ||
|
|
174
|
+
if ('width' in after || 'height' in after || 'depth' in after ||
|
|
175
|
+
'fillStyle' in after || 'strokeStyle' in after || 'id' in after) {
|
|
152
176
|
this.update()
|
|
153
177
|
return
|
|
154
178
|
}
|