@fmsim/machine 2.0.0-beta.5 → 2.0.0-beta.53
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.js +2 -2
- package/dist/agv.js.map +1 -1
- package/dist/carrier-manager.js +93 -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/crane.js.map +1 -1
- package/dist/factories/agv-3d.js +38 -12
- package/dist/factories/agv-3d.js.map +1 -1
- package/dist/factories/conveyor-3d.js +166 -81
- package/dist/factories/conveyor-3d.js.map +1 -1
- package/dist/factories/crane-3d.js +5 -7
- package/dist/factories/crane-3d.js.map +1 -1
- package/dist/factories/crane-rail-3d.js +4 -6
- package/dist/factories/crane-rail-3d.js.map +1 -1
- package/dist/factories/machine-3d.js +464 -68
- package/dist/factories/machine-3d.js.map +1 -1
- package/dist/factories/oht-3d.js +52 -26
- package/dist/factories/oht-3d.js.map +1 -1
- package/dist/factories/shuttle-3d.js +32 -9
- 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/mcs-vehicle.js +8 -3
- package/dist/mcs-vehicle.js.map +1 -1
- package/dist/node.js +1 -1
- package/dist/node.js.map +1 -1
- package/dist/oht.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/stocker-capacity-bar.js +2 -2
- package/dist/stocker-capacity-bar.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/dist/utils/get-value-on-ranges.js +2 -4
- package/dist/utils/get-value-on-ranges.js.map +1 -1
- package/dist/zone-capacity-bar.js +2 -2
- package/dist/zone-capacity-bar.js.map +1 -1
- package/jest.config.js +10 -0
- package/package.json +4 -3
- package/src/agv.ts +2 -2
- package/src/carrier-manager.ts +117 -0
- package/src/carrier.ts +50 -4
- package/src/conveyor-join.ts +5 -2
- package/src/conveyor.ts +8 -0
- package/src/crane.ts +1 -1
- package/src/factories/agv-3d.ts +38 -13
- package/src/factories/conveyor-3d.ts +168 -84
- package/src/factories/crane-3d.ts +6 -8
- package/src/factories/crane-rail-3d.ts +5 -7
- package/src/factories/machine-3d.ts +551 -62
- package/src/factories/oht-3d.ts +50 -26
- package/src/factories/shuttle-3d.ts +30 -10
- package/src/index.ts +56 -34
- package/src/mcs-carrier-holder.ts +38 -34
- package/src/mcs-vehicle.ts +9 -6
- package/src/node.ts +1 -1
- package/src/oht.ts +1 -1
- package/src/scanner.ts +29 -0
- package/src/scene/root-container-override.ts +12 -0
- package/src/stocker-capacity-bar.ts +2 -2
- package/src/templates/index.ts +2 -0
- package/src/templates/scanner.ts +20 -0
- package/src/utils/get-value-on-ranges.ts +2 -4
- package/src/zone-capacity-bar.ts +2 -2
- 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
|
@@ -9,10 +9,11 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import * as THREE from 'three'
|
|
12
|
-
import
|
|
12
|
+
import * as BufferGeometryUtils from 'three/examples/jsm/utils/BufferGeometryUtils.js'
|
|
13
|
+
import { RealObjectGroup, registerRealObjectFactory, disposeObject3D } from '@hatiolab/things-scene'
|
|
13
14
|
import type { Component } from '@hatiolab/things-scene'
|
|
14
15
|
import { LEGEND_CAPACITY } from '../features/mcs-status-default'
|
|
15
|
-
import {
|
|
16
|
+
import { getValueOnRanges } from '../utils/get-value-on-ranges'
|
|
16
17
|
|
|
17
18
|
// ── 설정 타입 ────────────────────────────────────────────
|
|
18
19
|
|
|
@@ -30,11 +31,23 @@ interface MCS3DConfig {
|
|
|
30
31
|
/** statusColor 미사용 시 기본 색상 */
|
|
31
32
|
defaultColor: number
|
|
32
33
|
/** 'box' | 'hexagon' */
|
|
33
|
-
shape: 'box' | 'hexagon'
|
|
34
|
+
shape: 'box' | 'hexagon' | 'plane'
|
|
34
35
|
/** mesh 생성 전 가시성 판정 — false 반환 시 mesh 미생성 */
|
|
35
36
|
isVisible?: (component: Component) => boolean
|
|
36
37
|
/** 부모 대비 크기 비율 (기본 1.0). < 1이면 부모 중심에 배치. */
|
|
37
38
|
sizeRatio?: number
|
|
39
|
+
/** 상부 면 제거 (5면 개방 박스) */
|
|
40
|
+
openTop?: boolean
|
|
41
|
+
/** 상판 무반사 (MeshBasicMaterial) */
|
|
42
|
+
matteTop?: boolean
|
|
43
|
+
/** 상판에 2D 렌더링 텍스처 적용 */
|
|
44
|
+
texturedTop?: boolean
|
|
45
|
+
/** 자체 발광 강도 (0~1). 조명 약한 면에서도 색상 유지 */
|
|
46
|
+
emissiveIntensity?: number
|
|
47
|
+
/** 바닥면 유지 (기본 false — 바닥면 제거) */
|
|
48
|
+
keepBottomFace?: boolean
|
|
49
|
+
/** syncZPosOffset 고정값 (설정 시 다른 로직 무시) */
|
|
50
|
+
zPosOverride?: number
|
|
38
51
|
}
|
|
39
52
|
|
|
40
53
|
/**
|
|
@@ -78,37 +91,51 @@ class MCSRealObject3D extends RealObjectGroup {
|
|
|
78
91
|
}
|
|
79
92
|
|
|
80
93
|
protected get syncZPosOffset(): number {
|
|
94
|
+
// syncZPosOffset = placement + geometric (center origin = effectiveDepth/2)
|
|
95
|
+
// 중첩 보정(parent shift 상쇄)은 base RealObject.cz가 parent.geometricOffsetY를
|
|
96
|
+
// 빼는 방식으로 처리하므로 여기선 placement + 본인 geometric만 반환
|
|
97
|
+
const halfD = this.effectiveDepth / 2
|
|
98
|
+
// zPosOverride: 고정 높이 (OHTLine 등)
|
|
99
|
+
if (this._config.zPosOverride != null) {
|
|
100
|
+
return this._config.zPosOverride + halfD
|
|
101
|
+
}
|
|
81
102
|
// Port: 이송 표면 높이에 배치
|
|
82
103
|
if (this._config.defaultZPosFromParent) {
|
|
83
|
-
return TRANSPORT_SURFACE_HEIGHT
|
|
104
|
+
return TRANSPORT_SURFACE_HEIGHT + halfD
|
|
84
105
|
}
|
|
85
|
-
// Carrier:
|
|
106
|
+
// Carrier: 부모 유형에 따라 배치 위치 결정
|
|
86
107
|
if (this._config.sizeRatio && this._config.sizeRatio < 1) {
|
|
87
108
|
const parentType = this.component.parent?.state?.type
|
|
88
|
-
|
|
89
|
-
|
|
109
|
+
// OHT: 그리퍼 아래에 매달림 — placement = -effectiveDepth(carrier top이 OHT bottom)
|
|
110
|
+
if (parentType === 'OHT') {
|
|
111
|
+
return -this.effectiveDepth + halfD
|
|
112
|
+
}
|
|
113
|
+
if (parentType === 'Port' || parentType === 'Shelf' || parentType === 'Shuttle') {
|
|
114
|
+
return halfD
|
|
90
115
|
}
|
|
91
|
-
return TRANSPORT_SURFACE_HEIGHT
|
|
116
|
+
return TRANSPORT_SURFACE_HEIGHT + halfD
|
|
92
117
|
}
|
|
93
|
-
return
|
|
118
|
+
return halfD
|
|
94
119
|
}
|
|
95
120
|
|
|
121
|
+
// geometric 부분(center origin: depth/2)은 base 기본값 사용 — override 불필요
|
|
122
|
+
|
|
96
123
|
// 컴포넌트의 상태 색상(statusColor) 또는 fillStyle → Three.js 색상 변환
|
|
97
|
-
|
|
124
|
+
protected resolveColor(): number {
|
|
98
125
|
const comp = this.component as any
|
|
99
126
|
|
|
100
127
|
// 1) MCSStatusMixin.statusColor (legend 기반 상태 색상)
|
|
101
128
|
// '#F0F0F0'은 상태 미설정 시 기본 fallback — 무시하고 defaultColor 사용
|
|
102
129
|
try {
|
|
103
130
|
const sc = comp.statusColor
|
|
104
|
-
if (typeof sc === 'string' && sc && sc.toUpperCase() !== '#F0F0F0') return new THREE.Color(sc).getHex()
|
|
131
|
+
if (typeof sc === 'string' && sc && sc.toUpperCase() !== '#F0F0F0' && sc !== 'transparent') return new THREE.Color(sc).getHex()
|
|
105
132
|
} catch {
|
|
106
133
|
/* statusColor 미구현 시 무시 */
|
|
107
134
|
}
|
|
108
135
|
|
|
109
136
|
// 2) state.fillStyle fallback
|
|
110
137
|
const { fillStyle } = this.component.state
|
|
111
|
-
if (fillStyle && typeof fillStyle === 'string') {
|
|
138
|
+
if (fillStyle && typeof fillStyle === 'string' && fillStyle !== 'transparent') {
|
|
112
139
|
try {
|
|
113
140
|
return new THREE.Color(fillStyle).getHex()
|
|
114
141
|
} catch {
|
|
@@ -157,7 +184,7 @@ class MCSRealObject3D extends RealObjectGroup {
|
|
|
157
184
|
|
|
158
185
|
private buildBoxGeometry(w: number, d: number, h: number): THREE.BufferGeometry {
|
|
159
186
|
const geo = new THREE.BoxGeometry(w, d, h)
|
|
160
|
-
return this.removeBottomFace(geo)
|
|
187
|
+
return this._config.keepBottomFace ? geo : this.removeBottomFace(geo)
|
|
161
188
|
}
|
|
162
189
|
|
|
163
190
|
private buildHexagonGeometry(w: number, d: number, h: number): THREE.BufferGeometry {
|
|
@@ -188,8 +215,16 @@ class MCSRealObject3D extends RealObjectGroup {
|
|
|
188
215
|
return this.removeBottomFace(geo)
|
|
189
216
|
}
|
|
190
217
|
|
|
218
|
+
private buildPlaneGeometry(w: number, h: number): THREE.BufferGeometry {
|
|
219
|
+
const geo = new THREE.PlaneGeometry(w, h)
|
|
220
|
+
geo.rotateX(-Math.PI / 2)
|
|
221
|
+
return geo
|
|
222
|
+
}
|
|
223
|
+
|
|
191
224
|
// ── Build ──
|
|
192
225
|
|
|
226
|
+
protected _ownMesh?: THREE.Mesh
|
|
227
|
+
|
|
193
228
|
build() {
|
|
194
229
|
super.build()
|
|
195
230
|
|
|
@@ -209,34 +244,67 @@ class MCSRealObject3D extends RealObjectGroup {
|
|
|
209
244
|
const { opacity, metalness, roughness, shape } = this._config
|
|
210
245
|
|
|
211
246
|
const geometry =
|
|
212
|
-
shape === '
|
|
213
|
-
? this.
|
|
214
|
-
:
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
247
|
+
shape === 'plane'
|
|
248
|
+
? this.buildPlaneGeometry(w, h)
|
|
249
|
+
: shape === 'hexagon'
|
|
250
|
+
? this.buildHexagonGeometry(w, depth, h)
|
|
251
|
+
: this.buildBoxGeometry(w, depth, h)
|
|
252
|
+
|
|
253
|
+
const isPlane = shape === 'plane'
|
|
254
|
+
const material = isPlane
|
|
255
|
+
? new THREE.MeshBasicMaterial({
|
|
256
|
+
color,
|
|
257
|
+
opacity,
|
|
258
|
+
transparent: opacity < 1,
|
|
259
|
+
side: THREE.DoubleSide,
|
|
260
|
+
})
|
|
261
|
+
: new THREE.MeshStandardMaterial({
|
|
262
|
+
color,
|
|
263
|
+
metalness,
|
|
264
|
+
roughness,
|
|
265
|
+
opacity,
|
|
266
|
+
transparent: opacity < 1,
|
|
267
|
+
side: opacity < 1 ? THREE.DoubleSide : THREE.FrontSide,
|
|
268
|
+
...(this._config.emissiveIntensity ? { emissive: color, emissiveIntensity: this._config.emissiveIntensity } : {}),
|
|
269
|
+
})
|
|
225
270
|
// z-fighting 방지: 바닥면(floor grid)보다 항상 앞에 렌더링
|
|
226
271
|
material.polygonOffset = true
|
|
227
272
|
material.polygonOffsetFactor = -1
|
|
228
273
|
material.polygonOffsetUnit = -1
|
|
229
274
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
275
|
+
// BoxGeometry material groups: +x(0), -x(1), +y(2), -y(3), +z(4), -z(5)
|
|
276
|
+
let meshMaterial: THREE.Material | THREE.Material[] = material
|
|
277
|
+
if (shape === 'box') {
|
|
278
|
+
if (this._config.openTop) {
|
|
279
|
+
// 상단면 제거 → 5면 개방 박스
|
|
280
|
+
const invisible = new THREE.MeshBasicMaterial({ visible: false })
|
|
281
|
+
meshMaterial = [material, material, invisible, material, material, material]
|
|
282
|
+
} else if (this._config.texturedTop) {
|
|
283
|
+
// 상판에 2D 렌더링 텍스처
|
|
284
|
+
const topMat = this.buildTopTexture(w, h)
|
|
285
|
+
meshMaterial = [material, material, topMat, material, material, material]
|
|
286
|
+
} else if (this._config.matteTop) {
|
|
287
|
+
// 상판만 무반사
|
|
288
|
+
const matte = new THREE.MeshBasicMaterial({ color, opacity, transparent: opacity < 1 })
|
|
289
|
+
meshMaterial = [material, material, matte, material, material, material]
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
this._ownMesh = new THREE.Mesh(geometry, meshMaterial)
|
|
294
|
+
// ownGroup 기준(bottom-up): mesh 바닥을 y=0에 맞추고 ownGroup이 -d/2로 shift해
|
|
295
|
+
// center-origin object3d에 정렬됨.
|
|
296
|
+
// box/hexagon (center-local geometry): y=rawDepth/2 (바닥을 0에 맞춤)
|
|
297
|
+
// plane: y=1 (바닥 살짝 띄움)
|
|
298
|
+
this._ownMesh.position.y = isPlane ? 1 : rawDepth / 2
|
|
299
|
+
this._ownMesh.castShadow = !isPlane
|
|
300
|
+
this._ownMesh.receiveShadow = true
|
|
301
|
+
this.components3D.add(this._ownMesh)
|
|
236
302
|
}
|
|
237
303
|
|
|
238
304
|
// ── Change handling ──
|
|
239
305
|
|
|
306
|
+
private _updatePending = false
|
|
307
|
+
|
|
240
308
|
onchange(after: Record<string, unknown>, before: Record<string, unknown>) {
|
|
241
309
|
if (
|
|
242
310
|
'width' in after ||
|
|
@@ -244,18 +312,117 @@ class MCSRealObject3D extends RealObjectGroup {
|
|
|
244
312
|
'depth' in after ||
|
|
245
313
|
'fillStyle' in after ||
|
|
246
314
|
'strokeStyle' in after ||
|
|
247
|
-
'id' in after
|
|
315
|
+
'id' in after ||
|
|
316
|
+
'EMPTYTYPE' in after ||
|
|
317
|
+
'CARRIERSTATUS' in after
|
|
248
318
|
) {
|
|
249
|
-
|
|
319
|
+
// 연속 setState에 의한 다중 rebuild 방지: 한 프레임에 한 번만 실행
|
|
320
|
+
if (!this._updatePending) {
|
|
321
|
+
this._updatePending = true
|
|
322
|
+
queueMicrotask(() => {
|
|
323
|
+
this._updatePending = false
|
|
324
|
+
this.update()
|
|
325
|
+
})
|
|
326
|
+
}
|
|
250
327
|
return
|
|
251
328
|
}
|
|
252
329
|
|
|
253
330
|
super.onchange(after, before)
|
|
254
331
|
}
|
|
255
332
|
|
|
256
|
-
|
|
333
|
+
/**
|
|
334
|
+
* 상판 텍스처: 컴포넌트의 2D render()를 캔버스에 그려서 텍스처로 사용
|
|
335
|
+
*/
|
|
336
|
+
protected buildTopTexture(w: number, h: number): THREE.MeshBasicMaterial {
|
|
337
|
+
const scale = 4
|
|
338
|
+
const cw = this.nextPow2(w * scale)
|
|
339
|
+
const ch = this.nextPow2(h * scale)
|
|
340
|
+
const canvas = document.createElement('canvas')
|
|
341
|
+
canvas.width = cw
|
|
342
|
+
canvas.height = ch
|
|
343
|
+
const ctx = canvas.getContext('2d')!
|
|
344
|
+
|
|
345
|
+
const statusColor = '#' + this.resolveColor().toString(16).padStart(6, '0')
|
|
346
|
+
const emptyType = (this.component.state as any)?.EMPTYTYPE || ''
|
|
347
|
+
const text = emptyType === 'FULL' ? 'F' : emptyType === 'EMPTY' ? 'E' : emptyType === 'EMPTYEMPTY' ? 'X' : ''
|
|
348
|
+
|
|
349
|
+
// 실제 사용 영역 (nextPow2 이전 크기)
|
|
350
|
+
const usedW = w * scale
|
|
351
|
+
const usedH = h * scale
|
|
352
|
+
|
|
353
|
+
// 바탕: 캐리어 상태 색상 (사용 영역만)
|
|
354
|
+
ctx.fillStyle = statusColor
|
|
355
|
+
ctx.fillRect(0, 0, usedW, usedH)
|
|
356
|
+
|
|
357
|
+
// 텍스트 (사용 영역 중앙)
|
|
358
|
+
if (text) {
|
|
359
|
+
const fontSize = Math.round(Math.min(usedW, usedH) * 0.7)
|
|
360
|
+
ctx.font = `bold ${fontSize}px Arial`
|
|
361
|
+
ctx.textAlign = 'center'
|
|
362
|
+
ctx.textBaseline = 'middle'
|
|
363
|
+
ctx.fillStyle = '#000000'
|
|
364
|
+
ctx.fillText(text, usedW / 2, usedH / 2)
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const texture = new THREE.CanvasTexture(canvas)
|
|
368
|
+
texture.offset.set(0, 1 - usedH / ch)
|
|
369
|
+
texture.repeat.set(usedW / cw, usedH / ch)
|
|
370
|
+
texture.colorSpace = THREE.SRGBColorSpace
|
|
371
|
+
|
|
372
|
+
return new THREE.MeshBasicMaterial({
|
|
373
|
+
map: texture,
|
|
374
|
+
transparent: true,
|
|
375
|
+
side: THREE.DoubleSide,
|
|
376
|
+
})
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
private nextPow2(n: number): number {
|
|
380
|
+
if (!(n > 0)) return 1
|
|
381
|
+
let v = Math.ceil(n)
|
|
382
|
+
v--
|
|
383
|
+
v |= v >> 1; v |= v >> 2; v |= v >> 4; v |= v >> 8; v |= v >> 16
|
|
384
|
+
return v + 1
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* 자신의 mesh만 제거 후 rebuild.
|
|
389
|
+
* 기본 update()는 clear()로 object3d의 모든 children을 제거하는데,
|
|
390
|
+
* 컨테이너(Port 등)의 경우 자식 컴포넌트(Carrier 등)의 3D 오브젝트도 함께 제거됨.
|
|
391
|
+
* 이를 방지하기 위해 자신이 생성한 mesh만 제거한다.
|
|
392
|
+
*/
|
|
393
|
+
update() {
|
|
394
|
+
// 자신의 mesh만 제거 (자식 컴포넌트 3D 오브젝트 보존)
|
|
395
|
+
if (this._ownMesh) {
|
|
396
|
+
this.components3D.remove(this._ownMesh)
|
|
397
|
+
disposeObject3D(this._ownMesh)
|
|
398
|
+
this._ownMesh = undefined
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
this.build()
|
|
402
|
+
this.updateTransform()
|
|
403
|
+
this.updateAlpha()
|
|
404
|
+
this.updateHidden()
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// RealObjectGroup 기본 동작 비활성화
|
|
257
408
|
updateDimension() {}
|
|
258
|
-
|
|
409
|
+
|
|
410
|
+
updateAlpha() {
|
|
411
|
+
const state = this.component.state
|
|
412
|
+
// alpha가 명시적으로 설정되어 있으면 그 값을 사용, 없으면 config 기본값 적용
|
|
413
|
+
const effective = ('alpha' in state) ? (state.alpha as number) : this._config.opacity
|
|
414
|
+
|
|
415
|
+
this.object3d.traverse((o: any) => {
|
|
416
|
+
if (!o.material) return
|
|
417
|
+
const materials = Array.isArray(o.material) ? o.material : [o.material]
|
|
418
|
+
for (const m of materials) {
|
|
419
|
+
if (!m.visible) continue // openTop invisible face 무시
|
|
420
|
+
if (m.map) continue // 텍스처 material은 자체 transparent 유지
|
|
421
|
+
m.opacity = effective
|
|
422
|
+
m.transparent = effective < 1
|
|
423
|
+
}
|
|
424
|
+
})
|
|
425
|
+
}
|
|
259
426
|
}
|
|
260
427
|
|
|
261
428
|
// ── 팩토리 등록 헬퍼 ─────────────────────────────────────
|
|
@@ -287,18 +454,106 @@ register('Buffer', {
|
|
|
287
454
|
})
|
|
288
455
|
|
|
289
456
|
register('AGVLine', {
|
|
290
|
-
depthRatio: 0
|
|
457
|
+
depthRatio: 0,
|
|
458
|
+
defaultDepth: 0.1,
|
|
291
459
|
metalness: 0.6,
|
|
292
460
|
roughness: 0.3,
|
|
293
461
|
defaultColor: 0x888899,
|
|
294
462
|
})
|
|
295
463
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
464
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
465
|
+
// OHTLine — 천장 I-beam 레일
|
|
466
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
467
|
+
|
|
468
|
+
const OHT_LINE_RAIL_COLOR = 0xcccccc
|
|
469
|
+
const OHT_LINE_HEIGHT = 7 * TRANSPORT_SURFACE_HEIGHT // OHT 상단과 일치
|
|
470
|
+
|
|
471
|
+
class OHTLineRealObject3D extends RealObjectGroup {
|
|
472
|
+
private _mesh?: THREE.Mesh
|
|
473
|
+
|
|
474
|
+
get effectiveDepth(): number { return 0 }
|
|
475
|
+
protected get syncZPosOffset(): number { return OHT_LINE_HEIGHT }
|
|
476
|
+
|
|
477
|
+
build() {
|
|
478
|
+
super.build()
|
|
479
|
+
|
|
480
|
+
const { width = 10, height = 10 } = this.component.state
|
|
481
|
+
const w = Math.abs(width)
|
|
482
|
+
const h = Math.abs(height)
|
|
483
|
+
if (!w || !h) return
|
|
484
|
+
|
|
485
|
+
// I-beam 치수: 이송 방향 = 긴 변, 폭은 2D의 1/2
|
|
486
|
+
const isVertical = w < h
|
|
487
|
+
const shortSide = isVertical ? w : h // 2D 원본 짧은 쪽 폭
|
|
488
|
+
const beltW = shortSide * 0.25 // I-beam 폭: 원본의 1/4
|
|
489
|
+
const beamH = Math.max(beltW * 0.5, 3) // I-beam 전체 높이
|
|
490
|
+
const inset = shortSide * 0.5 // 양끝 여백: 원본 폭의 1/2
|
|
491
|
+
const railLen = (isVertical ? h : w) - inset * 2
|
|
492
|
+
const flangeH = beamH * 0.2 // 플랜지 두께
|
|
493
|
+
const flangeW = beltW * 0.8 // 플랜지 폭
|
|
494
|
+
const webH = beamH - flangeH * 2 // 웹 높이
|
|
495
|
+
const webW = Math.max(flangeW * 0.25, 1) // 웹 두께
|
|
496
|
+
|
|
497
|
+
const mat = new THREE.MeshStandardMaterial({
|
|
498
|
+
color: OHT_LINE_RAIL_COLOR,
|
|
499
|
+
metalness: 0.5,
|
|
500
|
+
roughness: 0.4,
|
|
501
|
+
})
|
|
502
|
+
|
|
503
|
+
const geoms: THREE.BufferGeometry[] = []
|
|
504
|
+
|
|
505
|
+
// 상부 플랜지
|
|
506
|
+
const topFlange = isVertical
|
|
507
|
+
? new THREE.BoxGeometry(flangeW, flangeH, railLen)
|
|
508
|
+
: new THREE.BoxGeometry(railLen, flangeH, flangeW)
|
|
509
|
+
topFlange.translate(0, -flangeH / 2, 0)
|
|
510
|
+
geoms.push(topFlange)
|
|
511
|
+
|
|
512
|
+
// 웹 (수직판)
|
|
513
|
+
const web = isVertical
|
|
514
|
+
? new THREE.BoxGeometry(webW, webH, railLen)
|
|
515
|
+
: new THREE.BoxGeometry(railLen, webH, webW)
|
|
516
|
+
web.translate(0, -flangeH - webH / 2, 0)
|
|
517
|
+
geoms.push(web)
|
|
518
|
+
|
|
519
|
+
// 하부 플랜지
|
|
520
|
+
const botFlange = isVertical
|
|
521
|
+
? new THREE.BoxGeometry(flangeW, flangeH, railLen)
|
|
522
|
+
: new THREE.BoxGeometry(railLen, flangeH, flangeW)
|
|
523
|
+
botFlange.translate(0, -flangeH - webH - flangeH / 2, 0)
|
|
524
|
+
geoms.push(botFlange)
|
|
525
|
+
|
|
526
|
+
this._mesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(geoms), mat)
|
|
527
|
+
this._mesh.castShadow = true
|
|
528
|
+
this.object3d.add(this._mesh)
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
update() {
|
|
532
|
+
if (this._mesh) {
|
|
533
|
+
this.object3d.remove(this._mesh)
|
|
534
|
+
this._mesh.geometry.dispose()
|
|
535
|
+
;(this._mesh.material as THREE.Material).dispose()
|
|
536
|
+
this._mesh = undefined
|
|
537
|
+
}
|
|
538
|
+
this.build()
|
|
539
|
+
this.updateTransform()
|
|
540
|
+
this.updateHidden()
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
onchange(after: Record<string, unknown>, before: Record<string, unknown>) {
|
|
544
|
+
if ('width' in after || 'height' in after || 'depth' in after ||
|
|
545
|
+
'fillStyle' in after || 'strokeStyle' in after) {
|
|
546
|
+
this.update()
|
|
547
|
+
return
|
|
548
|
+
}
|
|
549
|
+
super.onchange(after, before)
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
updateDimension() {}
|
|
553
|
+
updateAlpha() {}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
registerRealObjectFactory('OHTLine', (component: Component) => new OHTLineRealObject3D(component))
|
|
302
557
|
|
|
303
558
|
// STOCKER: 벽면에 용량 게이지 바를 포함하는 커스텀 3D 오브젝트
|
|
304
559
|
const STOCKER_CONFIG: MCS3DConfig = {
|
|
@@ -396,8 +651,9 @@ class StockerRealObject3D extends MCSRealObject3D {
|
|
|
396
651
|
mat.polygonOffsetUnit = -2
|
|
397
652
|
|
|
398
653
|
this.gaugeMesh = new THREE.Mesh(geo, mat)
|
|
654
|
+
// ownGroup 기준(bottom-up): 볼륨 상단 = +depth. 그 위로 0.5 띄움.
|
|
399
655
|
this.gaugeMesh.position.set(0, depth + 0.5, 0)
|
|
400
|
-
this.
|
|
656
|
+
this.components3D.add(this.gaugeMesh)
|
|
401
657
|
}
|
|
402
658
|
|
|
403
659
|
private resolveGauge(data: any, mode: string) {
|
|
@@ -442,7 +698,7 @@ class StockerRealObject3D extends MCSRealObject3D {
|
|
|
442
698
|
const fullup = data.FULLUP === 'T'
|
|
443
699
|
const usage = Math.round((cur / max) * 100)
|
|
444
700
|
const legend = (this.component as any).root?.style?.LEGEND_CAPACITY || LEGEND_CAPACITY
|
|
445
|
-
const color = fullup ? 'red' : (
|
|
701
|
+
const color = fullup ? 'red' : (getValueOnRanges(usage, legend) || '#44aa44')
|
|
446
702
|
return {
|
|
447
703
|
segments: [{ ratio: usage / 100, color }],
|
|
448
704
|
borderColor: color,
|
|
@@ -452,6 +708,11 @@ class StockerRealObject3D extends MCSRealObject3D {
|
|
|
452
708
|
}
|
|
453
709
|
}
|
|
454
710
|
|
|
711
|
+
update() {
|
|
712
|
+
this.clearGauge()
|
|
713
|
+
super.update()
|
|
714
|
+
}
|
|
715
|
+
|
|
455
716
|
onchange(after: Record<string, unknown>, before: Record<string, unknown>) {
|
|
456
717
|
if ('data' in after) {
|
|
457
718
|
this.clearGauge()
|
|
@@ -467,7 +728,7 @@ class StockerRealObject3D extends MCSRealObject3D {
|
|
|
467
728
|
|
|
468
729
|
private clearGauge() {
|
|
469
730
|
if (this.gaugeMesh) {
|
|
470
|
-
this.
|
|
731
|
+
this.components3D.remove(this.gaugeMesh)
|
|
471
732
|
this.gaugeMesh.geometry?.dispose()
|
|
472
733
|
const mat = this.gaugeMesh.material as THREE.MeshBasicMaterial
|
|
473
734
|
mat?.map?.dispose()
|
|
@@ -483,14 +744,18 @@ registerRealObjectFactory('STOCKER', (component: Component) => new StockerRealOb
|
|
|
483
744
|
// Unit (이동체 · 홀더) — 낮은 depth, 불투명
|
|
484
745
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
485
746
|
|
|
486
|
-
|
|
487
|
-
|
|
747
|
+
// Port: 바닥판 (PlaneGeometry)
|
|
748
|
+
const PORT_CONFIG: MCS3DConfig = {
|
|
749
|
+
...DEFAULTS,
|
|
750
|
+
depthRatio: 0.01,
|
|
488
751
|
defaultZPosFromParent: true,
|
|
489
|
-
opacity: 0.
|
|
490
|
-
metalness: 0.
|
|
491
|
-
roughness: 0.
|
|
752
|
+
opacity: 0.1,
|
|
753
|
+
metalness: 0.3,
|
|
754
|
+
roughness: 0.8,
|
|
492
755
|
defaultColor: 0xb0a090,
|
|
493
|
-
|
|
756
|
+
shape: 'plane',
|
|
757
|
+
}
|
|
758
|
+
registerRealObjectFactory('Port', (component: Component) => new MCSRealObject3D(component, PORT_CONFIG))
|
|
494
759
|
|
|
495
760
|
register('Shelf', {
|
|
496
761
|
depthRatio: 0.25,
|
|
@@ -499,14 +764,25 @@ register('Shelf', {
|
|
|
499
764
|
defaultColor: 0xaabbcc,
|
|
500
765
|
})
|
|
501
766
|
|
|
502
|
-
|
|
767
|
+
// Carrier: CARRIERSTATUS에 따른 3D 애니메이션 지원
|
|
768
|
+
const CARRIER_CONFIG: MCS3DConfig = {
|
|
769
|
+
...DEFAULTS,
|
|
503
770
|
depthRatio: 0.5,
|
|
504
|
-
metalness: 0
|
|
505
|
-
roughness:
|
|
506
|
-
defaultColor:
|
|
771
|
+
metalness: 0,
|
|
772
|
+
roughness: 1,
|
|
773
|
+
defaultColor: 0xf0f0f0,
|
|
507
774
|
isVisible: (c: Component) => !!(c.state as any).id,
|
|
508
775
|
sizeRatio: 0.8,
|
|
509
|
-
|
|
776
|
+
texturedTop: true,
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
class CarrierRealObject3D extends MCSRealObject3D {
|
|
780
|
+
constructor(component: Component) {
|
|
781
|
+
super(component, CARRIER_CONFIG)
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
registerRealObjectFactory('Carrier', (component: Component) => new CarrierRealObject3D(component))
|
|
510
786
|
|
|
511
787
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
512
788
|
// Gauge (용량 게이지) — 얇은 바
|
|
@@ -516,8 +792,221 @@ register('Carrier', {
|
|
|
516
792
|
// Gauge (용량 게이지) — 2D 렌더링을 Plane 텍스처로 3D 표시
|
|
517
793
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
518
794
|
|
|
519
|
-
// Gauge
|
|
520
|
-
//
|
|
521
|
-
registerRealObjectFactory('StockerCapacityBar', () =>
|
|
522
|
-
registerRealObjectFactory('MCSGaugeCapacityBar', () =>
|
|
523
|
-
registerRealObjectFactory('ZoneCapacityBar', () =>
|
|
795
|
+
// Gauge 컴포넌트는 3D에서 표시하지 않음 (부모 STOCKER의 3D 모델에서 직접 게이지 렌더링)
|
|
796
|
+
// null 반환으로 3D 오브젝트 생성을 억제하고, default factory fallback도 방지
|
|
797
|
+
registerRealObjectFactory('StockerCapacityBar', () => undefined as any)
|
|
798
|
+
registerRealObjectFactory('MCSGaugeCapacityBar', () => undefined as any)
|
|
799
|
+
registerRealObjectFactory('ZoneCapacityBar', () => undefined as any)
|
|
800
|
+
|
|
801
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
802
|
+
// PortFlow — 2D render를 Plane 텍스처로 3D 표시
|
|
803
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
804
|
+
|
|
805
|
+
class PortFlowRealObject3D extends RealObjectGroup {
|
|
806
|
+
private _mesh?: THREE.Mesh
|
|
807
|
+
private _texture?: THREE.CanvasTexture
|
|
808
|
+
|
|
809
|
+
get effectiveDepth(): number { return 0 }
|
|
810
|
+
protected get syncZPosOffset(): number {
|
|
811
|
+
// 부모 Port/Conveyor의 zPos와 동일하게
|
|
812
|
+
const parent = (this.component as any).parentObject
|
|
813
|
+
if (parent?.state?.zPos != null) return parent.state.zPos
|
|
814
|
+
return TRANSPORT_SURFACE_HEIGHT
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
build() {
|
|
818
|
+
super.build()
|
|
819
|
+
|
|
820
|
+
const { width = 10, height = 10 } = this.component.state
|
|
821
|
+
const w = Math.abs(width)
|
|
822
|
+
const h = Math.abs(height)
|
|
823
|
+
if (!w || !h) return
|
|
824
|
+
|
|
825
|
+
// 2D render를 캔버스에 캡처
|
|
826
|
+
// 화살표 날개 + 텍스트 영역이 bounds 밖으로 나가므로 전체 영역 계산
|
|
827
|
+
const { left, top } = this.component.bounds
|
|
828
|
+
const { fontSize = 24, labelPosition = 'top' } = this.component.state
|
|
829
|
+
const arrowPad = Math.min(w, h) * 0.4
|
|
830
|
+
|
|
831
|
+
// 텍스트 영역 확장
|
|
832
|
+
let padLeft = arrowPad, padRight = arrowPad, padTop = arrowPad, padBottom = arrowPad
|
|
833
|
+
if ((this.component as any).hasTextProperty) {
|
|
834
|
+
switch (labelPosition) {
|
|
835
|
+
case 'top': padTop = Math.max(padTop, fontSize + 4); break
|
|
836
|
+
case 'bottom': padBottom = Math.max(padBottom, fontSize + 4); break
|
|
837
|
+
case 'left': padLeft = Math.max(padLeft, fontSize * 4 + 4); break
|
|
838
|
+
case 'right': padRight = Math.max(padRight, fontSize * 4 + 4); break
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
const totalW = w + padLeft + padRight
|
|
843
|
+
const totalH = h + padTop + padBottom
|
|
844
|
+
const scale = 2
|
|
845
|
+
const canvas = document.createElement('canvas')
|
|
846
|
+
canvas.width = totalW * scale
|
|
847
|
+
canvas.height = totalH * scale
|
|
848
|
+
const ctx = canvas.getContext('2d')!
|
|
849
|
+
|
|
850
|
+
ctx.save()
|
|
851
|
+
ctx.scale(scale, scale)
|
|
852
|
+
ctx.translate(-left + padLeft, -top + padTop)
|
|
853
|
+
try {
|
|
854
|
+
// 화살표 렌더
|
|
855
|
+
this.component.render(ctx)
|
|
856
|
+
ctx.fillStyle = this.component.state.strokeStyle || '#666666'
|
|
857
|
+
ctx.fill()
|
|
858
|
+
// 텍스트 렌더
|
|
859
|
+
if ((this.component as any).hasTextProperty && (this.component as any).drawText) {
|
|
860
|
+
;(this.component as any).drawText(ctx)
|
|
861
|
+
}
|
|
862
|
+
} catch { /* render 실패 시 빈 텍스처 */ }
|
|
863
|
+
ctx.restore()
|
|
864
|
+
|
|
865
|
+
this._texture = new THREE.CanvasTexture(canvas)
|
|
866
|
+
this._texture.colorSpace = THREE.SRGBColorSpace
|
|
867
|
+
|
|
868
|
+
const geo = new THREE.PlaneGeometry(totalW, totalH)
|
|
869
|
+
geo.rotateX(-Math.PI / 2)
|
|
870
|
+
|
|
871
|
+
const mat = new THREE.MeshBasicMaterial({
|
|
872
|
+
map: this._texture,
|
|
873
|
+
alphaTest: 0.1,
|
|
874
|
+
side: THREE.DoubleSide,
|
|
875
|
+
})
|
|
876
|
+
|
|
877
|
+
this._mesh = new THREE.Mesh(geo, mat)
|
|
878
|
+
// 비대칭 패딩 보정: 플레인 센터를 컴포넌트 bounds 센터에 맞춤
|
|
879
|
+
this._mesh.position.x = (padRight - padLeft) / 2
|
|
880
|
+
this._mesh.position.y = 1
|
|
881
|
+
this._mesh.position.z = (padBottom - padTop) / 2
|
|
882
|
+
this.object3d.add(this._mesh)
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
update() {
|
|
886
|
+
if (this._mesh) {
|
|
887
|
+
this.object3d.remove(this._mesh)
|
|
888
|
+
this._mesh.geometry.dispose()
|
|
889
|
+
;(this._mesh.material as THREE.Material).dispose()
|
|
890
|
+
this._texture?.dispose()
|
|
891
|
+
this._mesh = undefined
|
|
892
|
+
this._texture = undefined
|
|
893
|
+
}
|
|
894
|
+
this.build()
|
|
895
|
+
this.updateTransform()
|
|
896
|
+
this.updateHidden()
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
onchange(after: Record<string, unknown>, before: Record<string, unknown>) {
|
|
900
|
+
if ('width' in after || 'height' in after || 'orientation' in after ||
|
|
901
|
+
'direction' in after || 'strokeStyle' in after || 'INOUTTYPE' in after) {
|
|
902
|
+
this.update()
|
|
903
|
+
return
|
|
904
|
+
}
|
|
905
|
+
super.onchange(after, before)
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
updateDimension() {}
|
|
909
|
+
updateAlpha() {}
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
registerRealObjectFactory('PortFlow', (component: Component) => new PortFlowRealObject3D(component))
|
|
913
|
+
|
|
914
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
915
|
+
// Node — 2D render를 바닥 Plane 텍스처로 3D 표시
|
|
916
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
917
|
+
|
|
918
|
+
class NodeRealObject3D extends RealObjectGroup {
|
|
919
|
+
private _mesh?: THREE.Mesh
|
|
920
|
+
private _texture?: THREE.CanvasTexture
|
|
921
|
+
|
|
922
|
+
get effectiveDepth(): number { return 0 }
|
|
923
|
+
protected get syncZPosOffset(): number { return 0 }
|
|
924
|
+
|
|
925
|
+
build() {
|
|
926
|
+
super.build()
|
|
927
|
+
|
|
928
|
+
const { cx, cy } = this.component.state
|
|
929
|
+
const radius = 8
|
|
930
|
+
const size = radius * 2
|
|
931
|
+
const scale = 2
|
|
932
|
+
const pad = 2
|
|
933
|
+
|
|
934
|
+
const canvas = document.createElement('canvas')
|
|
935
|
+
canvas.width = (size + pad * 2) * scale
|
|
936
|
+
canvas.height = (size + pad * 2) * scale
|
|
937
|
+
const ctx = canvas.getContext('2d')!
|
|
938
|
+
|
|
939
|
+
ctx.save()
|
|
940
|
+
ctx.scale(scale, scale)
|
|
941
|
+
ctx.translate(-cx + radius + pad, -cy + radius + pad)
|
|
942
|
+
try {
|
|
943
|
+
this.component.render(ctx)
|
|
944
|
+
const { fillStyle = '#888888' } = this.component.state
|
|
945
|
+
ctx.fillStyle = typeof fillStyle === 'string' ? fillStyle : '#888888'
|
|
946
|
+
ctx.fill()
|
|
947
|
+
const { strokeStyle } = this.component.state
|
|
948
|
+
if (strokeStyle && typeof strokeStyle === 'string') {
|
|
949
|
+
ctx.strokeStyle = strokeStyle
|
|
950
|
+
ctx.lineWidth = 1
|
|
951
|
+
ctx.stroke()
|
|
952
|
+
}
|
|
953
|
+
} catch { /* render 실패 시 빈 텍스처 */ }
|
|
954
|
+
ctx.restore()
|
|
955
|
+
|
|
956
|
+
this._texture = new THREE.CanvasTexture(canvas)
|
|
957
|
+
this._texture.colorSpace = THREE.SRGBColorSpace
|
|
958
|
+
|
|
959
|
+
const geo = new THREE.PlaneGeometry(size + pad * 2, size + pad * 2)
|
|
960
|
+
geo.rotateX(-Math.PI / 2)
|
|
961
|
+
|
|
962
|
+
const mat = new THREE.MeshBasicMaterial({
|
|
963
|
+
map: this._texture,
|
|
964
|
+
alphaTest: 0.1,
|
|
965
|
+
side: THREE.DoubleSide,
|
|
966
|
+
})
|
|
967
|
+
|
|
968
|
+
this._mesh = new THREE.Mesh(geo, mat)
|
|
969
|
+
this._mesh.position.y = 1
|
|
970
|
+
this.object3d.add(this._mesh)
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
update() {
|
|
974
|
+
if (this._mesh) {
|
|
975
|
+
this.object3d.remove(this._mesh)
|
|
976
|
+
this._mesh.geometry.dispose()
|
|
977
|
+
;(this._mesh.material as THREE.Material).dispose()
|
|
978
|
+
this._texture?.dispose()
|
|
979
|
+
this._mesh = undefined
|
|
980
|
+
this._texture = undefined
|
|
981
|
+
}
|
|
982
|
+
this.build()
|
|
983
|
+
this.updateTransform()
|
|
984
|
+
this.updateHidden()
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
onchange(after: Record<string, unknown>, before: Record<string, unknown>) {
|
|
988
|
+
if ('fillStyle' in after || 'strokeStyle' in after) {
|
|
989
|
+
this.update()
|
|
990
|
+
return
|
|
991
|
+
}
|
|
992
|
+
super.onchange(after, before)
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
updateDimension() {}
|
|
996
|
+
updateAlpha() {}
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
registerRealObjectFactory('Node', (component: Component) => new NodeRealObject3D(component))
|
|
1000
|
+
|
|
1001
|
+
// Scanner — 이송 표면 높이의 기둥 형태
|
|
1002
|
+
const SCANNER_CONFIG: MCS3DConfig = {
|
|
1003
|
+
...DEFAULTS,
|
|
1004
|
+
defaultDepth: TRANSPORT_SURFACE_HEIGHT * 0.2,
|
|
1005
|
+
defaultZPosFromParent: true,
|
|
1006
|
+
opacity: 1,
|
|
1007
|
+
metalness: 0.3,
|
|
1008
|
+
roughness: 0.7,
|
|
1009
|
+
defaultColor: 0x888888,
|
|
1010
|
+
shape: 'box',
|
|
1011
|
+
}
|
|
1012
|
+
registerRealObjectFactory('Scanner', (component: Component) => new MCSRealObject3D(component, SCANNER_CONFIG))
|