@fmsim/machine 2.0.0-beta.20 → 2.0.0-beta.23

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.20",
5
+ "version": "2.0.0-beta.23",
6
6
  "main": "dist/index.js",
7
7
  "module": "dist/index.js",
8
8
  "things-scene": true,
@@ -26,7 +26,7 @@
26
26
  "migration": "things-factory-migration"
27
27
  },
28
28
  "dependencies": {
29
- "@fmsim/api": "^2.0.0-beta.16",
29
+ "@fmsim/api": "^2.0.0-beta.21",
30
30
  "@hatiolab/things-scene": "^10.0.0-beta.1"
31
31
  },
32
32
  "devDependencies": {
@@ -55,5 +55,5 @@
55
55
  "prettier --write"
56
56
  ]
57
57
  },
58
- "gitHead": "a1eba5205556e34ec21bbecbd7816fe0eaa85dce"
58
+ "gitHead": "65798679f16af5b8ca50f2bc9d198d1ea84ed79e"
59
59
  }
package/src/conveyor.ts CHANGED
@@ -14,6 +14,14 @@ const NATURE: ComponentNature = {
14
14
  options: ['', 'conveyor', 'belt', 'rail']
15
15
  }
16
16
  },
17
+ {
18
+ type: 'select',
19
+ label: 'conveyor-orientation',
20
+ name: 'orientation',
21
+ property: {
22
+ options: ['', 'horizontal', 'vertical']
23
+ }
24
+ },
17
25
  ...MCSTransport.properties
18
26
  ]
19
27
  }
