@fmsim/machine 2.0.0-beta.7 → 2.0.0-beta.75
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 +81 -5
- 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 +8 -9
- 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 +384 -133
- package/dist/factories/machine-3d.js.map +1 -1
- package/dist/factories/oht-3d.js +20 -20
- package/dist/factories/oht-3d.js.map +1 -1
- package/dist/factories/shuttle-3d.js +2 -5
- package/dist/factories/shuttle-3d.js.map +1 -1
- package/dist/features/conveyor-mixin.js.map +1 -1
- package/dist/features/mcs-status-mixin.js +22 -0
- package/dist/features/mcs-status-mixin.js.map +1 -1
- package/dist/features/mcs-zone-mixin.js.map +1 -1
- package/dist/features/parent-object-mixin.js.map +1 -1
- package/dist/floating-container.js +5 -0
- package/dist/floating-container.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-machine.js +28 -0
- package/dist/mcs-machine.js.map +1 -1
- package/dist/mcs-unit.js +10 -0
- package/dist/mcs-unit.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/static-container.js +5 -0
- package/dist/static-container.js.map +1 -1
- package/dist/stocker-abnormal-bar.js +1 -1
- package/dist/stocker-abnormal-bar.js.map +1 -1
- package/dist/stocker-capacity-bar.js +3 -3
- package/dist/stocker-capacity-bar.js.map +1 -1
- package/dist/stocker-carrier-bar.js +1 -1
- package/dist/stocker-carrier-bar.js.map +1 -1
- package/dist/stocker.js +6 -0
- package/dist/stocker.js.map +1 -1
- package/dist/templates/backdrop.js +30 -0
- package/dist/templates/backdrop.js.map +1 -0
- package/dist/templates/decal.js +21 -0
- package/dist/templates/decal.js.map +1 -0
- package/dist/templates/index.js +7 -1
- 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 +3 -3
- package/dist/zone-capacity-bar.js.map +1 -1
- package/icons/backdrop.png +0 -0
- package/icons/decal.png +0 -0
- 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 +80 -3
- 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 +9 -10
- 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 +452 -145
- package/src/factories/oht-3d.ts +20 -20
- package/src/factories/shuttle-3d.ts +2 -6
- package/src/features/conveyor-mixin.ts +1 -1
- package/src/features/mcs-status-mixin.ts +26 -4
- package/src/features/mcs-zone-mixin.ts +1 -1
- package/src/features/parent-object-mixin.ts +1 -1
- package/src/floating-container.ts +5 -0
- package/src/index.ts +56 -34
- package/src/mcs-carrier-holder.ts +38 -34
- package/src/mcs-machine.ts +29 -0
- package/src/mcs-unit.ts +12 -1
- 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/static-container.ts +5 -0
- package/src/stocker-abnormal-bar.ts +1 -1
- package/src/stocker-capacity-bar.ts +3 -3
- package/src/stocker-carrier-bar.ts +1 -1
- package/src/stocker.ts +7 -0
- package/src/templates/backdrop.ts +30 -0
- package/src/templates/decal.ts +21 -0
- package/src/templates/index.ts +7 -1
- package/src/templates/scanner.ts +20 -0
- package/src/utils/get-value-on-ranges.ts +2 -4
- package/src/zone-capacity-bar.ts +3 -3
- package/translations/en.json +16 -1
- package/translations/ja.json +16 -1
- package/translations/ko.json +16 -1
- package/translations/ms.json +16 -1
- package/translations/zh.json +16 -1
|
@@ -9,10 +9,11 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import * as THREE from 'three'
|
|
12
|
+
import * as BufferGeometryUtils from 'three/examples/jsm/utils/BufferGeometryUtils.js'
|
|
12
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
|
|
|
@@ -29,8 +30,8 @@ interface MCS3DConfig {
|
|
|
29
30
|
roughness: number
|
|
30
31
|
/** statusColor 미사용 시 기본 색상 */
|
|
31
32
|
defaultColor: number
|
|
32
|
-
/** 'box' | 'hexagon' */
|
|
33
|
-
shape: 'box' | 'hexagon'
|
|
33
|
+
/** 'box' | 'hexagon' | 'plane' | 'triangle' */
|
|
34
|
+
shape: 'box' | 'hexagon' | 'plane' | 'triangle'
|
|
34
35
|
/** mesh 생성 전 가시성 판정 — false 반환 시 mesh 미생성 */
|
|
35
36
|
isVisible?: (component: Component) => boolean
|
|
36
37
|
/** 부모 대비 크기 비율 (기본 1.0). < 1이면 부모 중심에 배치. */
|
|
@@ -41,6 +42,12 @@ interface MCS3DConfig {
|
|
|
41
42
|
matteTop?: boolean
|
|
42
43
|
/** 상판에 2D 렌더링 텍스처 적용 */
|
|
43
44
|
texturedTop?: boolean
|
|
45
|
+
/** 자체 발광 강도 (0~1). 조명 약한 면에서도 색상 유지 */
|
|
46
|
+
emissiveIntensity?: number
|
|
47
|
+
/** 바닥면 유지 (기본 false — 바닥면 제거) */
|
|
48
|
+
keepBottomFace?: boolean
|
|
49
|
+
/** syncZPosOffset 고정값 (설정 시 다른 로직 무시) */
|
|
50
|
+
zPosOverride?: number
|
|
44
51
|
}
|
|
45
52
|
|
|
46
53
|
/**
|
|
@@ -69,36 +76,64 @@ class MCSRealObject3D extends RealObjectGroup {
|
|
|
69
76
|
this._config = config
|
|
70
77
|
}
|
|
71
78
|
|
|
72
|
-
// width, height 절대값 (음수 값은 양수로 취급)
|
|
79
|
+
// width, height 절대값 (음수 값은 양수로 취급).
|
|
80
|
+
// `resizable:false` 컴포넌트(예: Scanner)는 state.width/height 가 없고
|
|
81
|
+
// bounds 만 있을 수 있으므로 bounds 를 fallback 으로 사용.
|
|
82
|
+
//
|
|
83
|
+
// `??` only catches null/undefined — a property-panel input that produced
|
|
84
|
+
// NaN would slip through and corrupt every downstream Math.* call. Use a
|
|
85
|
+
// finite check so NaN falls through to bounds → 10 like missing values do.
|
|
73
86
|
protected get absSize(): { w: number; h: number } {
|
|
74
|
-
const
|
|
87
|
+
const state = (this.component.state || {}) as any
|
|
88
|
+
const bounds = (this.component.bounds || {}) as any
|
|
89
|
+
const width = pickFinite(state.width, bounds.width, 10)
|
|
90
|
+
const height = pickFinite(state.height, bounds.height, 10)
|
|
75
91
|
return { w: Math.abs(width), h: Math.abs(height) }
|
|
76
92
|
}
|
|
77
93
|
|
|
78
94
|
get effectiveDepth(): number {
|
|
79
95
|
const { depth } = this.component.state
|
|
80
|
-
if (depth)
|
|
96
|
+
// Truthy `if (depth)` already rejected NaN/0/undefined, but be explicit
|
|
97
|
+
// so the intent (and the resolution chain) reads cleanly: only an
|
|
98
|
+
// explicit positive finite number wins; everything else → defaults.
|
|
99
|
+
if (typeof depth === 'number' && Number.isFinite(depth) && depth > 0) {
|
|
100
|
+
return depth
|
|
101
|
+
}
|
|
81
102
|
if (this._config.defaultDepth) return this._config.defaultDepth
|
|
82
103
|
const { w, h } = this.absSize
|
|
83
104
|
return Math.min(w, h) * this._config.depthRatio
|
|
84
105
|
}
|
|
85
106
|
|
|
86
107
|
protected get syncZPosOffset(): number {
|
|
108
|
+
// syncZPosOffset = placement + geometric (center origin = effectiveDepth/2)
|
|
109
|
+
// 중첩 보정(parent shift 상쇄)은 base RealObject.cz가 parent.geometricOffsetY를
|
|
110
|
+
// 빼는 방식으로 처리하므로 여기선 placement + 본인 geometric만 반환
|
|
111
|
+
const halfD = this.effectiveDepth / 2
|
|
112
|
+
// zPosOverride: 고정 높이 (OHTLine 등)
|
|
113
|
+
if (this._config.zPosOverride != null) {
|
|
114
|
+
return this._config.zPosOverride + halfD
|
|
115
|
+
}
|
|
87
116
|
// Port: 이송 표면 높이에 배치
|
|
88
117
|
if (this._config.defaultZPosFromParent) {
|
|
89
|
-
return TRANSPORT_SURFACE_HEIGHT
|
|
118
|
+
return TRANSPORT_SURFACE_HEIGHT + halfD
|
|
90
119
|
}
|
|
91
|
-
// Carrier:
|
|
120
|
+
// Carrier: 부모 유형에 따라 배치 위치 결정
|
|
92
121
|
if (this._config.sizeRatio && this._config.sizeRatio < 1) {
|
|
93
122
|
const parentType = this.component.parent?.state?.type
|
|
94
|
-
|
|
95
|
-
|
|
123
|
+
// OHT: 그리퍼 아래에 매달림 — placement = -effectiveDepth(carrier top이 OHT bottom)
|
|
124
|
+
if (parentType === 'OHT') {
|
|
125
|
+
return -this.effectiveDepth + halfD
|
|
96
126
|
}
|
|
97
|
-
|
|
127
|
+
if (parentType === 'Port' || parentType === 'Shelf' || parentType === 'Shuttle') {
|
|
128
|
+
return halfD
|
|
129
|
+
}
|
|
130
|
+
return TRANSPORT_SURFACE_HEIGHT + halfD
|
|
98
131
|
}
|
|
99
|
-
return
|
|
132
|
+
return halfD
|
|
100
133
|
}
|
|
101
134
|
|
|
135
|
+
// geometric 부분(center origin: depth/2)은 base 기본값 사용 — override 불필요
|
|
136
|
+
|
|
102
137
|
// 컴포넌트의 상태 색상(statusColor) 또는 fillStyle → Three.js 색상 변환
|
|
103
138
|
protected resolveColor(): number {
|
|
104
139
|
const comp = this.component as any
|
|
@@ -107,14 +142,14 @@ class MCSRealObject3D extends RealObjectGroup {
|
|
|
107
142
|
// '#F0F0F0'은 상태 미설정 시 기본 fallback — 무시하고 defaultColor 사용
|
|
108
143
|
try {
|
|
109
144
|
const sc = comp.statusColor
|
|
110
|
-
if (typeof sc === 'string' && sc && sc.toUpperCase() !== '#F0F0F0') return new THREE.Color(sc).getHex()
|
|
145
|
+
if (typeof sc === 'string' && sc && sc.toUpperCase() !== '#F0F0F0' && sc !== 'transparent') return new THREE.Color(sc).getHex()
|
|
111
146
|
} catch {
|
|
112
147
|
/* statusColor 미구현 시 무시 */
|
|
113
148
|
}
|
|
114
149
|
|
|
115
150
|
// 2) state.fillStyle fallback
|
|
116
151
|
const { fillStyle } = this.component.state
|
|
117
|
-
if (fillStyle && typeof fillStyle === 'string') {
|
|
152
|
+
if (fillStyle && typeof fillStyle === 'string' && fillStyle !== 'transparent') {
|
|
118
153
|
try {
|
|
119
154
|
return new THREE.Color(fillStyle).getHex()
|
|
120
155
|
} catch {
|
|
@@ -163,7 +198,7 @@ class MCSRealObject3D extends RealObjectGroup {
|
|
|
163
198
|
|
|
164
199
|
private buildBoxGeometry(w: number, d: number, h: number): THREE.BufferGeometry {
|
|
165
200
|
const geo = new THREE.BoxGeometry(w, d, h)
|
|
166
|
-
return this.removeBottomFace(geo)
|
|
201
|
+
return this._config.keepBottomFace ? geo : this.removeBottomFace(geo)
|
|
167
202
|
}
|
|
168
203
|
|
|
169
204
|
private buildHexagonGeometry(w: number, d: number, h: number): THREE.BufferGeometry {
|
|
@@ -194,6 +229,43 @@ class MCSRealObject3D extends RealObjectGroup {
|
|
|
194
229
|
return this.removeBottomFace(geo)
|
|
195
230
|
}
|
|
196
231
|
|
|
232
|
+
private buildPlaneGeometry(w: number, h: number): THREE.BufferGeometry {
|
|
233
|
+
const geo = new THREE.PlaneGeometry(w, h)
|
|
234
|
+
geo.rotateX(-Math.PI / 2)
|
|
235
|
+
return geo
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* 삼각형 prism — 2D Triangle 의 꼭짓점(state.x1/y1, x2/y2, x3/y3) 을 그대로 사용.
|
|
240
|
+
* things-scene 의 TriangleExtrude 와 동일한 방식 — 각 Scanner 인스턴스의
|
|
241
|
+
* 실제 정점 배치(방향, 비대칭 등) 가 3D 에 반영된다.
|
|
242
|
+
*
|
|
243
|
+
* `geometry.center()` 로 로컬 원점을 bounding-box 중심으로 맞추므로
|
|
244
|
+
* 후속 placement 와 정합.
|
|
245
|
+
*/
|
|
246
|
+
private buildTriangleGeometry(d: number): THREE.BufferGeometry {
|
|
247
|
+
const state = (this.component.state || {}) as any
|
|
248
|
+
const { x1 = 0, y1 = 0, x2 = 0, y2 = 0, x3 = 0, y3 = 0 } = state
|
|
249
|
+
|
|
250
|
+
const shape = new THREE.Shape()
|
|
251
|
+
shape.moveTo(x1, y1)
|
|
252
|
+
shape.lineTo(x2, y2)
|
|
253
|
+
shape.lineTo(x3, y3)
|
|
254
|
+
shape.closePath()
|
|
255
|
+
|
|
256
|
+
const geo = new THREE.ExtrudeGeometry(shape, {
|
|
257
|
+
steps: 1,
|
|
258
|
+
depth: d,
|
|
259
|
+
bevelEnabled: false,
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
// bounding box 중심으로 원점 정렬 후 Y-up 좌표계로 회전
|
|
263
|
+
geo.center()
|
|
264
|
+
geo.rotateX(Math.PI / 2)
|
|
265
|
+
|
|
266
|
+
return this.removeBottomFace(geo)
|
|
267
|
+
}
|
|
268
|
+
|
|
197
269
|
// ── Build ──
|
|
198
270
|
|
|
199
271
|
protected _ownMesh?: THREE.Mesh
|
|
@@ -217,19 +289,31 @@ class MCSRealObject3D extends RealObjectGroup {
|
|
|
217
289
|
const { opacity, metalness, roughness, shape } = this._config
|
|
218
290
|
|
|
219
291
|
const geometry =
|
|
220
|
-
shape === '
|
|
221
|
-
? this.
|
|
222
|
-
:
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
292
|
+
shape === 'plane'
|
|
293
|
+
? this.buildPlaneGeometry(w, h)
|
|
294
|
+
: shape === 'hexagon'
|
|
295
|
+
? this.buildHexagonGeometry(w, depth, h)
|
|
296
|
+
: shape === 'triangle'
|
|
297
|
+
? this.buildTriangleGeometry(depth)
|
|
298
|
+
: this.buildBoxGeometry(w, depth, h)
|
|
299
|
+
|
|
300
|
+
const isPlane = shape === 'plane'
|
|
301
|
+
const material = isPlane
|
|
302
|
+
? new THREE.MeshBasicMaterial({
|
|
303
|
+
color,
|
|
304
|
+
opacity,
|
|
305
|
+
transparent: opacity < 1,
|
|
306
|
+
side: THREE.DoubleSide,
|
|
307
|
+
})
|
|
308
|
+
: new THREE.MeshStandardMaterial({
|
|
309
|
+
color,
|
|
310
|
+
metalness,
|
|
311
|
+
roughness,
|
|
312
|
+
opacity,
|
|
313
|
+
transparent: opacity < 1,
|
|
314
|
+
side: opacity < 1 ? THREE.DoubleSide : THREE.FrontSide,
|
|
315
|
+
...(this._config.emissiveIntensity ? { emissive: color, emissiveIntensity: this._config.emissiveIntensity } : {}),
|
|
316
|
+
})
|
|
233
317
|
// z-fighting 방지: 바닥면(floor grid)보다 항상 앞에 렌더링
|
|
234
318
|
material.polygonOffset = true
|
|
235
319
|
material.polygonOffsetFactor = -1
|
|
@@ -254,11 +338,14 @@ class MCSRealObject3D extends RealObjectGroup {
|
|
|
254
338
|
}
|
|
255
339
|
|
|
256
340
|
this._ownMesh = new THREE.Mesh(geometry, meshMaterial)
|
|
257
|
-
//
|
|
258
|
-
|
|
259
|
-
|
|
341
|
+
// ownGroup 기준(bottom-up): mesh 바닥을 y=0에 맞추고 ownGroup이 -d/2로 shift해
|
|
342
|
+
// center-origin object3d에 정렬됨.
|
|
343
|
+
// box/hexagon (center-local geometry): y=rawDepth/2 (바닥을 0에 맞춤)
|
|
344
|
+
// plane: y=1 (바닥 살짝 띄움)
|
|
345
|
+
this._ownMesh.position.y = isPlane ? 1 : rawDepth / 2
|
|
346
|
+
this._ownMesh.castShadow = !isPlane
|
|
260
347
|
this._ownMesh.receiveShadow = true
|
|
261
|
-
this.
|
|
348
|
+
this.components3D.add(this._ownMesh)
|
|
262
349
|
}
|
|
263
350
|
|
|
264
351
|
// ── Change handling ──
|
|
@@ -353,7 +440,7 @@ class MCSRealObject3D extends RealObjectGroup {
|
|
|
353
440
|
update() {
|
|
354
441
|
// 자신의 mesh만 제거 (자식 컴포넌트 3D 오브젝트 보존)
|
|
355
442
|
if (this._ownMesh) {
|
|
356
|
-
this.
|
|
443
|
+
this.components3D.remove(this._ownMesh)
|
|
357
444
|
disposeObject3D(this._ownMesh)
|
|
358
445
|
this._ownMesh = undefined
|
|
359
446
|
}
|
|
@@ -368,9 +455,9 @@ class MCSRealObject3D extends RealObjectGroup {
|
|
|
368
455
|
updateDimension() {}
|
|
369
456
|
|
|
370
457
|
updateAlpha() {
|
|
371
|
-
const
|
|
372
|
-
|
|
373
|
-
const effective =
|
|
458
|
+
const state = this.component.state
|
|
459
|
+
// alpha가 명시적으로 설정되어 있으면 그 값을 사용, 없으면 config 기본값 적용
|
|
460
|
+
const effective = ('alpha' in state) ? (state.alpha as number) : this._config.opacity
|
|
374
461
|
|
|
375
462
|
this.object3d.traverse((o: any) => {
|
|
376
463
|
if (!o.material) return
|
|
@@ -385,6 +472,20 @@ class MCSRealObject3D extends RealObjectGroup {
|
|
|
385
472
|
}
|
|
386
473
|
}
|
|
387
474
|
|
|
475
|
+
// ── 헬퍼 ────────────────────────────────────────────────
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Returns the first argument that is a finite number. NaN slips past `??`
|
|
479
|
+
* (which only catches null/undefined), so without this a property-panel
|
|
480
|
+
* input producing NaN would corrupt absSize and cascade through Math.*.
|
|
481
|
+
*/
|
|
482
|
+
function pickFinite(...candidates: unknown[]): number {
|
|
483
|
+
for (const c of candidates) {
|
|
484
|
+
if (typeof c === 'number' && Number.isFinite(c)) return c
|
|
485
|
+
}
|
|
486
|
+
return 0
|
|
487
|
+
}
|
|
488
|
+
|
|
388
489
|
// ── 팩토리 등록 헬퍼 ─────────────────────────────────────
|
|
389
490
|
|
|
390
491
|
function register(type: string, overrides: Partial<MCS3DConfig> = {}) {
|
|
@@ -414,18 +515,106 @@ register('Buffer', {
|
|
|
414
515
|
})
|
|
415
516
|
|
|
416
517
|
register('AGVLine', {
|
|
417
|
-
depthRatio: 0
|
|
518
|
+
depthRatio: 0,
|
|
519
|
+
defaultDepth: 0.1,
|
|
418
520
|
metalness: 0.6,
|
|
419
521
|
roughness: 0.3,
|
|
420
522
|
defaultColor: 0x888899,
|
|
421
523
|
})
|
|
422
524
|
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
525
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
526
|
+
// OHTLine — 천장 I-beam 레일
|
|
527
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
528
|
+
|
|
529
|
+
const OHT_LINE_RAIL_COLOR = 0xcccccc
|
|
530
|
+
const OHT_LINE_HEIGHT = 7 * TRANSPORT_SURFACE_HEIGHT // OHT 상단과 일치
|
|
531
|
+
|
|
532
|
+
class OHTLineRealObject3D extends RealObjectGroup {
|
|
533
|
+
private _mesh?: THREE.Mesh
|
|
534
|
+
|
|
535
|
+
get effectiveDepth(): number { return 0 }
|
|
536
|
+
protected get syncZPosOffset(): number { return OHT_LINE_HEIGHT }
|
|
537
|
+
|
|
538
|
+
build() {
|
|
539
|
+
super.build()
|
|
540
|
+
|
|
541
|
+
const { width = 10, height = 10 } = this.component.state
|
|
542
|
+
const w = Math.abs(width)
|
|
543
|
+
const h = Math.abs(height)
|
|
544
|
+
if (!w || !h) return
|
|
545
|
+
|
|
546
|
+
// I-beam 치수: 이송 방향 = 긴 변, 폭은 2D의 1/2
|
|
547
|
+
const isVertical = w < h
|
|
548
|
+
const shortSide = isVertical ? w : h // 2D 원본 짧은 쪽 폭
|
|
549
|
+
const beltW = shortSide * 0.25 // I-beam 폭: 원본의 1/4
|
|
550
|
+
const beamH = Math.max(beltW * 0.5, 3) // I-beam 전체 높이
|
|
551
|
+
const inset = shortSide * 0.5 // 양끝 여백: 원본 폭의 1/2
|
|
552
|
+
const railLen = (isVertical ? h : w) - inset * 2
|
|
553
|
+
const flangeH = beamH * 0.2 // 플랜지 두께
|
|
554
|
+
const flangeW = beltW * 0.8 // 플랜지 폭
|
|
555
|
+
const webH = beamH - flangeH * 2 // 웹 높이
|
|
556
|
+
const webW = Math.max(flangeW * 0.25, 1) // 웹 두께
|
|
557
|
+
|
|
558
|
+
const mat = new THREE.MeshStandardMaterial({
|
|
559
|
+
color: OHT_LINE_RAIL_COLOR,
|
|
560
|
+
metalness: 0.5,
|
|
561
|
+
roughness: 0.4,
|
|
562
|
+
})
|
|
563
|
+
|
|
564
|
+
const geoms: THREE.BufferGeometry[] = []
|
|
565
|
+
|
|
566
|
+
// 상부 플랜지
|
|
567
|
+
const topFlange = isVertical
|
|
568
|
+
? new THREE.BoxGeometry(flangeW, flangeH, railLen)
|
|
569
|
+
: new THREE.BoxGeometry(railLen, flangeH, flangeW)
|
|
570
|
+
topFlange.translate(0, -flangeH / 2, 0)
|
|
571
|
+
geoms.push(topFlange)
|
|
572
|
+
|
|
573
|
+
// 웹 (수직판)
|
|
574
|
+
const web = isVertical
|
|
575
|
+
? new THREE.BoxGeometry(webW, webH, railLen)
|
|
576
|
+
: new THREE.BoxGeometry(railLen, webH, webW)
|
|
577
|
+
web.translate(0, -flangeH - webH / 2, 0)
|
|
578
|
+
geoms.push(web)
|
|
579
|
+
|
|
580
|
+
// 하부 플랜지
|
|
581
|
+
const botFlange = isVertical
|
|
582
|
+
? new THREE.BoxGeometry(flangeW, flangeH, railLen)
|
|
583
|
+
: new THREE.BoxGeometry(railLen, flangeH, flangeW)
|
|
584
|
+
botFlange.translate(0, -flangeH - webH - flangeH / 2, 0)
|
|
585
|
+
geoms.push(botFlange)
|
|
586
|
+
|
|
587
|
+
this._mesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(geoms), mat)
|
|
588
|
+
this._mesh.castShadow = true
|
|
589
|
+
this.object3d.add(this._mesh)
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
update() {
|
|
593
|
+
if (this._mesh) {
|
|
594
|
+
this.object3d.remove(this._mesh)
|
|
595
|
+
this._mesh.geometry.dispose()
|
|
596
|
+
;(this._mesh.material as THREE.Material).dispose()
|
|
597
|
+
this._mesh = undefined
|
|
598
|
+
}
|
|
599
|
+
this.build()
|
|
600
|
+
this.updateTransform()
|
|
601
|
+
this.updateHidden()
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
onchange(after: Record<string, unknown>, before: Record<string, unknown>) {
|
|
605
|
+
if ('width' in after || 'height' in after || 'depth' in after ||
|
|
606
|
+
'fillStyle' in after || 'strokeStyle' in after) {
|
|
607
|
+
this.update()
|
|
608
|
+
return
|
|
609
|
+
}
|
|
610
|
+
super.onchange(after, before)
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
updateDimension() {}
|
|
614
|
+
updateAlpha() {}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
registerRealObjectFactory('OHTLine', (component: Component) => new OHTLineRealObject3D(component))
|
|
429
618
|
|
|
430
619
|
// STOCKER: 벽면에 용량 게이지 바를 포함하는 커스텀 3D 오브젝트
|
|
431
620
|
const STOCKER_CONFIG: MCS3DConfig = {
|
|
@@ -523,8 +712,9 @@ class StockerRealObject3D extends MCSRealObject3D {
|
|
|
523
712
|
mat.polygonOffsetUnit = -2
|
|
524
713
|
|
|
525
714
|
this.gaugeMesh = new THREE.Mesh(geo, mat)
|
|
715
|
+
// ownGroup 기준(bottom-up): 볼륨 상단 = +depth. 그 위로 0.5 띄움.
|
|
526
716
|
this.gaugeMesh.position.set(0, depth + 0.5, 0)
|
|
527
|
-
this.
|
|
717
|
+
this.components3D.add(this.gaugeMesh)
|
|
528
718
|
}
|
|
529
719
|
|
|
530
720
|
private resolveGauge(data: any, mode: string) {
|
|
@@ -569,7 +759,7 @@ class StockerRealObject3D extends MCSRealObject3D {
|
|
|
569
759
|
const fullup = data.FULLUP === 'T'
|
|
570
760
|
const usage = Math.round((cur / max) * 100)
|
|
571
761
|
const legend = (this.component as any).root?.style?.LEGEND_CAPACITY || LEGEND_CAPACITY
|
|
572
|
-
const color = fullup ? 'red' : (
|
|
762
|
+
const color = fullup ? 'red' : (getValueOnRanges(usage, legend) || '#44aa44')
|
|
573
763
|
return {
|
|
574
764
|
segments: [{ ratio: usage / 100, color }],
|
|
575
765
|
borderColor: color,
|
|
@@ -579,6 +769,11 @@ class StockerRealObject3D extends MCSRealObject3D {
|
|
|
579
769
|
}
|
|
580
770
|
}
|
|
581
771
|
|
|
772
|
+
update() {
|
|
773
|
+
this.clearGauge()
|
|
774
|
+
super.update()
|
|
775
|
+
}
|
|
776
|
+
|
|
582
777
|
onchange(after: Record<string, unknown>, before: Record<string, unknown>) {
|
|
583
778
|
if ('data' in after) {
|
|
584
779
|
this.clearGauge()
|
|
@@ -594,7 +789,7 @@ class StockerRealObject3D extends MCSRealObject3D {
|
|
|
594
789
|
|
|
595
790
|
private clearGauge() {
|
|
596
791
|
if (this.gaugeMesh) {
|
|
597
|
-
this.
|
|
792
|
+
this.components3D.remove(this.gaugeMesh)
|
|
598
793
|
this.gaugeMesh.geometry?.dispose()
|
|
599
794
|
const mat = this.gaugeMesh.material as THREE.MeshBasicMaterial
|
|
600
795
|
mat?.map?.dispose()
|
|
@@ -610,16 +805,16 @@ registerRealObjectFactory('STOCKER', (component: Component) => new StockerRealOb
|
|
|
610
805
|
// Unit (이동체 · 홀더) — 낮은 depth, 불투명
|
|
611
806
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
612
807
|
|
|
613
|
-
// Port:
|
|
808
|
+
// Port: 바닥판 (PlaneGeometry)
|
|
614
809
|
const PORT_CONFIG: MCS3DConfig = {
|
|
615
810
|
...DEFAULTS,
|
|
616
|
-
depthRatio: 0.
|
|
811
|
+
depthRatio: 0.01,
|
|
617
812
|
defaultZPosFromParent: true,
|
|
618
|
-
opacity: 0.
|
|
619
|
-
metalness: 0.
|
|
620
|
-
roughness: 0.
|
|
813
|
+
opacity: 0.1,
|
|
814
|
+
metalness: 0.3,
|
|
815
|
+
roughness: 0.8,
|
|
621
816
|
defaultColor: 0xb0a090,
|
|
622
|
-
|
|
817
|
+
shape: 'plane',
|
|
623
818
|
}
|
|
624
819
|
registerRealObjectFactory('Port', (component: Component) => new MCSRealObject3D(component, PORT_CONFIG))
|
|
625
820
|
|
|
@@ -634,134 +829,246 @@ register('Shelf', {
|
|
|
634
829
|
const CARRIER_CONFIG: MCS3DConfig = {
|
|
635
830
|
...DEFAULTS,
|
|
636
831
|
depthRatio: 0.5,
|
|
637
|
-
metalness: 0
|
|
638
|
-
roughness:
|
|
639
|
-
defaultColor:
|
|
832
|
+
metalness: 0,
|
|
833
|
+
roughness: 1,
|
|
834
|
+
defaultColor: 0xf0f0f0,
|
|
640
835
|
isVisible: (c: Component) => !!(c.state as any).id,
|
|
641
836
|
sizeRatio: 0.8,
|
|
642
837
|
texturedTop: true,
|
|
643
838
|
}
|
|
644
839
|
|
|
645
840
|
class CarrierRealObject3D extends MCSRealObject3D {
|
|
646
|
-
private _animRaf: number | null = null
|
|
647
|
-
private _animStartTime = 0
|
|
648
|
-
|
|
649
841
|
constructor(component: Component) {
|
|
650
842
|
super(component, CARRIER_CONFIG)
|
|
651
843
|
}
|
|
844
|
+
}
|
|
652
845
|
|
|
653
|
-
|
|
654
|
-
super.build()
|
|
655
|
-
this.startCarrierAnimation()
|
|
656
|
-
}
|
|
846
|
+
registerRealObjectFactory('Carrier', (component: Component) => new CarrierRealObject3D(component))
|
|
657
847
|
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
*/
|
|
662
|
-
private startCarrierAnimation() {
|
|
663
|
-
this.stopCarrierAnimation()
|
|
848
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
849
|
+
// Gauge (용량 게이지) — 얇은 바
|
|
850
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
664
851
|
|
|
665
|
-
|
|
666
|
-
|
|
852
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
853
|
+
// Gauge (용량 게이지) — 2D 렌더링을 Plane 텍스처로 3D 표시
|
|
854
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
667
855
|
|
|
668
|
-
|
|
669
|
-
|
|
856
|
+
// Gauge 컴포넌트는 3D에서 표시하지 않음 (부모 STOCKER의 3D 모델에서 직접 게이지 렌더링)
|
|
857
|
+
// null 반환으로 3D 오브젝트 생성을 억제하고, default factory fallback도 방지
|
|
858
|
+
registerRealObjectFactory('StockerCapacityBar', () => undefined as any)
|
|
859
|
+
registerRealObjectFactory('MCSGaugeCapacityBar', () => undefined as any)
|
|
860
|
+
registerRealObjectFactory('ZoneCapacityBar', () => undefined as any)
|
|
670
861
|
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
862
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
863
|
+
// PortFlow — 2D render를 Plane 텍스처로 3D 표시
|
|
864
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
674
865
|
|
|
675
|
-
|
|
676
|
-
|
|
866
|
+
class PortFlowRealObject3D extends RealObjectGroup {
|
|
867
|
+
private _mesh?: THREE.Mesh
|
|
868
|
+
private _texture?: THREE.CanvasTexture
|
|
677
869
|
|
|
678
|
-
|
|
870
|
+
get effectiveDepth(): number { return 0 }
|
|
871
|
+
protected get syncZPosOffset(): number {
|
|
872
|
+
// 부모 Port/Conveyor의 zPos와 동일하게
|
|
873
|
+
const parent = (this.component as any).parentObject
|
|
874
|
+
if (parent?.state?.zPos != null) return parent.state.zPos
|
|
875
|
+
return TRANSPORT_SURFACE_HEIGHT
|
|
876
|
+
}
|
|
679
877
|
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
const obj = this.object3d
|
|
878
|
+
build() {
|
|
879
|
+
super.build()
|
|
683
880
|
|
|
684
|
-
const
|
|
685
|
-
|
|
881
|
+
const { width = 10, height = 10 } = this.component.state
|
|
882
|
+
const w = Math.abs(width)
|
|
883
|
+
const h = Math.abs(height)
|
|
884
|
+
if (!w || !h) return
|
|
885
|
+
|
|
886
|
+
// 2D render를 캔버스에 캡처
|
|
887
|
+
// 화살표 날개 + 텍스트 영역이 bounds 밖으로 나가므로 전체 영역 계산
|
|
888
|
+
const { left, top } = this.component.bounds
|
|
889
|
+
const { fontSize = 24, labelPosition = 'top' } = this.component.state
|
|
890
|
+
const arrowPad = Math.min(w, h) * 0.4
|
|
891
|
+
|
|
892
|
+
// 텍스트 영역 확장
|
|
893
|
+
let padLeft = arrowPad, padRight = arrowPad, padTop = arrowPad, padBottom = arrowPad
|
|
894
|
+
if ((this.component as any).hasTextProperty) {
|
|
895
|
+
switch (labelPosition) {
|
|
896
|
+
case 'top': padTop = Math.max(padTop, fontSize + 4); break
|
|
897
|
+
case 'bottom': padBottom = Math.max(padBottom, fontSize + 4); break
|
|
898
|
+
case 'left': padLeft = Math.max(padLeft, fontSize * 4 + 4); break
|
|
899
|
+
case 'right': padRight = Math.max(padRight, fontSize * 4 + 4); break
|
|
900
|
+
}
|
|
901
|
+
}
|
|
686
902
|
|
|
687
|
-
|
|
688
|
-
|
|
903
|
+
const totalW = w + padLeft + padRight
|
|
904
|
+
const totalH = h + padTop + padBottom
|
|
905
|
+
const scale = 2
|
|
906
|
+
const canvas = document.createElement('canvas')
|
|
907
|
+
canvas.width = totalW * scale
|
|
908
|
+
canvas.height = totalH * scale
|
|
909
|
+
const ctx = canvas.getContext('2d')!
|
|
689
910
|
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
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
|
-
}
|
|
911
|
+
ctx.save()
|
|
912
|
+
ctx.scale(scale, scale)
|
|
913
|
+
ctx.translate(-left + padLeft, -top + padTop)
|
|
914
|
+
try {
|
|
915
|
+
// 화살표 렌더
|
|
916
|
+
this.component.render(ctx)
|
|
917
|
+
ctx.fillStyle = this.component.state.strokeStyle || '#666666'
|
|
918
|
+
ctx.fill()
|
|
919
|
+
// 텍스트 렌더
|
|
920
|
+
if ((this.component as any).hasTextProperty && (this.component as any).drawText) {
|
|
921
|
+
;(this.component as any).drawText(ctx)
|
|
717
922
|
}
|
|
923
|
+
} catch { /* render 실패 시 빈 텍스처 */ }
|
|
924
|
+
ctx.restore()
|
|
718
925
|
|
|
719
|
-
|
|
720
|
-
|
|
926
|
+
this._texture = new THREE.CanvasTexture(canvas)
|
|
927
|
+
this._texture.colorSpace = THREE.SRGBColorSpace
|
|
721
928
|
|
|
722
|
-
|
|
723
|
-
|
|
929
|
+
const geo = new THREE.PlaneGeometry(totalW, totalH)
|
|
930
|
+
geo.rotateX(-Math.PI / 2)
|
|
724
931
|
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
}
|
|
730
|
-
}
|
|
932
|
+
const mat = new THREE.MeshBasicMaterial({
|
|
933
|
+
map: this._texture,
|
|
934
|
+
alphaTest: 0.1,
|
|
935
|
+
side: THREE.DoubleSide,
|
|
936
|
+
})
|
|
731
937
|
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
this.object3d.scale.set(1, 1, 1)
|
|
739
|
-
this.object3d.rotation.y = 0
|
|
938
|
+
this._mesh = new THREE.Mesh(geo, mat)
|
|
939
|
+
// 비대칭 패딩 보정: 플레인 센터를 컴포넌트 bounds 센터에 맞춤
|
|
940
|
+
this._mesh.position.x = (padRight - padLeft) / 2
|
|
941
|
+
this._mesh.position.y = 1
|
|
942
|
+
this._mesh.position.z = (padBottom - padTop) / 2
|
|
943
|
+
this.object3d.add(this._mesh)
|
|
740
944
|
}
|
|
741
945
|
|
|
742
946
|
update() {
|
|
743
|
-
this.
|
|
744
|
-
|
|
947
|
+
if (this._mesh) {
|
|
948
|
+
this.object3d.remove(this._mesh)
|
|
949
|
+
this._mesh.geometry.dispose()
|
|
950
|
+
;(this._mesh.material as THREE.Material).dispose()
|
|
951
|
+
this._texture?.dispose()
|
|
952
|
+
this._mesh = undefined
|
|
953
|
+
this._texture = undefined
|
|
954
|
+
}
|
|
955
|
+
this.build()
|
|
956
|
+
this.updateTransform()
|
|
957
|
+
this.updateHidden()
|
|
745
958
|
}
|
|
746
959
|
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
960
|
+
onchange(after: Record<string, unknown>, before: Record<string, unknown>) {
|
|
961
|
+
if ('width' in after || 'height' in after || 'orientation' in after ||
|
|
962
|
+
'direction' in after || 'strokeStyle' in after || 'INOUTTYPE' in after) {
|
|
963
|
+
this.update()
|
|
964
|
+
return
|
|
965
|
+
}
|
|
966
|
+
super.onchange(after, before)
|
|
750
967
|
}
|
|
968
|
+
|
|
969
|
+
updateDimension() {}
|
|
970
|
+
updateAlpha() {}
|
|
751
971
|
}
|
|
752
972
|
|
|
753
|
-
registerRealObjectFactory('
|
|
973
|
+
registerRealObjectFactory('PortFlow', (component: Component) => new PortFlowRealObject3D(component))
|
|
754
974
|
|
|
755
975
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
756
|
-
//
|
|
976
|
+
// Node — 2D render를 바닥 Plane 텍스처로 3D 표시
|
|
757
977
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
758
978
|
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
979
|
+
class NodeRealObject3D extends RealObjectGroup {
|
|
980
|
+
private _mesh?: THREE.Mesh
|
|
981
|
+
private _texture?: THREE.CanvasTexture
|
|
982
|
+
|
|
983
|
+
get effectiveDepth(): number { return 0 }
|
|
984
|
+
protected get syncZPosOffset(): number { return 0 }
|
|
985
|
+
|
|
986
|
+
build() {
|
|
987
|
+
super.build()
|
|
988
|
+
|
|
989
|
+
const { cx, cy } = this.component.state
|
|
990
|
+
const radius = 8
|
|
991
|
+
const size = radius * 2
|
|
992
|
+
const scale = 2
|
|
993
|
+
const pad = 2
|
|
994
|
+
|
|
995
|
+
const canvas = document.createElement('canvas')
|
|
996
|
+
canvas.width = (size + pad * 2) * scale
|
|
997
|
+
canvas.height = (size + pad * 2) * scale
|
|
998
|
+
const ctx = canvas.getContext('2d')!
|
|
999
|
+
|
|
1000
|
+
ctx.save()
|
|
1001
|
+
ctx.scale(scale, scale)
|
|
1002
|
+
ctx.translate(-cx + radius + pad, -cy + radius + pad)
|
|
1003
|
+
try {
|
|
1004
|
+
this.component.render(ctx)
|
|
1005
|
+
const { fillStyle = '#888888' } = this.component.state
|
|
1006
|
+
ctx.fillStyle = typeof fillStyle === 'string' ? fillStyle : '#888888'
|
|
1007
|
+
ctx.fill()
|
|
1008
|
+
const { strokeStyle } = this.component.state
|
|
1009
|
+
if (strokeStyle && typeof strokeStyle === 'string') {
|
|
1010
|
+
ctx.strokeStyle = strokeStyle
|
|
1011
|
+
ctx.lineWidth = 1
|
|
1012
|
+
ctx.stroke()
|
|
1013
|
+
}
|
|
1014
|
+
} catch { /* render 실패 시 빈 텍스처 */ }
|
|
1015
|
+
ctx.restore()
|
|
1016
|
+
|
|
1017
|
+
this._texture = new THREE.CanvasTexture(canvas)
|
|
1018
|
+
this._texture.colorSpace = THREE.SRGBColorSpace
|
|
1019
|
+
|
|
1020
|
+
const geo = new THREE.PlaneGeometry(size + pad * 2, size + pad * 2)
|
|
1021
|
+
geo.rotateX(-Math.PI / 2)
|
|
1022
|
+
|
|
1023
|
+
const mat = new THREE.MeshBasicMaterial({
|
|
1024
|
+
map: this._texture,
|
|
1025
|
+
alphaTest: 0.1,
|
|
1026
|
+
side: THREE.DoubleSide,
|
|
1027
|
+
})
|
|
1028
|
+
|
|
1029
|
+
this._mesh = new THREE.Mesh(geo, mat)
|
|
1030
|
+
this._mesh.position.y = 1
|
|
1031
|
+
this.object3d.add(this._mesh)
|
|
1032
|
+
}
|
|
762
1033
|
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
1034
|
+
update() {
|
|
1035
|
+
if (this._mesh) {
|
|
1036
|
+
this.object3d.remove(this._mesh)
|
|
1037
|
+
this._mesh.geometry.dispose()
|
|
1038
|
+
;(this._mesh.material as THREE.Material).dispose()
|
|
1039
|
+
this._texture?.dispose()
|
|
1040
|
+
this._mesh = undefined
|
|
1041
|
+
this._texture = undefined
|
|
1042
|
+
}
|
|
1043
|
+
this.build()
|
|
1044
|
+
this.updateTransform()
|
|
1045
|
+
this.updateHidden()
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
onchange(after: Record<string, unknown>, before: Record<string, unknown>) {
|
|
1049
|
+
if ('fillStyle' in after || 'strokeStyle' in after) {
|
|
1050
|
+
this.update()
|
|
1051
|
+
return
|
|
1052
|
+
}
|
|
1053
|
+
super.onchange(after, before)
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
updateDimension() {}
|
|
1057
|
+
updateAlpha() {}
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
registerRealObjectFactory('Node', (component: Component) => new NodeRealObject3D(component))
|
|
1061
|
+
|
|
1062
|
+
// Scanner — 2D Triangle 실루엣과 정합되는 삼각형 prism.
|
|
1063
|
+
// 이송 표면 높이의 0.2배(=6) 얇은 기둥 — 원래 값 유지, shape 만 'box' → 'triangle'.
|
|
1064
|
+
const SCANNER_CONFIG: MCS3DConfig = {
|
|
1065
|
+
...DEFAULTS,
|
|
1066
|
+
defaultDepth: TRANSPORT_SURFACE_HEIGHT * 0.2,
|
|
1067
|
+
defaultZPosFromParent: true,
|
|
1068
|
+
opacity: 1,
|
|
1069
|
+
metalness: 0.3,
|
|
1070
|
+
roughness: 0.7,
|
|
1071
|
+
defaultColor: 0x888888,
|
|
1072
|
+
shape: 'triangle',
|
|
1073
|
+
}
|
|
1074
|
+
registerRealObjectFactory('Scanner', (component: Component) => new MCSRealObject3D(component, SCANNER_CONFIG))
|