@fmsim/machine 2.0.0-beta.3 → 2.0.0-beta.30
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-manager.js +92 -0
- package/dist/carrier-manager.js.map +1 -0
- package/dist/carrier.js +51 -6
- 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 +8 -0
- package/dist/conveyor.js.map +1 -1
- package/dist/factories/agv-3d.js +36 -8
- package/dist/factories/agv-3d.js.map +1 -1
- package/dist/factories/conveyor-3d.js +159 -69
- package/dist/factories/conveyor-3d.js.map +1 -1
- package/dist/factories/machine-3d.js +604 -82
- package/dist/factories/machine-3d.js.map +1 -1
- package/dist/factories/oht-3d.js +50 -24
- 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/index.js +50 -34
- package/dist/index.js.map +1 -1
- package/dist/mcs-carrier-holder.js +39 -22
- package/dist/mcs-carrier-holder.js.map +1 -1
- package/dist/node.js +1 -1
- package/dist/node.js.map +1 -1
- package/dist/scanner.js +30 -0
- package/dist/scanner.js.map +1 -0
- package/dist/scene/root-container-override.js +9 -0
- package/dist/scene/root-container-override.js.map +1 -1
- package/dist/templates/index.js +2 -0
- 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/jest.config.js +10 -0
- package/package.json +4 -3
- package/src/carrier-manager.ts +116 -0
- package/src/carrier.ts +50 -4
- package/src/conveyor-join.ts +5 -2
- package/src/conveyor.ts +8 -0
- package/src/factories/agv-3d.ts +36 -8
- package/src/factories/conveyor-3d.ts +161 -71
- package/src/factories/machine-3d.ts +708 -72
- package/src/factories/oht-3d.ts +48 -24
- package/src/factories/shuttle-3d.ts +30 -6
- package/src/index.ts +56 -34
- package/src/mcs-carrier-holder.ts +38 -34
- package/src/node.ts +1 -1
- package/src/scanner.ts +29 -0
- package/src/scene/root-container-override.ts +12 -0
- package/src/templates/index.ts +2 -0
- package/src/templates/scanner.ts +20 -0
- package/translations/en.json +2 -1
- package/translations/ja.json +2 -1
- package/translations/ko.json +2 -1
- package/translations/ms.json +2 -1
- package/translations/zh.json +2 -1
package/jest.config.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
rootDir: '../../',
|
|
3
|
+
roots: ['<rootDir>/packages/machine'],
|
|
4
|
+
moduleFileExtensions: ['ts', 'tsx', 'js', 'json'],
|
|
5
|
+
transform: {
|
|
6
|
+
'^.+\\.tsx?$': 'babel-jest'
|
|
7
|
+
},
|
|
8
|
+
testRegex: 'packages/machine/.*\\.test\\.tsx?$',
|
|
9
|
+
testEnvironment: 'node'
|
|
10
|
+
}
|
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.30",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"module": "dist/index.js",
|
|
8
8
|
"things-scene": true,
|
|
@@ -22,10 +22,11 @@
|
|
|
22
22
|
"prepublish": "tsc; exit 0",
|
|
23
23
|
"lint": "eslint --ext .ts,.html . --ignore-path .gitignore && prettier \"**/*.ts\" --check --ignore-path .gitignore",
|
|
24
24
|
"format": "eslint --ext .ts,.html . --fix --ignore-path .gitignore && prettier \"**/*.ts\" --write --ignore-path .gitignore",
|
|
25
|
+
"test": "jest --config jest.config.js",
|
|
25
26
|
"migration": "things-factory-migration"
|
|
26
27
|
},
|
|
27
28
|
"dependencies": {
|
|
28
|
-
"@fmsim/api": "^2.0.0-beta.
|
|
29
|
+
"@fmsim/api": "^2.0.0-beta.29",
|
|
29
30
|
"@hatiolab/things-scene": "^10.0.0-beta.1"
|
|
30
31
|
},
|
|
31
32
|
"devDependencies": {
|
|
@@ -54,5 +55,5 @@
|
|
|
54
55
|
"prettier --write"
|
|
55
56
|
]
|
|
56
57
|
},
|
|
57
|
-
"gitHead": "
|
|
58
|
+
"gitHead": "e411167ccba03a7da574b27730922fd699298dc6"
|
|
58
59
|
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { Model } from '@hatiolab/things-scene'
|
|
2
|
+
import type Carrier from './carrier'
|
|
3
|
+
import type MCSCarrierHolder from './mcs-carrier-holder'
|
|
4
|
+
|
|
5
|
+
const DYING_TIMEOUT = 1000
|
|
6
|
+
|
|
7
|
+
export default class CarrierManager {
|
|
8
|
+
private carriers: Map<string, Carrier> = new Map()
|
|
9
|
+
private dyingTimers: Map<string, ReturnType<typeof setTimeout>> = new Map()
|
|
10
|
+
private root: any
|
|
11
|
+
|
|
12
|
+
constructor(root: any) {
|
|
13
|
+
this.root = root
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
get(carrierName: string): Carrier | undefined {
|
|
17
|
+
return this.carriers.get(carrierName)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
update(holder: MCSCarrierHolder, after: Record<string, any>, before: Record<string, any>): void {
|
|
21
|
+
const afterName = after?.CARRIERNAME
|
|
22
|
+
const beforeName = before?.CARRIERNAME
|
|
23
|
+
|
|
24
|
+
// 이전에는 캐리어가 있었는데 새 데이터에는 없다 → dying 후보
|
|
25
|
+
if (beforeName && (beforeName !== afterName || !afterName)) {
|
|
26
|
+
const carrier = this.carriers.get(beforeName)
|
|
27
|
+
if (carrier && carrier.parent === holder) {
|
|
28
|
+
this.scheduleDying(beforeName)
|
|
29
|
+
}
|
|
30
|
+
return
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (!afterName) {
|
|
34
|
+
return
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// dying 상태면 구출
|
|
38
|
+
const wasDying = this.dyingTimers.has(afterName)
|
|
39
|
+
this.cancelDying(afterName)
|
|
40
|
+
|
|
41
|
+
const { width, height } = holder.state
|
|
42
|
+
let carrier = this.carriers.get(afterName)
|
|
43
|
+
|
|
44
|
+
if (!carrier) {
|
|
45
|
+
// 새로 생성
|
|
46
|
+
carrier = Model.compile(
|
|
47
|
+
{
|
|
48
|
+
id: afterName,
|
|
49
|
+
type: 'Carrier',
|
|
50
|
+
left: 0,
|
|
51
|
+
top: 0,
|
|
52
|
+
width,
|
|
53
|
+
height,
|
|
54
|
+
showText: holder.getState('showCarrierText') || false
|
|
55
|
+
},
|
|
56
|
+
this.root.app
|
|
57
|
+
) as unknown as Carrier
|
|
58
|
+
|
|
59
|
+
this.carriers.set(afterName, carrier)
|
|
60
|
+
holder.addComponent(carrier, { silent: true })
|
|
61
|
+
} else if (carrier.parent !== holder && (!beforeName && afterName)) {
|
|
62
|
+
// CARRIERNAME이 새로 나타남 (이전엔 없었음) → 다른 holder로 이동
|
|
63
|
+
;(holder as any).reparent(carrier, { animated: true, duration: 2000, ease: 'inout' })
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (carrier.parent === holder) {
|
|
67
|
+
carrier.data = after
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
private scheduleDying(carrierName: string): void {
|
|
72
|
+
this.cancelDying(carrierName)
|
|
73
|
+
|
|
74
|
+
const timer = setTimeout(() => {
|
|
75
|
+
this.dyingTimers.delete(carrierName)
|
|
76
|
+
|
|
77
|
+
const carrier = this.carriers.get(carrierName)
|
|
78
|
+
if (carrier) {
|
|
79
|
+
this.carriers.delete(carrierName)
|
|
80
|
+
if (!carrier.disposed) {
|
|
81
|
+
carrier.removeSelf(true)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}, DYING_TIMEOUT)
|
|
85
|
+
|
|
86
|
+
this.dyingTimers.set(carrierName, timer)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
private cancelDying(carrierName: string): void {
|
|
90
|
+
const timer = this.dyingTimers.get(carrierName)
|
|
91
|
+
if (timer) {
|
|
92
|
+
clearTimeout(timer)
|
|
93
|
+
this.dyingTimers.delete(carrierName)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
onCarrierDisposed(carrierName: string): void {
|
|
98
|
+
this.cancelDying(carrierName)
|
|
99
|
+
this.carriers.delete(carrierName)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
dispose(): void {
|
|
103
|
+
for (const timer of this.dyingTimers.values()) {
|
|
104
|
+
clearTimeout(timer)
|
|
105
|
+
}
|
|
106
|
+
this.dyingTimers.clear()
|
|
107
|
+
|
|
108
|
+
for (const carrier of this.carriers.values()) {
|
|
109
|
+
if (!carrier.disposed) {
|
|
110
|
+
carrier.removeSelf(true)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
this.carriers.clear()
|
|
115
|
+
}
|
|
116
|
+
}
|
package/src/carrier.ts
CHANGED
|
@@ -37,6 +37,40 @@ export default class Carrier extends MCSStatusMixin(ParentObjectMixin(Shape)) {
|
|
|
37
37
|
|
|
38
38
|
set path(path) {}
|
|
39
39
|
|
|
40
|
+
|
|
41
|
+
added(parent: any) {
|
|
42
|
+
super.added(parent)
|
|
43
|
+
|
|
44
|
+
// 새 부모에 맞춰 크기 및 상태 갱신
|
|
45
|
+
if (parent) {
|
|
46
|
+
const { width, height } = parent.state || {}
|
|
47
|
+
if (width && height) {
|
|
48
|
+
this.set({ width, height })
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
this.syncStateFromData()
|
|
53
|
+
|
|
54
|
+
// 3D 오브젝트 최초 생성 (동기적 — reparent()에서 _realObject 필요)
|
|
55
|
+
if ((this.root as any)?.threed && !this._realObject) {
|
|
56
|
+
;(this.root as any).model_layer?._threeCapability?.addObject(this)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// reparent 애니메이션은 things-scene의 reparent()가 처리
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* data로부터 carrier 상태를 동기화 (생성 시, 부모 변경 시 공통 사용)
|
|
64
|
+
*/
|
|
65
|
+
private syncStateFromData(): void {
|
|
66
|
+
const { CARRIERNAME, CARRIERTYPE = '', EMPTYTYPE, CARRIERSTATUS } = this.data || {}
|
|
67
|
+
|
|
68
|
+
this.setState('id', CARRIERNAME)
|
|
69
|
+
this.setState('EMPTYTYPE', EMPTYTYPE)
|
|
70
|
+
this.setState('CARRIERTYPE', CARRIERTYPE)
|
|
71
|
+
this.setState('CARRIERSTATUS', CARRIERSTATUS)
|
|
72
|
+
}
|
|
73
|
+
|
|
40
74
|
onchangeData(after: Record<string, any>, before: Record<string, any>): void {
|
|
41
75
|
const { CARRIERNAME, CARRIERTYPE = '', EMPTYTYPE, CARRIERSTATUS } = this.data || {}
|
|
42
76
|
|
|
@@ -46,7 +80,7 @@ export default class Carrier extends MCSStatusMixin(ParentObjectMixin(Shape)) {
|
|
|
46
80
|
this.setState('CARRIERSTATUS', CARRIERSTATUS)
|
|
47
81
|
|
|
48
82
|
// TODO carrierstatus에 따라서 매핑되는 애니메이션 테마 수행
|
|
49
|
-
if (after.data
|
|
83
|
+
if (after.data?.CARRIERSTATUS !== before.data?.CARRIERSTATUS) {
|
|
50
84
|
const { CARRIERSTATUS: lastCarrierStatus } = before.data || {}
|
|
51
85
|
const lastAnimationConfig = lastCarrierStatus && this.getAnimationConfig(lastCarrierStatus)
|
|
52
86
|
|
|
@@ -129,16 +163,22 @@ export default class Carrier extends MCSStatusMixin(ParentObjectMixin(Shape)) {
|
|
|
129
163
|
}
|
|
130
164
|
|
|
131
165
|
// 캐시에 없으면 파싱하고 저장
|
|
132
|
-
|
|
166
|
+
let theme = (this.root as any)?.style?.[carrierAnimationName]
|
|
133
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
|
+
|
|
134
174
|
// 문자열 형태의 설정들을 미리 파싱
|
|
135
175
|
const parsedTheme = {}
|
|
136
176
|
for (const [status, config] of Object.entries(theme)) {
|
|
137
|
-
if (typeof config === 'string') {
|
|
177
|
+
if (typeof config === 'string' && config.trim()) {
|
|
138
178
|
try {
|
|
139
179
|
parsedTheme[status] = JSON5.parse(config)
|
|
140
180
|
} catch (e) {
|
|
141
|
-
console.error('JSON5 parse error:', e)
|
|
181
|
+
console.error('JSON5 parse error:', e, 'input:', JSON.stringify(config), 'status:', status, 'theme:', carrierAnimationName)
|
|
142
182
|
parsedTheme[status] = config
|
|
143
183
|
}
|
|
144
184
|
} else {
|
|
@@ -249,6 +289,12 @@ export default class Carrier extends MCSStatusMixin(ParentObjectMixin(Shape)) {
|
|
|
249
289
|
}
|
|
250
290
|
}
|
|
251
291
|
|
|
292
|
+
dispose(): void {
|
|
293
|
+
;(this.root as any)?._carrierManager?.onCarrierDisposed(this.state.id)
|
|
294
|
+
|
|
295
|
+
super.dispose()
|
|
296
|
+
}
|
|
297
|
+
|
|
252
298
|
async detailInfo() {
|
|
253
299
|
const { type, id } = this.state
|
|
254
300
|
|
package/src/conveyor-join.ts
CHANGED
|
@@ -119,8 +119,11 @@ export default class ConveyorJoin extends ConveyorMixin(MCSTransport) {
|
|
|
119
119
|
if (normx * normx + normy * normy < 0.25 && ratiox * ratiox + ratioy * ratioy > 0.25) {
|
|
120
120
|
const angle = normalizeAngle(Math.atan2(-normy, normx) - Math.PI / 2)
|
|
121
121
|
|
|
122
|
-
if (
|
|
123
|
-
return true
|
|
122
|
+
if (startAngle <= endAngle) {
|
|
123
|
+
if (angle >= startAngle && angle <= endAngle) return true
|
|
124
|
+
} else {
|
|
125
|
+
// wraparound: startAngle > endAngle (예: π → 0)
|
|
126
|
+
if (angle >= startAngle || angle <= endAngle) return true
|
|
124
127
|
}
|
|
125
128
|
}
|
|
126
129
|
|
package/src/conveyor.ts
CHANGED
|
@@ -14,6 +14,14 @@ const NATURE: ComponentNature = {
|
|
|
14
14
|
options: ['', 'conveyor', 'belt', 'rail']
|
|
15
15
|
}
|
|
16
16
|
},
|
|
17
|
+
{
|
|
18
|
+
type: 'select',
|
|
19
|
+
label: 'conveyor-orientation',
|
|
20
|
+
name: 'orientation',
|
|
21
|
+
property: {
|
|
22
|
+
options: ['', 'horizontal', 'vertical']
|
|
23
|
+
}
|
|
24
|
+
},
|
|
17
25
|
...MCSTransport.properties
|
|
18
26
|
]
|
|
19
27
|
}
|
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
|
|
@@ -40,14 +41,14 @@ export class AGV3D extends RealObjectGroup {
|
|
|
40
41
|
// '#F0F0F0'은 상태 미설정 시 기본 fallback — 무시하고 defaultColor 사용
|
|
41
42
|
try {
|
|
42
43
|
const sc = comp.statusColor
|
|
43
|
-
if (typeof sc === 'string' && sc && sc.toUpperCase() !== '#F0F0F0') return new THREE.Color(sc).getHex()
|
|
44
|
+
if (typeof sc === 'string' && sc && sc.toUpperCase() !== '#F0F0F0' && sc !== 'transparent') return new THREE.Color(sc).getHex()
|
|
44
45
|
} catch {
|
|
45
46
|
/* statusColor 미구현 시 무시 */
|
|
46
47
|
}
|
|
47
48
|
|
|
48
49
|
// 2) state.fillStyle fallback
|
|
49
50
|
const { fillStyle } = this.component.state
|
|
50
|
-
if (fillStyle && typeof fillStyle === 'string') {
|
|
51
|
+
if (fillStyle && typeof fillStyle === 'string' && fillStyle !== 'transparent') {
|
|
51
52
|
try {
|
|
52
53
|
return new THREE.Color(fillStyle).getHex()
|
|
53
54
|
} catch {
|
|
@@ -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,17 +116,46 @@ 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 ||
|
|
154
|
+
'EMPTYTYPE' in after || 'CARRIERSTATUS' in after) {
|
|
128
155
|
this.update()
|
|
129
156
|
return
|
|
130
157
|
}
|
|
158
|
+
|
|
131
159
|
super.onchange(after, before)
|
|
132
160
|
}
|
|
133
161
|
}
|