@@ -41,14 +41,14 @@ export class AGV3D extends RealObjectGroup {
41
41
  // '#F0F0F0'은 상태 미설정 시 기본 fallback — 무시하고 defaultColor 사용
42
42
  try {
43
43
  const sc = comp.statusColor
44
- if (typeof sc === 'string' && sc && sc.toUpperCase() !== '#F0F0F0') return new THREE.Color(sc).getHex()
44
+ if (typeof sc === 'string' && sc && sc.toUpperCase() !== '#F0F0F0' && sc !== 'transparent') return new THREE.Color(sc).getHex()
45
45
  } catch {
46
46
  /* statusColor 미구현 시 무시 */
47
47
  }
48
48
 
49
49
  // 2) state.fillStyle fallback
50
50
  const { fillStyle } = this.component.state
51
- if (fillStyle && typeof fillStyle === 'string') {
51
+ if (fillStyle && typeof fillStyle === 'string' && fillStyle !== 'transparent') {
52
52
  try {
53
53
  return new THREE.Color(fillStyle).getHex()
54
54
  } catch {
@@ -40,14 +40,14 @@ export class Conveyor3D extends RealObjectGroup {
40
40
  // '#F0F0F0'은 상태 미설정 시 기본 fallback — 무시하고 defaultColor 사용
41
41
  try {
42
42
  const sc = comp.statusColor
43
- if (typeof sc === 'string' && sc && sc.toUpperCase() !== '#F0F0F0') return new THREE.Color(sc).getHex()
43
+ if (typeof sc === 'string' && sc && sc.toUpperCase() !== '#F0F0F0' && sc !== 'transparent') return new THREE.Color(sc).getHex()
44
44
  } catch {
45
45
  /* statusColor 미구현 시 무시 */
46
46
  }
47
47
 
48
48
  // 2) state.fillStyle fallback
49
49
  const { fillStyle } = this.component.state
50
- if (fillStyle && typeof fillStyle === 'string') {
50
+ if (fillStyle && typeof fillStyle === 'string' && fillStyle !== 'transparent') {
51
51
  try {
52
52
  return new THREE.Color(fillStyle).getHex()
53
53
  } catch {
@@ -61,9 +61,19 @@ export class Conveyor3D extends RealObjectGroup {
61
61
  build() {
62
62
  super.build()
63
63
 
64
- const { width, height, conveyorType } = this.component.state
64
+ const { width: rawWidth, height: rawHeight, conveyorType, orientation } = this.component.state
65
65
  const depth = this.effectiveDepth
66
- if (!width || !height) return
66
+ if (!rawWidth || !rawHeight) return
67
+
68
+ // orientation: 'horizontal' = 롤러가 Z축(세로), 'vertical' = 롤러가 X축(가로)
69
+ // default = 긴 방향에 수직 (긴 축을 따라 이송)
70
+ const isVertical = orientation === 'vertical' ? true
71
+ : orientation === 'horizontal' ? false
72
+ : rawWidth < rawHeight // default: 세로가 길면 롤러는 가로(X축)
73
+ // isVertical=true → 롤러 축이 X(가로), 이송 방향이 Z(세로)
74
+ // isVertical=false → 롤러 축이 Z(세로), 이송 방향이 X(가로)
75
+ const width = rawWidth
76
+ const height = rawHeight
67
77
 
68
78
  // 2D와 동일 로직으로 rollWidth 산출
69
79
  const rollWidth = Math.max(10, Math.min(width, height) / 2)
@@ -138,7 +148,7 @@ export class Conveyor3D extends RealObjectGroup {
138
148
  } else if (conveyorType === 'belt') {
139
149
  this.buildBelt(width, height, depth, frameH, railW)
140
150
  } else {
141
- this.buildRollers(width, height, depth, frameH, railW, rollWidth)
151
+ this.buildRollers(width, height, depth, frameH, railW, rollWidth, isVertical)
142
152
  }
143
153
  }
144
154
 
@@ -148,28 +158,45 @@ export class Conveyor3D extends RealObjectGroup {
148
158
  depth: number,
149
159
  frameH: number,
150
160
  railW: number,
151
- rollWidth: number
161
+ rollWidth: number,
162
+ isVertical: boolean = false
152
163
  ) {
153
164
  const rollerRadius = Math.max(rollWidth / 8, 1)
154
- const rollerLength = height - railW * 2 - 0.5
155
165
  const diameter = rollerRadius * 2
156
- // 롤러 간 최소 간격 (z-fighting 방지용 미세 갭만)
157
166
  const step = diameter * 1.05
158
- // 롤러 상단 = 이송 표면 (y=depth)
159
167
  const rollerY = depth - rollerRadius
160
168
  const color = this.resolveColor()
161
169
 
162
170
  const rollerGeometries: THREE.BufferGeometry[] = []
163
- const count = Math.max(1, Math.floor(width / step))
164
- const totalSpan = (count - 1) * step
165
- const startX = -totalSpan / 2
166
-
167
- for (let i = 0; i < count; i++) {
168
- const x = startX + i * step
169
- const roller = new THREE.CylinderGeometry(rollerRadius, rollerRadius, rollerLength, 16)
170
- roller.rotateX(Math.PI / 2)
171
- roller.translate(x, rollerY, 0)
172
- rollerGeometries.push(roller)
171
+
172
+ if (isVertical) {
173
+ // 롤러 축 = X축(가로), 이송 방향 = Z축(세로)
174
+ const rollerLength = width - railW * 2 - 0.5
175
+ const count = Math.max(1, Math.floor(height / step))
176
+ const totalSpan = (count - 1) * step
177
+ const startZ = -totalSpan / 2
178
+
179
+ for (let i = 0; i < count; i++) {
180
+ const z = startZ + i * step
181
+ const roller = new THREE.CylinderGeometry(rollerRadius, rollerRadius, rollerLength, 16)
182
+ roller.rotateZ(Math.PI / 2)
183
+ roller.translate(0, rollerY, z)
184
+ rollerGeometries.push(roller)
185
+ }
186
+ } else {
187
+ // 롤러 축 = Z축(세로), 이송 방향 = X축(가로) — 기존 동작
188
+ const rollerLength = height - railW * 2 - 0.5
189
+ const count = Math.max(1, Math.floor(width / step))
190
+ const totalSpan = (count - 1) * step
191
+ const startX = -totalSpan / 2
192
+
193
+ for (let i = 0; i < count; i++) {
194
+ const x = startX + i * step
195
+ const roller = new THREE.CylinderGeometry(rollerRadius, rollerRadius, rollerLength, 16)
196
+ roller.rotateX(Math.PI / 2)
197
+ roller.translate(x, rollerY, 0)
198
+ rollerGeometries.push(roller)
199
+ }
173
200
  }
174
201
 
175
202
  if (rollerGeometries.length > 0) {
@@ -295,6 +322,7 @@ export class Conveyor3D extends RealObjectGroup {
295
322
  onchange(after: Record<string, unknown>, before: Record<string, unknown>) {
296
323
  if (
297
324
  'conveyorType' in after ||
325
+ 'orientation' in after ||
298
326
  'width' in after ||
299
327
  'height' in after ||
300
328
  'depth' in after ||
@@ -111,14 +111,14 @@ class MCSRealObject3D extends RealObjectGroup {
111
111
  // '#F0F0F0'은 상태 미설정 시 기본 fallback — 무시하고 defaultColor 사용
112
112
  try {
113
113
  const sc = comp.statusColor
114
- if (typeof sc === 'string' && sc && sc.toUpperCase() !== '#F0F0F0') return new THREE.Color(sc).getHex()
114
+ if (typeof sc === 'string' && sc && sc.toUpperCase() !== '#F0F0F0' && sc !== 'transparent') return new THREE.Color(sc).getHex()
115
115
  } catch {
116
116
  /* statusColor 미구현 시 무시 */
117
117
  }
118
118
 
119
119
  // 2) state.fillStyle fallback
120
120
  const { fillStyle } = this.component.state
121
- if (fillStyle && typeof fillStyle === 'string') {
121
+ if (fillStyle && typeof fillStyle === 'string' && fillStyle !== 'transparent') {
122
122
  try {
123
123
  return new THREE.Color(fillStyle).getHex()
124
124
  } catch {
@@ -685,3 +685,113 @@ registerRealObjectFactory('Carrier', (component: Component) => new CarrierRealOb
685
685
  registerRealObjectFactory('StockerCapacityBar', () => null as any)
686
686
  registerRealObjectFactory('MCSGaugeCapacityBar', () => null as any)
687
687
  registerRealObjectFactory('ZoneCapacityBar', () => null as any)
688
+
689
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
690
+ // PortFlow — 2D render를 Plane 텍스처로 3D 표시
691
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
692
+
693
+ class PortFlowRealObject3D extends RealObjectGroup {
694
+ private _mesh?: THREE.Mesh
695
+ private _texture?: THREE.CanvasTexture
696
+
697
+ get effectiveDepth(): number { return 0 }
698
+ protected get syncZPosOffset(): number {
699
+ // 부모 Port/Conveyor의 zPos와 동일하게
700
+ const parent = (this.component as any).parentObject
701
+ if (parent?.state?.zPos != null) return parent.state.zPos
702
+ return TRANSPORT_SURFACE_HEIGHT
703
+ }
704
+
705
+ build() {
706
+ super.build()
707
+
708
+ const { width = 10, height = 10 } = this.component.state
709
+ const w = Math.abs(width)
710
+ const h = Math.abs(height)
711
+ if (!w || !h) return
712
+
713
+ // 2D render를 캔버스에 캡처
714
+ // 화살표 날개 + 텍스트 영역이 bounds 밖으로 나가므로 전체 영역 계산
715
+ const { left, top } = this.component.bounds
716
+ const { fontSize = 24, labelPosition = 'top' } = this.component.state
717
+ const arrowPad = Math.min(w, h) * 0.4
718
+
719
+ // 텍스트 영역 확장
720
+ let padLeft = arrowPad, padRight = arrowPad, padTop = arrowPad, padBottom = arrowPad
721
+ if ((this.component as any).hasTextProperty) {
722
+ switch (labelPosition) {
723
+ case 'top': padTop = Math.max(padTop, fontSize + 4); break
724
+ case 'bottom': padBottom = Math.max(padBottom, fontSize + 4); break
725
+ case 'left': padLeft = Math.max(padLeft, fontSize * 4 + 4); break
726
+ case 'right': padRight = Math.max(padRight, fontSize * 4 + 4); break
727
+ }
728
+ }
729
+
730
+ const totalW = w + padLeft + padRight
731
+ const totalH = h + padTop + padBottom
732
+ const scale = 2
733
+ const canvas = document.createElement('canvas')
734
+ canvas.width = totalW * scale
735
+ canvas.height = totalH * scale
736
+ const ctx = canvas.getContext('2d')!
737
+
738
+ ctx.save()
739
+ ctx.scale(scale, scale)
740
+ ctx.translate(-left + padLeft, -top + padTop)
741
+ try {
742
+ // 화살표 렌더
743
+ this.component.render(ctx)
744
+ ctx.fillStyle = this.component.state.strokeStyle || '#666666'
745
+ ctx.fill()
746
+ // 텍스트 렌더
747
+ if ((this.component as any).hasTextProperty && (this.component as any).drawText) {
748
+ ;(this.component as any).drawText(ctx)
749
+ }
750
+ } catch { /* render 실패 시 빈 텍스처 */ }
751
+ ctx.restore()
752
+
753
+ this._texture = new THREE.CanvasTexture(canvas)
754
+ this._texture.colorSpace = THREE.SRGBColorSpace
755
+
756
+ const geo = new THREE.PlaneGeometry(totalW, totalH)
757
+ geo.rotateX(-Math.PI / 2)
758
+
759
+ const mat = new THREE.MeshBasicMaterial({
760
+ map: this._texture,
761
+ transparent: true,
762
+ side: THREE.DoubleSide,
763
+ depthWrite: false,
764
+ })
765
+
766
+ this._mesh = new THREE.Mesh(geo, mat)
767
+ this._mesh.position.y = 1
768
+ this.object3d.add(this._mesh)
769
+ }
770
+
771
+ update() {
772
+ if (this._mesh) {
773
+ this.object3d.remove(this._mesh)
774
+ this._mesh.geometry.dispose()
775
+ ;(this._mesh.material as THREE.Material).dispose()
776
+ this._texture?.dispose()
777
+ this._mesh = undefined
778
+ this._texture = undefined
779
+ }
780
+ this.build()
781
+ this.updateTransform()
782
+ }
783
+
784
+ onchange(after: Record<string, unknown>, before: Record<string, unknown>) {
785
+ if ('width' in after || 'height' in after || 'orientation' in after ||
786
+ 'direction' in after || 'strokeStyle' in after || 'INOUTTYPE' in after) {
787
+ this.update()
788
+ return
789
+ }
790
+ super.onchange(after, before)
791
+ }
792
+
793
+ updateDimension() {}
794
+ updateAlpha() {}
795
+ }
796
+
797
+ registerRealObjectFactory('PortFlow', (component: Component) => new PortFlowRealObject3D(component))
@@ -46,5 +46,6 @@
46
46
  "label.node-machine": "node machine",
47
47
  "label.nodes": "nodes",
48
48
  "label.vehicle-animation": "vehicle animation",
49
- "label.battery-rate-legend-name": "battery rate"
49
+ "label.battery-rate-legend-name": "battery rate",
50
+ "label.conveyor-orientation": "Transfer Direction"
50
51
  }
@@ -48,5 +48,6 @@
48
48
  "label.node-machine": "node machine",
49
49
  "label.nodes": "nodes",
50
50
  "label.vehicle-animation": "vehicle animation",
51
- "label.battery-rate-legend-name": "battery rate"
51
+ "label.battery-rate-legend-name": "battery rate",
52
+ "label.conveyor-orientation": "搬送方向"
52
53
  }
@@ -49,5 +49,6 @@
49
49
  "label.node-machine": "node machine",
50
50
  "label.nodes": "nodes",
51
51
  "label.vehicle-animation": "vehicle animation",
52
- "label.battery-rate-legend-name": "배터리 잔량"
52
+ "label.battery-rate-legend-name": "배터리 잔량",
53
+ "label.conveyor-orientation": "이송방향"
53
54
  }
@@ -48,5 +48,6 @@
48
48
  "label.node-machine": "node machine",
49
49
  "label.nodes": "nodes",
50
50
  "label.vehicle-animation": "vehicle animation",
51
- "label.battery-rate-legend-name": "battery rate"
51
+ "label.battery-rate-legend-name": "battery rate",
52
+ "label.conveyor-orientation": "Transfer Direction"
52
53
  }
@@ -48,5 +48,6 @@
48
48
  "label.node-machine": "node machine",
49
49
  "label.nodes": "nodes",
50
50
  "label.vehicle-animation": "vehicle animation",
51
- "label.battery-rate-legend-name": "battery rate"
51
+ "label.battery-rate-legend-name": "battery rate",
52
+ "label.conveyor-orientation": "输送方向"
52
53
  }