@fmsim/machine 2.0.0-beta.3 → 2.0.0-beta.31
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
|
@@ -9,8 +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, TriangleExtrude } from '@hatiolab/things-scene'
|
|
13
14
|
import type { Component } from '@hatiolab/things-scene'
|
|
15
|
+
import { LEGEND_CAPACITY } from '../features/mcs-status-default'
|
|
16
|
+
import { getVaueOnRanges } from '../utils/get-value-on-ranges'
|
|
14
17
|
|
|
15
18
|
// ── 설정 타입 ────────────────────────────────────────────
|
|
16
19
|
|
|
@@ -28,11 +31,23 @@ interface MCS3DConfig {
|
|
|
28
31
|
/** statusColor 미사용 시 기본 색상 */
|
|
29
32
|
defaultColor: number
|
|
30
33
|
/** 'box' | 'hexagon' */
|
|
31
|
-
shape: 'box' | 'hexagon'
|
|
34
|
+
shape: 'box' | 'hexagon' | 'plane'
|
|
32
35
|
/** mesh 생성 전 가시성 판정 — false 반환 시 mesh 미생성 */
|
|
33
36
|
isVisible?: (component: Component) => boolean
|
|
34
37
|
/** 부모 대비 크기 비율 (기본 1.0). < 1이면 부모 중심에 배치. */
|
|
35
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
|
|
36
51
|
}
|
|
37
52
|
|
|
38
53
|
/**
|
|
@@ -62,7 +77,7 @@ class MCSRealObject3D extends RealObjectGroup {
|
|
|
62
77
|
}
|
|
63
78
|
|
|
64
79
|
// width, height 절대값 (음수 값은 양수로 취급)
|
|
65
|
-
|
|
80
|
+
protected get absSize(): { w: number; h: number } {
|
|
66
81
|
const { width = 10, height = 10 } = this.component.state
|
|
67
82
|
return { w: Math.abs(width), h: Math.abs(height) }
|
|
68
83
|
}
|
|
@@ -76,14 +91,22 @@ class MCSRealObject3D extends RealObjectGroup {
|
|
|
76
91
|
}
|
|
77
92
|
|
|
78
93
|
protected get syncZPosOffset(): number {
|
|
94
|
+
// zPosOverride: 고정 높이 (OHTLine 등)
|
|
95
|
+
if (this._config.zPosOverride != null) {
|
|
96
|
+
return this._config.zPosOverride
|
|
97
|
+
}
|
|
79
98
|
// Port: 이송 표면 높이에 배치
|
|
80
99
|
if (this._config.defaultZPosFromParent) {
|
|
81
100
|
return TRANSPORT_SURFACE_HEIGHT
|
|
82
101
|
}
|
|
83
|
-
// Carrier:
|
|
102
|
+
// Carrier: 부모 유형에 따라 배치 위치 결정
|
|
84
103
|
if (this._config.sizeRatio && this._config.sizeRatio < 1) {
|
|
85
104
|
const parentType = this.component.parent?.state?.type
|
|
86
|
-
|
|
105
|
+
// OHT: 그리퍼 아래에 매달림 (캐리어 상단이 OHT 하단에 붙음)
|
|
106
|
+
if (parentType === 'OHT') {
|
|
107
|
+
return -this.effectiveDepth
|
|
108
|
+
}
|
|
109
|
+
if (parentType === 'Port' || parentType === 'Shelf' || parentType === 'Shuttle') {
|
|
87
110
|
return 0
|
|
88
111
|
}
|
|
89
112
|
return TRANSPORT_SURFACE_HEIGHT
|
|
@@ -92,21 +115,21 @@ class MCSRealObject3D extends RealObjectGroup {
|
|
|
92
115
|
}
|
|
93
116
|
|
|
94
117
|
// 컴포넌트의 상태 색상(statusColor) 또는 fillStyle → Three.js 색상 변환
|
|
95
|
-
|
|
118
|
+
protected resolveColor(): number {
|
|
96
119
|
const comp = this.component as any
|
|
97
120
|
|
|
98
121
|
// 1) MCSStatusMixin.statusColor (legend 기반 상태 색상)
|
|
99
122
|
// '#F0F0F0'은 상태 미설정 시 기본 fallback — 무시하고 defaultColor 사용
|
|
100
123
|
try {
|
|
101
124
|
const sc = comp.statusColor
|
|
102
|
-
if (typeof sc === 'string' && sc && sc.toUpperCase() !== '#F0F0F0') return new THREE.Color(sc).getHex()
|
|
125
|
+
if (typeof sc === 'string' && sc && sc.toUpperCase() !== '#F0F0F0' && sc !== 'transparent') return new THREE.Color(sc).getHex()
|
|
103
126
|
} catch {
|
|
104
127
|
/* statusColor 미구현 시 무시 */
|
|
105
128
|
}
|
|
106
129
|
|
|
107
130
|
// 2) state.fillStyle fallback
|
|
108
131
|
const { fillStyle } = this.component.state
|
|
109
|
-
if (fillStyle && typeof fillStyle === 'string') {
|
|
132
|
+
if (fillStyle && typeof fillStyle === 'string' && fillStyle !== 'transparent') {
|
|
110
133
|
try {
|
|
111
134
|
return new THREE.Color(fillStyle).getHex()
|
|
112
135
|
} catch {
|
|
@@ -155,7 +178,7 @@ class MCSRealObject3D extends RealObjectGroup {
|
|
|
155
178
|
|
|
156
179
|
private buildBoxGeometry(w: number, d: number, h: number): THREE.BufferGeometry {
|
|
157
180
|
const geo = new THREE.BoxGeometry(w, d, h)
|
|
158
|
-
return this.removeBottomFace(geo)
|
|
181
|
+
return this._config.keepBottomFace ? geo : this.removeBottomFace(geo)
|
|
159
182
|
}
|
|
160
183
|
|
|
161
184
|
private buildHexagonGeometry(w: number, d: number, h: number): THREE.BufferGeometry {
|
|
@@ -186,8 +209,16 @@ class MCSRealObject3D extends RealObjectGroup {
|
|
|
186
209
|
return this.removeBottomFace(geo)
|
|
187
210
|
}
|
|
188
211
|
|
|
212
|
+
private buildPlaneGeometry(w: number, h: number): THREE.BufferGeometry {
|
|
213
|
+
const geo = new THREE.PlaneGeometry(w, h)
|
|
214
|
+
geo.rotateX(-Math.PI / 2)
|
|
215
|
+
return geo
|
|
216
|
+
}
|
|
217
|
+
|
|
189
218
|
// ── Build ──
|
|
190
219
|
|
|
220
|
+
protected _ownMesh?: THREE.Mesh
|
|
221
|
+
|
|
191
222
|
build() {
|
|
192
223
|
super.build()
|
|
193
224
|
|
|
@@ -207,34 +238,64 @@ class MCSRealObject3D extends RealObjectGroup {
|
|
|
207
238
|
const { opacity, metalness, roughness, shape } = this._config
|
|
208
239
|
|
|
209
240
|
const geometry =
|
|
210
|
-
shape === '
|
|
211
|
-
? this.
|
|
212
|
-
:
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
241
|
+
shape === 'plane'
|
|
242
|
+
? this.buildPlaneGeometry(w, h)
|
|
243
|
+
: shape === 'hexagon'
|
|
244
|
+
? this.buildHexagonGeometry(w, depth, h)
|
|
245
|
+
: this.buildBoxGeometry(w, depth, h)
|
|
246
|
+
|
|
247
|
+
const isPlane = shape === 'plane'
|
|
248
|
+
const material = isPlane
|
|
249
|
+
? new THREE.MeshBasicMaterial({
|
|
250
|
+
color,
|
|
251
|
+
opacity,
|
|
252
|
+
transparent: opacity < 1,
|
|
253
|
+
side: THREE.DoubleSide,
|
|
254
|
+
})
|
|
255
|
+
: new THREE.MeshStandardMaterial({
|
|
256
|
+
color,
|
|
257
|
+
metalness,
|
|
258
|
+
roughness,
|
|
259
|
+
opacity,
|
|
260
|
+
transparent: opacity < 1,
|
|
261
|
+
side: opacity < 1 ? THREE.DoubleSide : THREE.FrontSide,
|
|
262
|
+
...(this._config.emissiveIntensity ? { emissive: color, emissiveIntensity: this._config.emissiveIntensity } : {}),
|
|
263
|
+
})
|
|
223
264
|
// z-fighting 방지: 바닥면(floor grid)보다 항상 앞에 렌더링
|
|
224
265
|
material.polygonOffset = true
|
|
225
266
|
material.polygonOffsetFactor = -1
|
|
226
267
|
material.polygonOffsetUnit = -1
|
|
227
268
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
269
|
+
// BoxGeometry material groups: +x(0), -x(1), +y(2), -y(3), +z(4), -z(5)
|
|
270
|
+
let meshMaterial: THREE.Material | THREE.Material[] = material
|
|
271
|
+
if (shape === 'box') {
|
|
272
|
+
if (this._config.openTop) {
|
|
273
|
+
// 상단면 제거 → 5면 개방 박스
|
|
274
|
+
const invisible = new THREE.MeshBasicMaterial({ visible: false })
|
|
275
|
+
meshMaterial = [material, material, invisible, material, material, material]
|
|
276
|
+
} else if (this._config.texturedTop) {
|
|
277
|
+
// 상판에 2D 렌더링 텍스처
|
|
278
|
+
const topMat = this.buildTopTexture(w, h)
|
|
279
|
+
meshMaterial = [material, material, topMat, material, material, material]
|
|
280
|
+
} else if (this._config.matteTop) {
|
|
281
|
+
// 상판만 무반사
|
|
282
|
+
const matte = new THREE.MeshBasicMaterial({ color, opacity, transparent: opacity < 1 })
|
|
283
|
+
meshMaterial = [material, material, matte, material, material, material]
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
this._ownMesh = new THREE.Mesh(geometry, meshMaterial)
|
|
288
|
+
// sizeRatio < 1이면 부모 중심에 배치 (rawDepth/2), plane은 살짝 띄움
|
|
289
|
+
this._ownMesh.position.y = isPlane ? 1 : ratio < 1 ? rawDepth / 2 : depth / 2
|
|
290
|
+
this._ownMesh.castShadow = !isPlane
|
|
291
|
+
this._ownMesh.receiveShadow = true
|
|
292
|
+
this.object3d.add(this._ownMesh)
|
|
234
293
|
}
|
|
235
294
|
|
|
236
295
|
// ── Change handling ──
|
|
237
296
|
|
|
297
|
+
private _updatePending = false
|
|
298
|
+
|
|
238
299
|
onchange(after: Record<string, unknown>, before: Record<string, unknown>) {
|
|
239
300
|
if (
|
|
240
301
|
'width' in after ||
|
|
@@ -242,18 +303,117 @@ class MCSRealObject3D extends RealObjectGroup {
|
|
|
242
303
|
'depth' in after ||
|
|
243
304
|
'fillStyle' in after ||
|
|
244
305
|
'strokeStyle' in after ||
|
|
245
|
-
'id' in after
|
|
306
|
+
'id' in after ||
|
|
307
|
+
'EMPTYTYPE' in after ||
|
|
308
|
+
'CARRIERSTATUS' in after
|
|
246
309
|
) {
|
|
247
|
-
|
|
310
|
+
// 연속 setState에 의한 다중 rebuild 방지: 한 프레임에 한 번만 실행
|
|
311
|
+
if (!this._updatePending) {
|
|
312
|
+
this._updatePending = true
|
|
313
|
+
queueMicrotask(() => {
|
|
314
|
+
this._updatePending = false
|
|
315
|
+
this.update()
|
|
316
|
+
})
|
|
317
|
+
}
|
|
248
318
|
return
|
|
249
319
|
}
|
|
250
320
|
|
|
251
321
|
super.onchange(after, before)
|
|
252
322
|
}
|
|
253
323
|
|
|
254
|
-
|
|
324
|
+
/**
|
|
325
|
+
* 상판 텍스처: 컴포넌트의 2D render()를 캔버스에 그려서 텍스처로 사용
|
|
326
|
+
*/
|
|
327
|
+
protected buildTopTexture(w: number, h: number): THREE.MeshBasicMaterial {
|
|
328
|
+
const scale = 4
|
|
329
|
+
const cw = this.nextPow2(w * scale)
|
|
330
|
+
const ch = this.nextPow2(h * scale)
|
|
331
|
+
const canvas = document.createElement('canvas')
|
|
332
|
+
canvas.width = cw
|
|
333
|
+
canvas.height = ch
|
|
334
|
+
const ctx = canvas.getContext('2d')!
|
|
335
|
+
|
|
336
|
+
const statusColor = '#' + this.resolveColor().toString(16).padStart(6, '0')
|
|
337
|
+
const emptyType = (this.component.state as any)?.EMPTYTYPE || ''
|
|
338
|
+
const text = emptyType === 'FULL' ? 'F' : emptyType === 'EMPTY' ? 'E' : emptyType === 'EMPTYEMPTY' ? 'X' : ''
|
|
339
|
+
|
|
340
|
+
// 실제 사용 영역 (nextPow2 이전 크기)
|
|
341
|
+
const usedW = w * scale
|
|
342
|
+
const usedH = h * scale
|
|
343
|
+
|
|
344
|
+
// 바탕: 캐리어 상태 색상 (사용 영역만)
|
|
345
|
+
ctx.fillStyle = statusColor
|
|
346
|
+
ctx.fillRect(0, 0, usedW, usedH)
|
|
347
|
+
|
|
348
|
+
// 텍스트 (사용 영역 중앙)
|
|
349
|
+
if (text) {
|
|
350
|
+
const fontSize = Math.round(Math.min(usedW, usedH) * 0.7)
|
|
351
|
+
ctx.font = `bold ${fontSize}px Arial`
|
|
352
|
+
ctx.textAlign = 'center'
|
|
353
|
+
ctx.textBaseline = 'middle'
|
|
354
|
+
ctx.fillStyle = '#000000'
|
|
355
|
+
ctx.fillText(text, usedW / 2, usedH / 2)
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const texture = new THREE.CanvasTexture(canvas)
|
|
359
|
+
texture.offset.set(0, 1 - usedH / ch)
|
|
360
|
+
texture.repeat.set(usedW / cw, usedH / ch)
|
|
361
|
+
texture.colorSpace = THREE.SRGBColorSpace
|
|
362
|
+
|
|
363
|
+
return new THREE.MeshBasicMaterial({
|
|
364
|
+
map: texture,
|
|
365
|
+
transparent: true,
|
|
366
|
+
side: THREE.DoubleSide,
|
|
367
|
+
})
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
private nextPow2(n: number): number {
|
|
371
|
+
if (!(n > 0)) return 1
|
|
372
|
+
let v = Math.ceil(n)
|
|
373
|
+
v--
|
|
374
|
+
v |= v >> 1; v |= v >> 2; v |= v >> 4; v |= v >> 8; v |= v >> 16
|
|
375
|
+
return v + 1
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* 자신의 mesh만 제거 후 rebuild.
|
|
380
|
+
* 기본 update()는 clear()로 object3d의 모든 children을 제거하는데,
|
|
381
|
+
* 컨테이너(Port 등)의 경우 자식 컴포넌트(Carrier 등)의 3D 오브젝트도 함께 제거됨.
|
|
382
|
+
* 이를 방지하기 위해 자신이 생성한 mesh만 제거한다.
|
|
383
|
+
*/
|
|
384
|
+
update() {
|
|
385
|
+
// 자신의 mesh만 제거 (자식 컴포넌트 3D 오브젝트 보존)
|
|
386
|
+
if (this._ownMesh) {
|
|
387
|
+
this.object3d.remove(this._ownMesh)
|
|
388
|
+
disposeObject3D(this._ownMesh)
|
|
389
|
+
this._ownMesh = undefined
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
this.build()
|
|
393
|
+
this.updateTransform()
|
|
394
|
+
this.updateAlpha()
|
|
395
|
+
this.updateHidden()
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// RealObjectGroup 기본 동작 비활성화
|
|
255
399
|
updateDimension() {}
|
|
256
|
-
|
|
400
|
+
|
|
401
|
+
updateAlpha() {
|
|
402
|
+
const state = this.component.state
|
|
403
|
+
// alpha가 명시적으로 설정되어 있으면 그 값을 사용, 없으면 config 기본값 적용
|
|
404
|
+
const effective = ('alpha' in state) ? (state.alpha as number) : this._config.opacity
|
|
405
|
+
|
|
406
|
+
this.object3d.traverse((o: any) => {
|
|
407
|
+
if (!o.material) return
|
|
408
|
+
const materials = Array.isArray(o.material) ? o.material : [o.material]
|
|
409
|
+
for (const m of materials) {
|
|
410
|
+
if (!m.visible) continue // openTop invisible face 무시
|
|
411
|
+
if (m.map) continue // 텍스처 material은 자체 transparent 유지
|
|
412
|
+
m.opacity = effective
|
|
413
|
+
m.transparent = effective < 1
|
|
414
|
+
}
|
|
415
|
+
})
|
|
416
|
+
}
|
|
257
417
|
}
|
|
258
418
|
|
|
259
419
|
// ── 팩토리 등록 헬퍼 ─────────────────────────────────────
|
|
@@ -269,7 +429,7 @@ function register(type: string, overrides: Partial<MCS3DConfig> = {}) {
|
|
|
269
429
|
|
|
270
430
|
register('Equipment', {
|
|
271
431
|
defaultDepth: 3 * TRANSPORT_SURFACE_HEIGHT,
|
|
272
|
-
opacity: 0.
|
|
432
|
+
opacity: 0.75,
|
|
273
433
|
metalness: 0.4,
|
|
274
434
|
roughness: 0.6,
|
|
275
435
|
defaultColor: 0x8899aa,
|
|
@@ -277,7 +437,7 @@ register('Equipment', {
|
|
|
277
437
|
|
|
278
438
|
register('Buffer', {
|
|
279
439
|
defaultDepth: TRANSPORT_SURFACE_HEIGHT,
|
|
280
|
-
opacity: 0
|
|
440
|
+
opacity: 1.0,
|
|
281
441
|
metalness: 0.3,
|
|
282
442
|
roughness: 0.7,
|
|
283
443
|
defaultColor: 0x99aa88,
|
|
@@ -285,39 +445,302 @@ register('Buffer', {
|
|
|
285
445
|
})
|
|
286
446
|
|
|
287
447
|
register('AGVLine', {
|
|
288
|
-
depthRatio: 0
|
|
448
|
+
depthRatio: 0,
|
|
449
|
+
defaultDepth: 0.1,
|
|
289
450
|
metalness: 0.6,
|
|
290
451
|
roughness: 0.3,
|
|
291
452
|
defaultColor: 0x888899,
|
|
292
453
|
})
|
|
293
454
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
455
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
456
|
+
// OHTLine — 천장 I-beam 레일
|
|
457
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
458
|
+
|
|
459
|
+
const OHT_LINE_RAIL_COLOR = 0xcccccc
|
|
460
|
+
const OHT_LINE_HEIGHT = 7 * TRANSPORT_SURFACE_HEIGHT // OHT 상단과 일치
|
|
461
|
+
|
|
462
|
+
class OHTLineRealObject3D extends RealObjectGroup {
|
|
463
|
+
private _mesh?: THREE.Mesh
|
|
464
|
+
|
|
465
|
+
get effectiveDepth(): number { return 0 }
|
|
466
|
+
protected get syncZPosOffset(): number { return OHT_LINE_HEIGHT }
|
|
467
|
+
|
|
468
|
+
build() {
|
|
469
|
+
super.build()
|
|
470
|
+
|
|
471
|
+
const { width = 10, height = 10 } = this.component.state
|
|
472
|
+
const w = Math.abs(width)
|
|
473
|
+
const h = Math.abs(height)
|
|
474
|
+
if (!w || !h) return
|
|
475
|
+
|
|
476
|
+
// I-beam 치수: 이송 방향 = 긴 변, 폭은 2D의 1/2
|
|
477
|
+
const isVertical = w < h
|
|
478
|
+
const shortSide = isVertical ? w : h // 2D 원본 짧은 쪽 폭
|
|
479
|
+
const beltW = shortSide * 0.25 // I-beam 폭: 원본의 1/4
|
|
480
|
+
const beamH = Math.max(beltW * 0.5, 3) // I-beam 전체 높이
|
|
481
|
+
const inset = shortSide * 0.5 // 양끝 여백: 원본 폭의 1/2
|
|
482
|
+
const railLen = (isVertical ? h : w) - inset * 2
|
|
483
|
+
const flangeH = beamH * 0.2 // 플랜지 두께
|
|
484
|
+
const flangeW = beltW * 0.8 // 플랜지 폭
|
|
485
|
+
const webH = beamH - flangeH * 2 // 웹 높이
|
|
486
|
+
const webW = Math.max(flangeW * 0.25, 1) // 웹 두께
|
|
487
|
+
|
|
488
|
+
const mat = new THREE.MeshStandardMaterial({
|
|
489
|
+
color: OHT_LINE_RAIL_COLOR,
|
|
490
|
+
metalness: 0.5,
|
|
491
|
+
roughness: 0.4,
|
|
492
|
+
})
|
|
493
|
+
|
|
494
|
+
const geoms: THREE.BufferGeometry[] = []
|
|
495
|
+
|
|
496
|
+
// 상부 플랜지
|
|
497
|
+
const topFlange = isVertical
|
|
498
|
+
? new THREE.BoxGeometry(flangeW, flangeH, railLen)
|
|
499
|
+
: new THREE.BoxGeometry(railLen, flangeH, flangeW)
|
|
500
|
+
topFlange.translate(0, -flangeH / 2, 0)
|
|
501
|
+
geoms.push(topFlange)
|
|
502
|
+
|
|
503
|
+
// 웹 (수직판)
|
|
504
|
+
const web = isVertical
|
|
505
|
+
? new THREE.BoxGeometry(webW, webH, railLen)
|
|
506
|
+
: new THREE.BoxGeometry(railLen, webH, webW)
|
|
507
|
+
web.translate(0, -flangeH - webH / 2, 0)
|
|
508
|
+
geoms.push(web)
|
|
509
|
+
|
|
510
|
+
// 하부 플랜지
|
|
511
|
+
const botFlange = isVertical
|
|
512
|
+
? new THREE.BoxGeometry(flangeW, flangeH, railLen)
|
|
513
|
+
: new THREE.BoxGeometry(railLen, flangeH, flangeW)
|
|
514
|
+
botFlange.translate(0, -flangeH - webH - flangeH / 2, 0)
|
|
515
|
+
geoms.push(botFlange)
|
|
516
|
+
|
|
517
|
+
this._mesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(geoms), mat)
|
|
518
|
+
this._mesh.castShadow = true
|
|
519
|
+
this.object3d.add(this._mesh)
|
|
520
|
+
}
|
|
300
521
|
|
|
301
|
-
|
|
522
|
+
update() {
|
|
523
|
+
if (this._mesh) {
|
|
524
|
+
this.object3d.remove(this._mesh)
|
|
525
|
+
this._mesh.geometry.dispose()
|
|
526
|
+
;(this._mesh.material as THREE.Material).dispose()
|
|
527
|
+
this._mesh = undefined
|
|
528
|
+
}
|
|
529
|
+
this.build()
|
|
530
|
+
this.updateTransform()
|
|
531
|
+
this.updateHidden()
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
onchange(after: Record<string, unknown>, before: Record<string, unknown>) {
|
|
535
|
+
if ('width' in after || 'height' in after || 'depth' in after ||
|
|
536
|
+
'fillStyle' in after || 'strokeStyle' in after) {
|
|
537
|
+
this.update()
|
|
538
|
+
return
|
|
539
|
+
}
|
|
540
|
+
super.onchange(after, before)
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
updateDimension() {}
|
|
544
|
+
updateAlpha() {}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
registerRealObjectFactory('OHTLine', (component: Component) => new OHTLineRealObject3D(component))
|
|
548
|
+
|
|
549
|
+
// STOCKER: 벽면에 용량 게이지 바를 포함하는 커스텀 3D 오브젝트
|
|
550
|
+
const STOCKER_CONFIG: MCS3DConfig = {
|
|
551
|
+
...DEFAULTS,
|
|
302
552
|
defaultDepth: 5 * TRANSPORT_SURFACE_HEIGHT,
|
|
303
|
-
opacity: 0.
|
|
553
|
+
opacity: 0.75,
|
|
304
554
|
metalness: 0.3,
|
|
305
555
|
roughness: 0.6,
|
|
306
556
|
defaultColor: 0x8899bb,
|
|
307
|
-
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
class StockerRealObject3D extends MCSRealObject3D {
|
|
560
|
+
private gaugeMesh?: THREE.Mesh
|
|
561
|
+
|
|
562
|
+
constructor(component: Component) {
|
|
563
|
+
super(component, STOCKER_CONFIG)
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
build() {
|
|
567
|
+
super.build()
|
|
568
|
+
this.buildGauge()
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
private buildGauge() {
|
|
572
|
+
const comp = this.component as any
|
|
573
|
+
const data = comp.data || {}
|
|
574
|
+
const gaugeMode = comp.root?.style?.stockerGaugeMode || 'capacity'
|
|
575
|
+
|
|
576
|
+
const gauge = this.resolveGauge(data, gaugeMode)
|
|
577
|
+
|
|
578
|
+
// 테두리는 스토커의 상태 색상 (machine status)
|
|
579
|
+
const statusColor = comp.statusColor
|
|
580
|
+
if (statusColor && statusColor.toUpperCase() !== '#F0F0F0') {
|
|
581
|
+
gauge.borderColor = statusColor
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
const { w, h } = this.absSize
|
|
585
|
+
const depth = this.effectiveDepth
|
|
586
|
+
const isWidthLonger = w >= h
|
|
587
|
+
|
|
588
|
+
// 게이지 캔버스 (고해상도)
|
|
589
|
+
const barW = 130
|
|
590
|
+
const barH = 26
|
|
591
|
+
const scale = 4
|
|
592
|
+
const canvasW = barW * scale
|
|
593
|
+
const canvasH = barH * scale
|
|
594
|
+
const canvas = document.createElement('canvas')
|
|
595
|
+
canvas.width = canvasW
|
|
596
|
+
canvas.height = canvasH
|
|
597
|
+
const ctx = canvas.getContext('2d')!
|
|
598
|
+
|
|
599
|
+
const pad = 12
|
|
600
|
+
|
|
601
|
+
// 배경 + 테두리
|
|
602
|
+
ctx.fillStyle = '#dddddd'
|
|
603
|
+
ctx.fillRect(0, 0, canvasW, canvasH)
|
|
604
|
+
ctx.strokeStyle = gauge.borderColor
|
|
605
|
+
ctx.lineWidth = 12
|
|
606
|
+
ctx.strokeRect(6, 6, canvasW - 12, canvasH - 12)
|
|
607
|
+
|
|
608
|
+
// 세그먼트 채움 (왼→오)
|
|
609
|
+
const innerW = canvasW - pad * 2
|
|
610
|
+
const innerH = canvasH - pad * 2
|
|
611
|
+
let x = pad
|
|
612
|
+
for (const seg of gauge.segments) {
|
|
613
|
+
const segW = Math.max(innerW * seg.ratio, seg.ratio > 0 ? 1 : 0)
|
|
614
|
+
if (segW <= 0) continue
|
|
615
|
+
ctx.fillStyle = seg.color
|
|
616
|
+
ctx.fillRect(x, pad, segW, innerH)
|
|
617
|
+
x += segW
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// 텍스트
|
|
621
|
+
ctx.font = `bold ${10 * scale}px sans-serif`
|
|
622
|
+
ctx.textAlign = 'center'
|
|
623
|
+
ctx.textBaseline = 'middle'
|
|
624
|
+
ctx.fillStyle = '#333333'
|
|
625
|
+
ctx.fillText(gauge.label, canvasW / 2, canvasH / 2)
|
|
626
|
+
|
|
627
|
+
// 텍스처 → Plane
|
|
628
|
+
const texture = new THREE.CanvasTexture(canvas)
|
|
629
|
+
texture.colorSpace = THREE.SRGBColorSpace
|
|
630
|
+
|
|
631
|
+
const geo = new THREE.PlaneGeometry(barW, barH)
|
|
632
|
+
geo.rotateX(-Math.PI / 2) // 수평 (지붕 위)
|
|
633
|
+
if (!isWidthLonger) geo.rotateY(Math.PI / 2)
|
|
634
|
+
|
|
635
|
+
const mat = new THREE.MeshBasicMaterial({
|
|
636
|
+
map: texture,
|
|
637
|
+
transparent: true,
|
|
638
|
+
side: THREE.DoubleSide,
|
|
639
|
+
})
|
|
640
|
+
mat.polygonOffset = true
|
|
641
|
+
mat.polygonOffsetFactor = -2
|
|
642
|
+
mat.polygonOffsetUnit = -2
|
|
643
|
+
|
|
644
|
+
this.gaugeMesh = new THREE.Mesh(geo, mat)
|
|
645
|
+
this.gaugeMesh.position.set(0, depth + 0.5, 0)
|
|
646
|
+
this.object3d.add(this.gaugeMesh)
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
private resolveGauge(data: any, mode: string) {
|
|
650
|
+
switch (mode) {
|
|
651
|
+
case 'carrier': {
|
|
652
|
+
const full = Number(data.FULL_CNT) || 0
|
|
653
|
+
const empty = Number(data.EMPTY_CNT) || 0
|
|
654
|
+
const emptyEmpty = Number(data.EMPTYEMPTY_CNT) || 0
|
|
655
|
+
const total = full + empty + emptyEmpty
|
|
656
|
+
const segments = total > 0
|
|
657
|
+
? [
|
|
658
|
+
{ ratio: full / total, color: '#4AA7FE' },
|
|
659
|
+
{ ratio: empty / total, color: '#8BDA5B' },
|
|
660
|
+
{ ratio: emptyEmpty / total, color: '#019D59' },
|
|
661
|
+
]
|
|
662
|
+
: [{ ratio: 1, color: '#dddddd' }]
|
|
663
|
+
return {
|
|
664
|
+
segments,
|
|
665
|
+
borderColor: '#4AA7FE',
|
|
666
|
+
label: total > 0 ? `F${full} E${empty} EE${emptyEmpty}` : '',
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
case 'abnormal': {
|
|
670
|
+
const normal = Number(data.CARRIER_NORMAL) || 0
|
|
671
|
+
const abnormal = Number(data.CARRIER_ABNORMAL) || 0
|
|
672
|
+
const total = normal + abnormal
|
|
673
|
+
const segments = total > 0
|
|
674
|
+
? [
|
|
675
|
+
{ ratio: normal / total, color: '#8BDA5B' },
|
|
676
|
+
{ ratio: abnormal / total, color: '#F4B4B4' },
|
|
677
|
+
]
|
|
678
|
+
: [{ ratio: 1, color: '#dddddd' }]
|
|
679
|
+
return {
|
|
680
|
+
segments,
|
|
681
|
+
borderColor: abnormal > 0 ? '#F4B4B4' : '#8BDA5B',
|
|
682
|
+
label: total > 0 ? `N${normal} A${abnormal}` : '',
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
default: { // capacity
|
|
686
|
+
const cur = Number(data.CURRENTCAPACITY) || 0
|
|
687
|
+
const max = Number(data.MAXCAPACITY) || 100
|
|
688
|
+
const fullup = data.FULLUP === 'T'
|
|
689
|
+
const usage = Math.round((cur / max) * 100)
|
|
690
|
+
const legend = (this.component as any).root?.style?.LEGEND_CAPACITY || LEGEND_CAPACITY
|
|
691
|
+
const color = fullup ? 'red' : (getVaueOnRanges(usage, legend) || '#44aa44')
|
|
692
|
+
return {
|
|
693
|
+
segments: [{ ratio: usage / 100, color }],
|
|
694
|
+
borderColor: color,
|
|
695
|
+
label: `${usage}%`,
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
onchange(after: Record<string, unknown>, before: Record<string, unknown>) {
|
|
702
|
+
if ('data' in after) {
|
|
703
|
+
this.clearGauge()
|
|
704
|
+
this.buildGauge()
|
|
705
|
+
}
|
|
706
|
+
super.onchange(after, before)
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
clear() {
|
|
710
|
+
this.clearGauge()
|
|
711
|
+
return super.clear()
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
private clearGauge() {
|
|
715
|
+
if (this.gaugeMesh) {
|
|
716
|
+
this.object3d.remove(this.gaugeMesh)
|
|
717
|
+
this.gaugeMesh.geometry?.dispose()
|
|
718
|
+
const mat = this.gaugeMesh.material as THREE.MeshBasicMaterial
|
|
719
|
+
mat?.map?.dispose()
|
|
720
|
+
mat?.dispose()
|
|
721
|
+
this.gaugeMesh = undefined
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
registerRealObjectFactory('STOCKER', (component: Component) => new StockerRealObject3D(component))
|
|
308
727
|
|
|
309
728
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
310
729
|
// Unit (이동체 · 홀더) — 낮은 depth, 불투명
|
|
311
730
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
312
731
|
|
|
313
|
-
|
|
314
|
-
|
|
732
|
+
// Port: 바닥판 (PlaneGeometry)
|
|
733
|
+
const PORT_CONFIG: MCS3DConfig = {
|
|
734
|
+
...DEFAULTS,
|
|
735
|
+
depthRatio: 0.01,
|
|
315
736
|
defaultZPosFromParent: true,
|
|
316
|
-
opacity: 0.
|
|
317
|
-
metalness: 0.
|
|
318
|
-
roughness: 0.
|
|
737
|
+
opacity: 0.1,
|
|
738
|
+
metalness: 0.3,
|
|
739
|
+
roughness: 0.8,
|
|
319
740
|
defaultColor: 0xb0a090,
|
|
320
|
-
|
|
741
|
+
shape: 'plane',
|
|
742
|
+
}
|
|
743
|
+
registerRealObjectFactory('Port', (component: Component) => new MCSRealObject3D(component, PORT_CONFIG))
|
|
321
744
|
|
|
322
745
|
register('Shelf', {
|
|
323
746
|
depthRatio: 0.25,
|
|
@@ -326,36 +749,249 @@ register('Shelf', {
|
|
|
326
749
|
defaultColor: 0xaabbcc,
|
|
327
750
|
})
|
|
328
751
|
|
|
329
|
-
|
|
752
|
+
// Carrier: CARRIERSTATUS에 따른 3D 애니메이션 지원
|
|
753
|
+
const CARRIER_CONFIG: MCS3DConfig = {
|
|
754
|
+
...DEFAULTS,
|
|
330
755
|
depthRatio: 0.5,
|
|
331
|
-
metalness: 0
|
|
332
|
-
roughness:
|
|
756
|
+
metalness: 0,
|
|
757
|
+
roughness: 1,
|
|
333
758
|
defaultColor: 0x66bb66,
|
|
334
759
|
isVisible: (c: Component) => !!(c.state as any).id,
|
|
335
760
|
sizeRatio: 0.8,
|
|
336
|
-
|
|
761
|
+
texturedTop: true,
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
class CarrierRealObject3D extends MCSRealObject3D {
|
|
765
|
+
constructor(component: Component) {
|
|
766
|
+
super(component, CARRIER_CONFIG)
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
registerRealObjectFactory('Carrier', (component: Component) => new CarrierRealObject3D(component))
|
|
337
771
|
|
|
338
772
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
339
773
|
// Gauge (용량 게이지) — 얇은 바
|
|
340
774
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
341
775
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
roughness: 0.8,
|
|
346
|
-
defaultColor: 0x44aa44,
|
|
347
|
-
})
|
|
776
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
777
|
+
// Gauge (용량 게이지) — 2D 렌더링을 Plane 텍스처로 3D 표시
|
|
778
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
348
779
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
})
|
|
780
|
+
// Gauge 컴포넌트 (StockerCapacityBar, ZoneCapacityBar, MCSGaugeCapacityBar)는
|
|
781
|
+
// 3D에서 표시하지 않음. 대신 부모(STOCKER 등)의 3D 모델에서 직접 게이지를 렌더링.
|
|
782
|
+
registerRealObjectFactory('StockerCapacityBar', () => null as any)
|
|
783
|
+
registerRealObjectFactory('MCSGaugeCapacityBar', () => null as any)
|
|
784
|
+
registerRealObjectFactory('ZoneCapacityBar', () => null as any)
|
|
355
785
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
786
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
787
|
+
// PortFlow — 2D render를 Plane 텍스처로 3D 표시
|
|
788
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
789
|
+
|
|
790
|
+
class PortFlowRealObject3D extends RealObjectGroup {
|
|
791
|
+
private _mesh?: THREE.Mesh
|
|
792
|
+
private _texture?: THREE.CanvasTexture
|
|
793
|
+
|
|
794
|
+
get effectiveDepth(): number { return 0 }
|
|
795
|
+
protected get syncZPosOffset(): number {
|
|
796
|
+
// 부모 Port/Conveyor의 zPos와 동일하게
|
|
797
|
+
const parent = (this.component as any).parentObject
|
|
798
|
+
if (parent?.state?.zPos != null) return parent.state.zPos
|
|
799
|
+
return TRANSPORT_SURFACE_HEIGHT
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
build() {
|
|
803
|
+
super.build()
|
|
804
|
+
|
|
805
|
+
const { width = 10, height = 10 } = this.component.state
|
|
806
|
+
const w = Math.abs(width)
|
|
807
|
+
const h = Math.abs(height)
|
|
808
|
+
if (!w || !h) return
|
|
809
|
+
|
|
810
|
+
// 2D render를 캔버스에 캡처
|
|
811
|
+
// 화살표 날개 + 텍스트 영역이 bounds 밖으로 나가므로 전체 영역 계산
|
|
812
|
+
const { left, top } = this.component.bounds
|
|
813
|
+
const { fontSize = 24, labelPosition = 'top' } = this.component.state
|
|
814
|
+
const arrowPad = Math.min(w, h) * 0.4
|
|
815
|
+
|
|
816
|
+
// 텍스트 영역 확장
|
|
817
|
+
let padLeft = arrowPad, padRight = arrowPad, padTop = arrowPad, padBottom = arrowPad
|
|
818
|
+
if ((this.component as any).hasTextProperty) {
|
|
819
|
+
switch (labelPosition) {
|
|
820
|
+
case 'top': padTop = Math.max(padTop, fontSize + 4); break
|
|
821
|
+
case 'bottom': padBottom = Math.max(padBottom, fontSize + 4); break
|
|
822
|
+
case 'left': padLeft = Math.max(padLeft, fontSize * 4 + 4); break
|
|
823
|
+
case 'right': padRight = Math.max(padRight, fontSize * 4 + 4); break
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
const totalW = w + padLeft + padRight
|
|
828
|
+
const totalH = h + padTop + padBottom
|
|
829
|
+
const scale = 2
|
|
830
|
+
const canvas = document.createElement('canvas')
|
|
831
|
+
canvas.width = totalW * scale
|
|
832
|
+
canvas.height = totalH * scale
|
|
833
|
+
const ctx = canvas.getContext('2d')!
|
|
834
|
+
|
|
835
|
+
ctx.save()
|
|
836
|
+
ctx.scale(scale, scale)
|
|
837
|
+
ctx.translate(-left + padLeft, -top + padTop)
|
|
838
|
+
try {
|
|
839
|
+
// 화살표 렌더
|
|
840
|
+
this.component.render(ctx)
|
|
841
|
+
ctx.fillStyle = this.component.state.strokeStyle || '#666666'
|
|
842
|
+
ctx.fill()
|
|
843
|
+
// 텍스트 렌더
|
|
844
|
+
if ((this.component as any).hasTextProperty && (this.component as any).drawText) {
|
|
845
|
+
;(this.component as any).drawText(ctx)
|
|
846
|
+
}
|
|
847
|
+
} catch { /* render 실패 시 빈 텍스처 */ }
|
|
848
|
+
ctx.restore()
|
|
849
|
+
|
|
850
|
+
this._texture = new THREE.CanvasTexture(canvas)
|
|
851
|
+
this._texture.colorSpace = THREE.SRGBColorSpace
|
|
852
|
+
|
|
853
|
+
const geo = new THREE.PlaneGeometry(totalW, totalH)
|
|
854
|
+
geo.rotateX(-Math.PI / 2)
|
|
855
|
+
|
|
856
|
+
const mat = new THREE.MeshBasicMaterial({
|
|
857
|
+
map: this._texture,
|
|
858
|
+
alphaTest: 0.1,
|
|
859
|
+
side: THREE.DoubleSide,
|
|
860
|
+
})
|
|
861
|
+
|
|
862
|
+
this._mesh = new THREE.Mesh(geo, mat)
|
|
863
|
+
// 비대칭 패딩 보정: 플레인 센터를 컴포넌트 bounds 센터에 맞춤
|
|
864
|
+
this._mesh.position.x = (padRight - padLeft) / 2
|
|
865
|
+
this._mesh.position.y = 1
|
|
866
|
+
this._mesh.position.z = (padBottom - padTop) / 2
|
|
867
|
+
this.object3d.add(this._mesh)
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
update() {
|
|
871
|
+
if (this._mesh) {
|
|
872
|
+
this.object3d.remove(this._mesh)
|
|
873
|
+
this._mesh.geometry.dispose()
|
|
874
|
+
;(this._mesh.material as THREE.Material).dispose()
|
|
875
|
+
this._texture?.dispose()
|
|
876
|
+
this._mesh = undefined
|
|
877
|
+
this._texture = undefined
|
|
878
|
+
}
|
|
879
|
+
this.build()
|
|
880
|
+
this.updateTransform()
|
|
881
|
+
this.updateHidden()
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
onchange(after: Record<string, unknown>, before: Record<string, unknown>) {
|
|
885
|
+
if ('width' in after || 'height' in after || 'orientation' in after ||
|
|
886
|
+
'direction' in after || 'strokeStyle' in after || 'INOUTTYPE' in after) {
|
|
887
|
+
this.update()
|
|
888
|
+
return
|
|
889
|
+
}
|
|
890
|
+
super.onchange(after, before)
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
updateDimension() {}
|
|
894
|
+
updateAlpha() {}
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
registerRealObjectFactory('PortFlow', (component: Component) => new PortFlowRealObject3D(component))
|
|
898
|
+
|
|
899
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
900
|
+
// Node — 2D render를 바닥 Plane 텍스처로 3D 표시
|
|
901
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
902
|
+
|
|
903
|
+
class NodeRealObject3D extends RealObjectGroup {
|
|
904
|
+
private _mesh?: THREE.Mesh
|
|
905
|
+
private _texture?: THREE.CanvasTexture
|
|
906
|
+
|
|
907
|
+
get effectiveDepth(): number { return 0 }
|
|
908
|
+
protected get syncZPosOffset(): number { return 0 }
|
|
909
|
+
|
|
910
|
+
build() {
|
|
911
|
+
super.build()
|
|
912
|
+
|
|
913
|
+
const { cx, cy } = this.component.state
|
|
914
|
+
const radius = 8
|
|
915
|
+
const size = radius * 2
|
|
916
|
+
const scale = 2
|
|
917
|
+
const pad = 2
|
|
918
|
+
|
|
919
|
+
const canvas = document.createElement('canvas')
|
|
920
|
+
canvas.width = (size + pad * 2) * scale
|
|
921
|
+
canvas.height = (size + pad * 2) * scale
|
|
922
|
+
const ctx = canvas.getContext('2d')!
|
|
923
|
+
|
|
924
|
+
ctx.save()
|
|
925
|
+
ctx.scale(scale, scale)
|
|
926
|
+
ctx.translate(-cx + radius + pad, -cy + radius + pad)
|
|
927
|
+
try {
|
|
928
|
+
this.component.render(ctx)
|
|
929
|
+
const { fillStyle = '#888888' } = this.component.state
|
|
930
|
+
ctx.fillStyle = typeof fillStyle === 'string' ? fillStyle : '#888888'
|
|
931
|
+
ctx.fill()
|
|
932
|
+
const { strokeStyle } = this.component.state
|
|
933
|
+
if (strokeStyle && typeof strokeStyle === 'string') {
|
|
934
|
+
ctx.strokeStyle = strokeStyle
|
|
935
|
+
ctx.lineWidth = 1
|
|
936
|
+
ctx.stroke()
|
|
937
|
+
}
|
|
938
|
+
} catch { /* render 실패 시 빈 텍스처 */ }
|
|
939
|
+
ctx.restore()
|
|
940
|
+
|
|
941
|
+
this._texture = new THREE.CanvasTexture(canvas)
|
|
942
|
+
this._texture.colorSpace = THREE.SRGBColorSpace
|
|
943
|
+
|
|
944
|
+
const geo = new THREE.PlaneGeometry(size + pad * 2, size + pad * 2)
|
|
945
|
+
geo.rotateX(-Math.PI / 2)
|
|
946
|
+
|
|
947
|
+
const mat = new THREE.MeshBasicMaterial({
|
|
948
|
+
map: this._texture,
|
|
949
|
+
alphaTest: 0.1,
|
|
950
|
+
side: THREE.DoubleSide,
|
|
951
|
+
})
|
|
952
|
+
|
|
953
|
+
this._mesh = new THREE.Mesh(geo, mat)
|
|
954
|
+
this._mesh.position.y = 1
|
|
955
|
+
this.object3d.add(this._mesh)
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
update() {
|
|
959
|
+
if (this._mesh) {
|
|
960
|
+
this.object3d.remove(this._mesh)
|
|
961
|
+
this._mesh.geometry.dispose()
|
|
962
|
+
;(this._mesh.material as THREE.Material).dispose()
|
|
963
|
+
this._texture?.dispose()
|
|
964
|
+
this._mesh = undefined
|
|
965
|
+
this._texture = undefined
|
|
966
|
+
}
|
|
967
|
+
this.build()
|
|
968
|
+
this.updateTransform()
|
|
969
|
+
this.updateHidden()
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
onchange(after: Record<string, unknown>, before: Record<string, unknown>) {
|
|
973
|
+
if ('fillStyle' in after || 'strokeStyle' in after) {
|
|
974
|
+
this.update()
|
|
975
|
+
return
|
|
976
|
+
}
|
|
977
|
+
super.onchange(after, before)
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
updateDimension() {}
|
|
981
|
+
updateAlpha() {}
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
registerRealObjectFactory('Node', (component: Component) => new NodeRealObject3D(component))
|
|
985
|
+
|
|
986
|
+
// Scanner — 이송 표면 높이의 기둥 형태
|
|
987
|
+
const SCANNER_CONFIG: MCS3DConfig = {
|
|
988
|
+
...DEFAULTS,
|
|
989
|
+
defaultDepth: TRANSPORT_SURFACE_HEIGHT * 0.2,
|
|
990
|
+
defaultZPosFromParent: true,
|
|
991
|
+
opacity: 1,
|
|
992
|
+
metalness: 0.3,
|
|
993
|
+
roughness: 0.7,
|
|
994
|
+
defaultColor: 0x888888,
|
|
995
|
+
shape: 'box',
|
|
996
|
+
}
|
|
997
|
+
registerRealObjectFactory('Scanner', (component: Component) => new MCSRealObject3D(component, SCANNER_CONFIG))
|