@fmsim/machine 2.0.0-beta.24 → 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/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.24",
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": "74ce3ac3d23c1fa9961ae505ac5c36d212d319f1"
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
- const railW = Math.max(height * 0.06, 2)
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 (along X)
98
- for (const zSign of [-1, 1]) {
99
- const rail = new THREE.BoxGeometry(width, frameH, railW)
100
- rail.translate(0, railY, zSign * (height / 2 - railW / 2))
101
- frameGeometries.push(rail)
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(lx, legCenterY, lz)
131
+ leg.translate(xSign * legXPos, legCenterY, zSign * legZPos)
115
132
  frameGeometries.push(leg)
116
133
  }
117
134
  }
118
135
 
119
- // Cross-bracing: 앞뒤 다리 연결 (Z 방향)
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
- for (const xSign of [-1, 1]) {
126
- const brace = new THREE.BoxGeometry(braceW, braceH, braceLen)
127
- brace.translate(xSign * (width / 2 - legThickness / 2), braceY, 0)
128
- frameGeometries.push(brace)
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
- // Cross-bracing: 좌우 다리 연결 (X 방향)
132
- const braceLenX = width - legThickness
133
- for (const zSign of [-1, 1]) {
134
- const brace = new THREE.BoxGeometry(braceLenX, braceH, braceW)
135
- brace.translate(0, braceY, zSign * (height / 2 - railW / 2))
136
- frameGeometries.push(brace)
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 leftDrumX = -width / 2 + drumInset
222
- const rightDrumX = width / 2 - drumInset
257
+ const drum1 = -transportLen / 2 + drumInset
258
+ const drum2 = transportLen / 2 - drumInset
223
259
 
224
- for (const dx of [leftDrumX, rightDrumX]) {
260
+ for (const dp of [drum1, drum2]) {
225
261
  const drum = new THREE.CylinderGeometry(drumRadius, drumRadius, drumLength, 16)
226
- drum.rotateX(Math.PI / 2)
227
- drum.translate(dx, beltY, 0)
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 = rightDrumX - leftDrumX
248
- const flatGeo = new THREE.BoxGeometry(flatLen, beltThickness, drumLength)
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 [dx, angleStart] of [
257
- [leftDrumX, Math.PI / 2],
258
- [rightDrumX, -Math.PI / 2],
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 x0 = dx + Math.cos(a0) * r
309
+ const p0 = dp + Math.cos(a0) * r
267
310
  const y0 = beltY + Math.sin(a0) * r
268
- const x1 = dx + Math.cos(a1) * r
311
+ const p1 = dp + Math.cos(a1) * r
269
312
  const y1 = beltY + Math.sin(a1) * r
270
313
 
271
- const segLen = Math.sqrt((x1 - x0) ** 2 + (y1 - y0) ** 2) * 1.05
272
- const segAngle = Math.atan2(y1 - y0, x1 - x0)
273
-
274
- const seg = new THREE.BoxGeometry(segLen, beltThickness, drumLength)
275
- seg.rotateZ(segAngle)
276
- seg.translate((x0 + x1) / 2, (y0 + y1) / 2, 0)
277
- wrapGeometries.push(seg)
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 = new THREE.BoxGeometry(flatLen, beltThickness, drumLength * 0.85)
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 innerWidth = height - railW * 2
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 X axis
308
- for (const zSign of [-1, 1]) {
309
- const rail = new THREE.BoxGeometry(width - railW * 2, railHeight, railW * 0.8)
310
- rail.translate(0, railY, zSign * (railSpacing / 2))
311
- railGeometries.push(rail)
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.12,
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
- register('OHTLine', {
446
- depthRatio: 0.12,
447
- metalness: 0.6,
448
- roughness: 0.3,
449
- defaultColor: 0x888899,
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 = {
@@ -782,6 +880,7 @@ class PortFlowRealObject3D extends RealObjectGroup {
782
880
  }
783
881
  this.build()
784
882
  this.updateTransform()
883
+ this.updateHidden()
785
884
  }
786
885
 
787
886
  onchange(after: Record<string, unknown>, before: Record<string, unknown>) {
@@ -798,3 +897,91 @@ class PortFlowRealObject3D extends RealObjectGroup {
798
897
  }
799
898
 
800
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))
package/src/node.ts CHANGED
@@ -28,7 +28,7 @@ export default class Node extends Shape {
28
28
  static RADIUS = 8
29
29
 
30
30
  is3dish() {
31
- return false
31
+ return true
32
32
  }
33
33
 
34
34
  isDescendible() {