@fmsim/machine 2.0.0-beta.23 → 2.0.0-beta.25
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/factories/conveyor-3d.js +116 -51
- package/dist/factories/conveyor-3d.js.map +1 -1
- package/dist/factories/machine-3d.js +164 -7
- package/dist/factories/machine-3d.js.map +1 -1
- package/dist/node.js +1 -1
- package/dist/node.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/factories/conveyor-3d.ts +114 -52
- package/src/factories/machine-3d.ts +197 -7
- package/src/node.ts +1 -1
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@fmsim/machine",
|
|
3
3
|
"description": "Layout View를 구성하는 설비(machine)들과 유닛을 구현한 모듈입니다.",
|
|
4
4
|
"author": "heartyoh",
|
|
5
|
-
"version": "2.0.0-beta.
|
|
5
|
+
"version": "2.0.0-beta.25",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"module": "dist/index.js",
|
|
8
8
|
"things-scene": true,
|
|
@@ -55,5 +55,5 @@
|
|
|
55
55
|
"prettier --write"
|
|
56
56
|
]
|
|
57
57
|
},
|
|
58
|
-
"gitHead": "
|
|
58
|
+
"gitHead": "df172316ee6e925c87fcfeb508bc50c770643b80"
|
|
59
59
|
}
|
|
@@ -80,7 +80,12 @@ export class Conveyor3D extends RealObjectGroup {
|
|
|
80
80
|
const rollerDiameter = Math.max(rollWidth, 2)
|
|
81
81
|
const frameH = Math.min(rollerDiameter, depth)
|
|
82
82
|
const legH = Math.max(depth - frameH, 0)
|
|
83
|
-
|
|
83
|
+
|
|
84
|
+
// isVertical일 때 이송 방향이 Z축: 레일은 Z축을 따라, 벨트폭은 width
|
|
85
|
+
// !isVertical일 때 이송 방향이 X축: 레일은 X축을 따라, 벨트폭은 height
|
|
86
|
+
const beltWidth = isVertical ? width : height
|
|
87
|
+
const railLen = isVertical ? height : width
|
|
88
|
+
const railW = Math.max(beltWidth * 0.06, 2)
|
|
84
89
|
const legThickness = Math.max(railW * 0.8, 2)
|
|
85
90
|
|
|
86
91
|
const frameMaterial = new THREE.MeshStandardMaterial({
|
|
@@ -94,11 +99,21 @@ export class Conveyor3D extends RealObjectGroup {
|
|
|
94
99
|
const frameGeometries: THREE.BufferGeometry[] = []
|
|
95
100
|
const railY = depth - frameH / 2
|
|
96
101
|
|
|
97
|
-
// Side rails
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
+
// Side rails — 이송 방향을 따라, 벨트 양측에 배치
|
|
103
|
+
if (isVertical) {
|
|
104
|
+
// 레일이 Z축(세로)을 따라, ±X에 배치
|
|
105
|
+
for (const xSign of [-1, 1]) {
|
|
106
|
+
const rail = new THREE.BoxGeometry(railW, frameH, height)
|
|
107
|
+
rail.translate(xSign * (width / 2 - railW / 2), railY, 0)
|
|
108
|
+
frameGeometries.push(rail)
|
|
109
|
+
}
|
|
110
|
+
} else {
|
|
111
|
+
// 레일이 X축(가로)을 따라, ±Z에 배치
|
|
112
|
+
for (const zSign of [-1, 1]) {
|
|
113
|
+
const rail = new THREE.BoxGeometry(width, frameH, railW)
|
|
114
|
+
rail.translate(0, railY, zSign * (height / 2 - railW / 2))
|
|
115
|
+
frameGeometries.push(rail)
|
|
116
|
+
}
|
|
102
117
|
}
|
|
103
118
|
|
|
104
119
|
// 4 legs at corners
|
|
@@ -106,34 +121,51 @@ export class Conveyor3D extends RealObjectGroup {
|
|
|
106
121
|
const legTopY = depth - frameH
|
|
107
122
|
const legCenterY = legTopY - legH / 2
|
|
108
123
|
|
|
124
|
+
// 다리 위치: 레일 안쪽에 정렬
|
|
125
|
+
const legXPos = isVertical ? (width / 2 - railW / 2) : (width / 2 - legThickness / 2)
|
|
126
|
+
const legZPos = isVertical ? (height / 2 - legThickness / 2) : (height / 2 - railW / 2)
|
|
127
|
+
|
|
109
128
|
for (const xSign of [-1, 1]) {
|
|
110
129
|
for (const zSign of [-1, 1]) {
|
|
111
|
-
const lx = xSign * (width / 2 - legThickness / 2)
|
|
112
|
-
const lz = zSign * (height / 2 - railW / 2)
|
|
113
130
|
const leg = new THREE.BoxGeometry(legThickness, legH, legThickness)
|
|
114
|
-
leg.translate(
|
|
131
|
+
leg.translate(xSign * legXPos, legCenterY, zSign * legZPos)
|
|
115
132
|
frameGeometries.push(leg)
|
|
116
133
|
}
|
|
117
134
|
}
|
|
118
135
|
|
|
119
|
-
// Cross-bracing
|
|
136
|
+
// Cross-bracing
|
|
120
137
|
const braceH = legThickness * 0.6
|
|
121
138
|
const braceW = legThickness * 0.6
|
|
122
|
-
const braceLen = height - railW
|
|
123
139
|
const braceY = legTopY - legH * 0.35
|
|
124
140
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
141
|
+
// 이송 방향 브레이스 (레일과 평행)
|
|
142
|
+
if (isVertical) {
|
|
143
|
+
for (const xSign of [-1, 1]) {
|
|
144
|
+
const brace = new THREE.BoxGeometry(braceW, braceH, height - legThickness)
|
|
145
|
+
brace.translate(xSign * legXPos, braceY, 0)
|
|
146
|
+
frameGeometries.push(brace)
|
|
147
|
+
}
|
|
148
|
+
} else {
|
|
149
|
+
for (const zSign of [-1, 1]) {
|
|
150
|
+
const brace = new THREE.BoxGeometry(width - legThickness, braceH, braceW)
|
|
151
|
+
brace.translate(0, braceY, zSign * legZPos)
|
|
152
|
+
frameGeometries.push(brace)
|
|
153
|
+
}
|
|
129
154
|
}
|
|
130
155
|
|
|
131
|
-
//
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
156
|
+
// 벨트폭 방향 브레이스 (레일과 수직)
|
|
157
|
+
if (isVertical) {
|
|
158
|
+
for (const zSign of [-1, 1]) {
|
|
159
|
+
const brace = new THREE.BoxGeometry(width - railW, braceH, braceW)
|
|
160
|
+
brace.translate(0, braceY, zSign * legZPos)
|
|
161
|
+
frameGeometries.push(brace)
|
|
162
|
+
}
|
|
163
|
+
} else {
|
|
164
|
+
for (const xSign of [-1, 1]) {
|
|
165
|
+
const brace = new THREE.BoxGeometry(braceW, braceH, height - railW)
|
|
166
|
+
brace.translate(xSign * legXPos, braceY, 0)
|
|
167
|
+
frameGeometries.push(brace)
|
|
168
|
+
}
|
|
137
169
|
}
|
|
138
170
|
}
|
|
139
171
|
|
|
@@ -144,9 +176,9 @@ export class Conveyor3D extends RealObjectGroup {
|
|
|
144
176
|
|
|
145
177
|
// --- Rollers, Belt, or Rails ---
|
|
146
178
|
if (conveyorType === 'rail') {
|
|
147
|
-
this.buildRails(width, height, depth, frameH, railW)
|
|
179
|
+
this.buildRails(width, height, depth, frameH, railW, isVertical)
|
|
148
180
|
} else if (conveyorType === 'belt') {
|
|
149
|
-
this.buildBelt(width, height, depth, frameH, railW)
|
|
181
|
+
this.buildBelt(width, height, depth, frameH, railW, isVertical)
|
|
150
182
|
} else {
|
|
151
183
|
this.buildRollers(width, height, depth, frameH, railW, rollWidth, isVertical)
|
|
152
184
|
}
|
|
@@ -209,22 +241,31 @@ export class Conveyor3D extends RealObjectGroup {
|
|
|
209
241
|
}
|
|
210
242
|
}
|
|
211
243
|
|
|
212
|
-
private buildBelt(width: number, height: number, depth: number, frameH: number, railW: number) {
|
|
244
|
+
private buildBelt(width: number, height: number, depth: number, frameH: number, railW: number, isVertical: boolean = false) {
|
|
213
245
|
const drumRadius = frameH * 0.38
|
|
214
|
-
const drumLength = height - railW * 2
|
|
215
246
|
const beltY = depth - drumRadius
|
|
216
247
|
const color = this.resolveColor()
|
|
217
248
|
|
|
249
|
+
// 이송 방향 길이와 벨트폭
|
|
250
|
+
const transportLen = isVertical ? height : width
|
|
251
|
+
const beltWidth = isVertical ? width : height
|
|
252
|
+
const drumLength = beltWidth - railW * 2
|
|
253
|
+
|
|
218
254
|
// Two end drums
|
|
219
255
|
const drumGeometries: THREE.BufferGeometry[] = []
|
|
220
256
|
const drumInset = drumRadius * 1.5
|
|
221
|
-
const
|
|
222
|
-
const
|
|
257
|
+
const drum1 = -transportLen / 2 + drumInset
|
|
258
|
+
const drum2 = transportLen / 2 - drumInset
|
|
223
259
|
|
|
224
|
-
for (const
|
|
260
|
+
for (const dp of [drum1, drum2]) {
|
|
225
261
|
const drum = new THREE.CylinderGeometry(drumRadius, drumRadius, drumLength, 16)
|
|
226
|
-
|
|
227
|
-
|
|
262
|
+
if (isVertical) {
|
|
263
|
+
drum.rotateZ(Math.PI / 2)
|
|
264
|
+
drum.translate(0, beltY, dp)
|
|
265
|
+
} else {
|
|
266
|
+
drum.rotateX(Math.PI / 2)
|
|
267
|
+
drum.translate(dp, beltY, 0)
|
|
268
|
+
}
|
|
228
269
|
drumGeometries.push(drum)
|
|
229
270
|
}
|
|
230
271
|
|
|
@@ -244,8 +285,10 @@ export class Conveyor3D extends RealObjectGroup {
|
|
|
244
285
|
})
|
|
245
286
|
|
|
246
287
|
// Flat belt top surface spanning between drums
|
|
247
|
-
const flatLen =
|
|
248
|
-
const flatGeo =
|
|
288
|
+
const flatLen = drum2 - drum1
|
|
289
|
+
const flatGeo = isVertical
|
|
290
|
+
? new THREE.BoxGeometry(drumLength, beltThickness, flatLen)
|
|
291
|
+
: new THREE.BoxGeometry(flatLen, beltThickness, drumLength)
|
|
249
292
|
const flatMesh = new THREE.Mesh(flatGeo, beltMaterial)
|
|
250
293
|
flatMesh.position.set(0, beltY + drumRadius, 0)
|
|
251
294
|
flatMesh.castShadow = true
|
|
@@ -253,9 +296,9 @@ export class Conveyor3D extends RealObjectGroup {
|
|
|
253
296
|
|
|
254
297
|
// Belt wrap around drums (segmented half-cylinder)
|
|
255
298
|
const wrapSegments = 12
|
|
256
|
-
for (const [
|
|
257
|
-
[
|
|
258
|
-
[
|
|
299
|
+
for (const [dp, angleStart] of [
|
|
300
|
+
[drum1, Math.PI / 2],
|
|
301
|
+
[drum2, -Math.PI / 2],
|
|
259
302
|
] as [number, number][]) {
|
|
260
303
|
const wrapGeometries: THREE.BufferGeometry[] = []
|
|
261
304
|
for (let i = 0; i < wrapSegments; i++) {
|
|
@@ -263,18 +306,25 @@ export class Conveyor3D extends RealObjectGroup {
|
|
|
263
306
|
const a1 = angleStart + (Math.PI * (i + 1)) / wrapSegments
|
|
264
307
|
const r = drumRadius + beltThickness / 2
|
|
265
308
|
|
|
266
|
-
const
|
|
309
|
+
const p0 = dp + Math.cos(a0) * r
|
|
267
310
|
const y0 = beltY + Math.sin(a0) * r
|
|
268
|
-
const
|
|
311
|
+
const p1 = dp + Math.cos(a1) * r
|
|
269
312
|
const y1 = beltY + Math.sin(a1) * r
|
|
270
313
|
|
|
271
|
-
const segLen = Math.sqrt((
|
|
272
|
-
const segAngle = Math.atan2(y1 - y0,
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
314
|
+
const segLen = Math.sqrt((p1 - p0) ** 2 + (y1 - y0) ** 2) * 1.05
|
|
315
|
+
const segAngle = Math.atan2(y1 - y0, p1 - p0)
|
|
316
|
+
|
|
317
|
+
if (isVertical) {
|
|
318
|
+
const seg = new THREE.BoxGeometry(drumLength, beltThickness, segLen)
|
|
319
|
+
seg.rotateX(-segAngle)
|
|
320
|
+
seg.translate(0, (y0 + y1) / 2, (p0 + p1) / 2)
|
|
321
|
+
wrapGeometries.push(seg)
|
|
322
|
+
} else {
|
|
323
|
+
const seg = new THREE.BoxGeometry(segLen, beltThickness, drumLength)
|
|
324
|
+
seg.rotateZ(segAngle)
|
|
325
|
+
seg.translate((p0 + p1) / 2, (y0 + y1) / 2, 0)
|
|
326
|
+
wrapGeometries.push(seg)
|
|
327
|
+
}
|
|
278
328
|
}
|
|
279
329
|
const wrapMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(wrapGeometries), beltMaterial)
|
|
280
330
|
wrapMesh.castShadow = true
|
|
@@ -282,17 +332,21 @@ export class Conveyor3D extends RealObjectGroup {
|
|
|
282
332
|
}
|
|
283
333
|
|
|
284
334
|
// Flat belt bottom (return path)
|
|
285
|
-
const bottomGeo =
|
|
335
|
+
const bottomGeo = isVertical
|
|
336
|
+
? new THREE.BoxGeometry(drumLength * 0.85, beltThickness, flatLen)
|
|
337
|
+
: new THREE.BoxGeometry(flatLen, beltThickness, drumLength * 0.85)
|
|
286
338
|
const bottomMesh = new THREE.Mesh(bottomGeo, beltMaterial)
|
|
287
339
|
bottomMesh.position.set(0, beltY - drumRadius, 0)
|
|
288
340
|
this.object3d.add(bottomMesh)
|
|
289
341
|
}
|
|
290
342
|
|
|
291
|
-
private buildRails(width: number, height: number, depth: number, frameH: number, railW: number) {
|
|
343
|
+
private buildRails(width: number, height: number, depth: number, frameH: number, railW: number, isVertical: boolean = false) {
|
|
292
344
|
const color = this.resolveColor()
|
|
293
345
|
const railHeight = frameH * 0.3
|
|
294
346
|
const railY = depth - railHeight / 2
|
|
295
|
-
const
|
|
347
|
+
const beltWidth = isVertical ? width : height
|
|
348
|
+
const transportLen = isVertical ? height : width
|
|
349
|
+
const innerWidth = beltWidth - railW * 2
|
|
296
350
|
// 두 레일 간격: 내부 폭의 60%
|
|
297
351
|
const railSpacing = innerWidth * 0.6
|
|
298
352
|
|
|
@@ -304,11 +358,19 @@ export class Conveyor3D extends RealObjectGroup {
|
|
|
304
358
|
|
|
305
359
|
const railGeometries: THREE.BufferGeometry[] = []
|
|
306
360
|
|
|
307
|
-
// Two parallel rails running along
|
|
308
|
-
|
|
309
|
-
const
|
|
310
|
-
|
|
311
|
-
|
|
361
|
+
// Two parallel rails running along transport direction
|
|
362
|
+
if (isVertical) {
|
|
363
|
+
for (const xSign of [-1, 1]) {
|
|
364
|
+
const rail = new THREE.BoxGeometry(railW * 0.8, railHeight, transportLen - railW * 2)
|
|
365
|
+
rail.translate(xSign * (railSpacing / 2), railY, 0)
|
|
366
|
+
railGeometries.push(rail)
|
|
367
|
+
}
|
|
368
|
+
} else {
|
|
369
|
+
for (const zSign of [-1, 1]) {
|
|
370
|
+
const rail = new THREE.BoxGeometry(transportLen - railW * 2, railHeight, railW * 0.8)
|
|
371
|
+
rail.translate(0, railY, zSign * (railSpacing / 2))
|
|
372
|
+
railGeometries.push(rail)
|
|
373
|
+
}
|
|
312
374
|
}
|
|
313
375
|
|
|
314
376
|
const railMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(railGeometries), railMaterial)
|
|
@@ -9,6 +9,7 @@
|
|
|
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'
|
|
@@ -45,6 +46,8 @@ interface MCS3DConfig {
|
|
|
45
46
|
emissiveIntensity?: number
|
|
46
47
|
/** 바닥면 유지 (기본 false — 바닥면 제거) */
|
|
47
48
|
keepBottomFace?: boolean
|
|
49
|
+
/** syncZPosOffset 고정값 (설정 시 다른 로직 무시) */
|
|
50
|
+
zPosOverride?: number
|
|
48
51
|
}
|
|
49
52
|
|
|
50
53
|
/**
|
|
@@ -88,6 +91,10 @@ class MCSRealObject3D extends RealObjectGroup {
|
|
|
88
91
|
}
|
|
89
92
|
|
|
90
93
|
protected get syncZPosOffset(): number {
|
|
94
|
+
// zPosOverride: 고정 높이 (OHTLine 등)
|
|
95
|
+
if (this._config.zPosOverride != null) {
|
|
96
|
+
return this._config.zPosOverride
|
|
97
|
+
}
|
|
91
98
|
// Port: 이송 표면 높이에 배치
|
|
92
99
|
if (this._config.defaultZPosFromParent) {
|
|
93
100
|
return TRANSPORT_SURFACE_HEIGHT
|
|
@@ -436,18 +443,109 @@ register('Buffer', {
|
|
|
436
443
|
})
|
|
437
444
|
|
|
438
445
|
register('AGVLine', {
|
|
439
|
-
depthRatio: 0
|
|
446
|
+
depthRatio: 0,
|
|
447
|
+
defaultDepth: 0.1,
|
|
440
448
|
metalness: 0.6,
|
|
441
449
|
roughness: 0.3,
|
|
442
450
|
defaultColor: 0x888899,
|
|
443
451
|
})
|
|
444
452
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
453
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
454
|
+
// OHTLine — 천장 I-beam 레일
|
|
455
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
456
|
+
|
|
457
|
+
const OHT_LINE_RAIL_COLOR = 0xcccccc
|
|
458
|
+
const OHT_LINE_HEIGHT = 7 * TRANSPORT_SURFACE_HEIGHT // OHT 상단과 일치
|
|
459
|
+
|
|
460
|
+
class OHTLineRealObject3D extends RealObjectGroup {
|
|
461
|
+
private _mesh?: THREE.Mesh
|
|
462
|
+
|
|
463
|
+
get effectiveDepth(): number { return 0 }
|
|
464
|
+
protected get syncZPosOffset(): number { return OHT_LINE_HEIGHT }
|
|
465
|
+
|
|
466
|
+
build() {
|
|
467
|
+
super.build()
|
|
468
|
+
|
|
469
|
+
const { width = 10, height = 10 } = this.component.state
|
|
470
|
+
const w = Math.abs(width)
|
|
471
|
+
const h = Math.abs(height)
|
|
472
|
+
if (!w || !h) return
|
|
473
|
+
|
|
474
|
+
// I-beam 치수: 이송 방향 = 긴 변, 폭은 2D의 1/2
|
|
475
|
+
const isVertical = w < h
|
|
476
|
+
const shortSide = isVertical ? w : h // 2D 원본 짧은 쪽 폭
|
|
477
|
+
const beltW = shortSide * 0.25 // I-beam 폭: 원본의 1/4
|
|
478
|
+
const beamH = Math.max(beltW * 0.5, 3) // I-beam 전체 높이
|
|
479
|
+
const inset = shortSide * 0.5 // 양끝 여백: 원본 폭의 1/2
|
|
480
|
+
const railLen = (isVertical ? h : w) - inset * 2
|
|
481
|
+
const flangeH = beamH * 0.2 // 플랜지 두께
|
|
482
|
+
const flangeW = beltW * 0.8 // 플랜지 폭
|
|
483
|
+
const webH = beamH - flangeH * 2 // 웹 높이
|
|
484
|
+
const webW = Math.max(flangeW * 0.25, 1) // 웹 두께
|
|
485
|
+
|
|
486
|
+
const mat = new THREE.MeshStandardMaterial({
|
|
487
|
+
color: OHT_LINE_RAIL_COLOR,
|
|
488
|
+
metalness: 0.5,
|
|
489
|
+
roughness: 0.4,
|
|
490
|
+
transparent: true,
|
|
491
|
+
opacity: 1.0,
|
|
492
|
+
depthWrite: false,
|
|
493
|
+
})
|
|
494
|
+
|
|
495
|
+
const geoms: THREE.BufferGeometry[] = []
|
|
496
|
+
|
|
497
|
+
// 상부 플랜지
|
|
498
|
+
const topFlange = isVertical
|
|
499
|
+
? new THREE.BoxGeometry(flangeW, flangeH, railLen)
|
|
500
|
+
: new THREE.BoxGeometry(railLen, flangeH, flangeW)
|
|
501
|
+
topFlange.translate(0, -flangeH / 2, 0)
|
|
502
|
+
geoms.push(topFlange)
|
|
503
|
+
|
|
504
|
+
// 웹 (수직판)
|
|
505
|
+
const web = isVertical
|
|
506
|
+
? new THREE.BoxGeometry(webW, webH, railLen)
|
|
507
|
+
: new THREE.BoxGeometry(railLen, webH, webW)
|
|
508
|
+
web.translate(0, -flangeH - webH / 2, 0)
|
|
509
|
+
geoms.push(web)
|
|
510
|
+
|
|
511
|
+
// 하부 플랜지
|
|
512
|
+
const botFlange = isVertical
|
|
513
|
+
? new THREE.BoxGeometry(flangeW, flangeH, railLen)
|
|
514
|
+
: new THREE.BoxGeometry(railLen, flangeH, flangeW)
|
|
515
|
+
botFlange.translate(0, -flangeH - webH - flangeH / 2, 0)
|
|
516
|
+
geoms.push(botFlange)
|
|
517
|
+
|
|
518
|
+
this._mesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(geoms), mat)
|
|
519
|
+
this._mesh.castShadow = true
|
|
520
|
+
this.object3d.add(this._mesh)
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
update() {
|
|
524
|
+
if (this._mesh) {
|
|
525
|
+
this.object3d.remove(this._mesh)
|
|
526
|
+
this._mesh.geometry.dispose()
|
|
527
|
+
;(this._mesh.material as THREE.Material).dispose()
|
|
528
|
+
this._mesh = undefined
|
|
529
|
+
}
|
|
530
|
+
this.build()
|
|
531
|
+
this.updateTransform()
|
|
532
|
+
this.updateHidden()
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
onchange(after: Record<string, unknown>, before: Record<string, unknown>) {
|
|
536
|
+
if ('width' in after || 'height' in after || 'depth' in after ||
|
|
537
|
+
'fillStyle' in after || 'strokeStyle' in after) {
|
|
538
|
+
this.update()
|
|
539
|
+
return
|
|
540
|
+
}
|
|
541
|
+
super.onchange(after, before)
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
updateDimension() {}
|
|
545
|
+
updateAlpha() {}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
registerRealObjectFactory('OHTLine', (component: Component) => new OHTLineRealObject3D(component))
|
|
451
549
|
|
|
452
550
|
// STOCKER: 벽면에 용량 게이지 바를 포함하는 커스텀 3D 오브젝트
|
|
453
551
|
const STOCKER_CONFIG: MCS3DConfig = {
|
|
@@ -764,7 +862,10 @@ class PortFlowRealObject3D extends RealObjectGroup {
|
|
|
764
862
|
})
|
|
765
863
|
|
|
766
864
|
this._mesh = new THREE.Mesh(geo, mat)
|
|
865
|
+
// 비대칭 패딩 보정: 플레인 센터를 컴포넌트 bounds 센터에 맞춤
|
|
866
|
+
this._mesh.position.x = (padRight - padLeft) / 2
|
|
767
867
|
this._mesh.position.y = 1
|
|
868
|
+
this._mesh.position.z = (padBottom - padTop) / 2
|
|
768
869
|
this.object3d.add(this._mesh)
|
|
769
870
|
}
|
|
770
871
|
|
|
@@ -779,6 +880,7 @@ class PortFlowRealObject3D extends RealObjectGroup {
|
|
|
779
880
|
}
|
|
780
881
|
this.build()
|
|
781
882
|
this.updateTransform()
|
|
883
|
+
this.updateHidden()
|
|
782
884
|
}
|
|
783
885
|
|
|
784
886
|
onchange(after: Record<string, unknown>, before: Record<string, unknown>) {
|
|
@@ -795,3 +897,91 @@ class PortFlowRealObject3D extends RealObjectGroup {
|
|
|
795
897
|
}
|
|
796
898
|
|
|
797
899
|
registerRealObjectFactory('PortFlow', (component: Component) => new PortFlowRealObject3D(component))
|
|
900
|
+
|
|
901
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
902
|
+
// Node — 2D render를 바닥 Plane 텍스처로 3D 표시
|
|
903
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
904
|
+
|
|
905
|
+
class NodeRealObject3D extends RealObjectGroup {
|
|
906
|
+
private _mesh?: THREE.Mesh
|
|
907
|
+
private _texture?: THREE.CanvasTexture
|
|
908
|
+
|
|
909
|
+
get effectiveDepth(): number { return 0 }
|
|
910
|
+
protected get syncZPosOffset(): number { return 0 }
|
|
911
|
+
|
|
912
|
+
build() {
|
|
913
|
+
super.build()
|
|
914
|
+
|
|
915
|
+
const { cx, cy } = this.component.state
|
|
916
|
+
const radius = 8
|
|
917
|
+
const size = radius * 2
|
|
918
|
+
const scale = 2
|
|
919
|
+
const pad = 2
|
|
920
|
+
|
|
921
|
+
const canvas = document.createElement('canvas')
|
|
922
|
+
canvas.width = (size + pad * 2) * scale
|
|
923
|
+
canvas.height = (size + pad * 2) * scale
|
|
924
|
+
const ctx = canvas.getContext('2d')!
|
|
925
|
+
|
|
926
|
+
ctx.save()
|
|
927
|
+
ctx.scale(scale, scale)
|
|
928
|
+
ctx.translate(-cx + radius + pad, -cy + radius + pad)
|
|
929
|
+
try {
|
|
930
|
+
this.component.render(ctx)
|
|
931
|
+
const { fillStyle = '#888888' } = this.component.state
|
|
932
|
+
ctx.fillStyle = typeof fillStyle === 'string' ? fillStyle : '#888888'
|
|
933
|
+
ctx.fill()
|
|
934
|
+
const { strokeStyle } = this.component.state
|
|
935
|
+
if (strokeStyle && typeof strokeStyle === 'string') {
|
|
936
|
+
ctx.strokeStyle = strokeStyle
|
|
937
|
+
ctx.lineWidth = 1
|
|
938
|
+
ctx.stroke()
|
|
939
|
+
}
|
|
940
|
+
} catch { /* render 실패 시 빈 텍스처 */ }
|
|
941
|
+
ctx.restore()
|
|
942
|
+
|
|
943
|
+
this._texture = new THREE.CanvasTexture(canvas)
|
|
944
|
+
this._texture.colorSpace = THREE.SRGBColorSpace
|
|
945
|
+
|
|
946
|
+
const geo = new THREE.PlaneGeometry(size + pad * 2, size + pad * 2)
|
|
947
|
+
geo.rotateX(-Math.PI / 2)
|
|
948
|
+
|
|
949
|
+
const mat = new THREE.MeshBasicMaterial({
|
|
950
|
+
map: this._texture,
|
|
951
|
+
transparent: true,
|
|
952
|
+
side: THREE.DoubleSide,
|
|
953
|
+
depthWrite: false,
|
|
954
|
+
})
|
|
955
|
+
|
|
956
|
+
this._mesh = new THREE.Mesh(geo, mat)
|
|
957
|
+
this._mesh.position.y = 1
|
|
958
|
+
this.object3d.add(this._mesh)
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
update() {
|
|
962
|
+
if (this._mesh) {
|
|
963
|
+
this.object3d.remove(this._mesh)
|
|
964
|
+
this._mesh.geometry.dispose()
|
|
965
|
+
;(this._mesh.material as THREE.Material).dispose()
|
|
966
|
+
this._texture?.dispose()
|
|
967
|
+
this._mesh = undefined
|
|
968
|
+
this._texture = undefined
|
|
969
|
+
}
|
|
970
|
+
this.build()
|
|
971
|
+
this.updateTransform()
|
|
972
|
+
this.updateHidden()
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
onchange(after: Record<string, unknown>, before: Record<string, unknown>) {
|
|
976
|
+
if ('fillStyle' in after || 'strokeStyle' in after) {
|
|
977
|
+
this.update()
|
|
978
|
+
return
|
|
979
|
+
}
|
|
980
|
+
super.onchange(after, before)
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
updateDimension() {}
|
|
984
|
+
updateAlpha() {}
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
registerRealObjectFactory('Node', (component: Component) => new NodeRealObject3D(component))
|