@fmsim/machine 1.0.41 → 1.0.42

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.
@@ -0,0 +1,312 @@
1
+ /*
2
+ * Copyright © HatioLab Inc. All rights reserved.
3
+ */
4
+
5
+ import { Component, Line, Control } from '@hatiolab/things-scene'
6
+ import { Node, Position } from './types'
7
+
8
+ /**
9
+ * NodePath 컴포넌트의 상태 인터페이스
10
+ */
11
+ export interface NodePathState {
12
+ nodes: Node[]
13
+ lineColor?: string
14
+ lineWidth?: number
15
+ defaultNodeSize?: number
16
+ defaultNodeColor?: string
17
+ nodeShape?: string
18
+ [key: string]: any
19
+ }
20
+
21
+ var controlHandler = {
22
+ currentIndex: 0,
23
+ ondragstart: function (point, index, component) {
24
+ component.mutatePath(null, function (path) {
25
+ const nodes = component.state.nodes as Node[]
26
+ const control = component.controls[index]
27
+ const { from, to, id } = control as any
28
+ const newId = `${Date.now()}`
29
+
30
+ if (id) {
31
+ const fromNode = nodes.find(node => node.id === id)
32
+ const fromIndex = nodes.indexOf(fromNode!)
33
+ controlHandler.currentIndex = fromIndex == 0 ? 0 : fromIndex + 1
34
+
35
+ nodes.splice(controlHandler.currentIndex, 0, {
36
+ id: newId,
37
+ position: point,
38
+ name: '',
39
+ connections: []
40
+ })
41
+
42
+ fromNode!.connections.push(newId)
43
+ path.splice(controlHandler.currentIndex, 0, point)
44
+ } else {
45
+ const fromNode = nodes.find(node => node.id === from)
46
+ controlHandler.currentIndex = nodes.indexOf(fromNode!) + 1
47
+
48
+ nodes.splice(controlHandler.currentIndex, 0, {
49
+ id: newId,
50
+ position: point,
51
+ name: '',
52
+ connections: [to]
53
+ })
54
+
55
+ const connections = fromNode!.connections
56
+ connections.splice(connections.indexOf(to), 1, newId)
57
+
58
+ path.splice(controlHandler.currentIndex, 0, point)
59
+ }
60
+ })
61
+ },
62
+
63
+ ondragmove: function (point, index, component) {
64
+ component.mutatePath(null, function (path) {
65
+ path[controlHandler.currentIndex] = point
66
+ })
67
+ },
68
+
69
+ ondragend: function (point, index, component) {}
70
+ }
71
+
72
+ const NATURE = {
73
+ mutable: false,
74
+ resizable: true,
75
+ rotatable: true,
76
+ properties: [
77
+ {
78
+ type: 'nodes',
79
+ label: 'nodes',
80
+ name: 'nodes'
81
+ }
82
+ ],
83
+ help: 'scene/component/node-path'
84
+ }
85
+
86
+ export default class NodePath extends Line {
87
+ static get nature() {
88
+ return NATURE
89
+ }
90
+
91
+ // 상태 프로퍼티에 타입 어노테이션을 추가합니다
92
+ declare state: NodePathState
93
+
94
+ get path(): Position[] {
95
+ return this.state.nodes.map(node => node.position)
96
+ }
97
+
98
+ set path(path: Position[]) {
99
+ const nodes = this.state.nodes || []
100
+
101
+ this.set(
102
+ 'nodes',
103
+ nodes.map((node, index) => ({ ...node, position: path[index] }))
104
+ )
105
+ }
106
+
107
+ contains(x: number, y: number): boolean {
108
+ var path = this.path
109
+ var result = false
110
+
111
+ path.forEach((p, idx) => {
112
+ let j = (idx + path.length + 1) % path.length
113
+
114
+ let x1 = p.x
115
+ let y1 = p.y
116
+ let x2 = path[j].x
117
+ let y2 = path[j].y
118
+
119
+ if (y1 > y != y2 > y && x < ((x2 - x1) * (y - y1)) / (y2 - y1) + x1) result = !result
120
+ })
121
+
122
+ return result
123
+ }
124
+
125
+ render(ctx: CanvasRenderingContext2D): void {
126
+ // 모든 연결 렌더링
127
+ this.renderConnections(ctx)
128
+
129
+ // 모든 노드 렌더링
130
+ this.renderNodes(ctx)
131
+ }
132
+
133
+ renderConnections(ctx: CanvasRenderingContext2D): void {
134
+ const { nodes = [], lineColor = '#000', lineWidth = 1 } = this.state
135
+
136
+ nodes.forEach(node => {
137
+ const { x: x1, y: y1 } = node.position
138
+ const connections = node.connections || []
139
+
140
+ connections.forEach(targetId => {
141
+ const targetNode = this.findNodeById(targetId)
142
+ if (targetNode) {
143
+ const { x: x2, y: y2 } = targetNode.position
144
+
145
+ ctx.beginPath()
146
+ ctx.moveTo(x1, y1)
147
+ ctx.lineTo(x2, y2)
148
+ ctx.strokeStyle = lineColor
149
+ ctx.lineWidth = lineWidth
150
+
151
+ ctx.stroke()
152
+ }
153
+ })
154
+ })
155
+ }
156
+
157
+ renderNodes(ctx: CanvasRenderingContext2D): void {
158
+ const { nodes = [], fillStyle = '#fff', strokeStyle = '#000' } = this.state
159
+
160
+ nodes.forEach(node => {
161
+ const { x, y } = node.position
162
+
163
+ ctx.beginPath()
164
+
165
+ ctx.arc(x, y, 10, 0, Math.PI * 2)
166
+
167
+ ctx.fillStyle = fillStyle
168
+ ctx.strokeStyle = strokeStyle
169
+
170
+ ctx.fill()
171
+ })
172
+ }
173
+
174
+ findNodeById(id: string): Node | undefined {
175
+ const nodes = this.state.nodes || []
176
+ return nodes.find(node => node.id === id)
177
+ }
178
+
179
+ findNodeIndexById(id: string): number {
180
+ const nodes = this.state.nodes || []
181
+ return nodes.findIndex(node => node.id === id)
182
+ }
183
+
184
+ addNode(x: number, y: number, name: string = '', type: string = 'normal'): string {
185
+ const nodes = [...(this.state.nodes || [])]
186
+ const nodeId = `${Date.now()}`
187
+
188
+ const node: Node = {
189
+ id: nodeId,
190
+ position: { x, y },
191
+ name,
192
+ connections: []
193
+ }
194
+
195
+ nodes.push(node)
196
+ this.setState('nodes', nodes)
197
+
198
+ return nodeId
199
+ }
200
+
201
+ moveNode(index: number, position: Position): void {
202
+ const nodes = [...(this.state.nodes || [])]
203
+ if (nodes[index]) {
204
+ nodes[index].position = position
205
+ this.setState('nodes', nodes)
206
+ }
207
+ }
208
+
209
+ connectNodes(sourceId: string, targetId: string): boolean {
210
+ if (sourceId === targetId) return false
211
+
212
+ const nodes = [...(this.state.nodes || [])]
213
+ const sourceIndex = this.findNodeIndexById(sourceId)
214
+ const targetIndex = this.findNodeIndexById(targetId)
215
+
216
+ if (sourceIndex >= 0 && targetIndex >= 0) {
217
+ // 이미 연결되어 있는지 확인
218
+ if (!nodes[sourceIndex].connections.includes(targetId)) {
219
+ nodes[sourceIndex].connections.push(targetId)
220
+ }
221
+
222
+ // 양방향 연결 (선택적)
223
+ // if (!nodes[targetIndex].connections.includes(sourceId)) {
224
+ // nodes[targetIndex].connections.push(sourceId)
225
+ // }
226
+
227
+ this.setState('nodes', nodes)
228
+ return true
229
+ }
230
+
231
+ return false
232
+ }
233
+
234
+ disconnectNodes(sourceId: string, targetId: string): void {
235
+ const nodes = [...(this.state.nodes || [])]
236
+ const sourceIndex = this.findNodeIndexById(sourceId)
237
+ const targetIndex = this.findNodeIndexById(targetId)
238
+
239
+ if (sourceIndex >= 0) {
240
+ nodes[sourceIndex].connections = nodes[sourceIndex].connections.filter(id => id !== targetId)
241
+ }
242
+
243
+ // 양방향 연결 해제 (선택적)
244
+ // if (targetIndex >= 0) {
245
+ // nodes[targetIndex].connections = nodes[targetIndex].connections.filter(id => id !== sourceId)
246
+ // }
247
+
248
+ this.setState('nodes', nodes)
249
+ }
250
+
251
+ removeNode(nodeId: string): void {
252
+ let nodes = [...(this.state.nodes || [])]
253
+
254
+ // 해당 노드 제거
255
+ nodes = nodes.filter(node => node.id !== nodeId)
256
+
257
+ // 다른 노드들의 연결에서도 제거
258
+ nodes.forEach(node => {
259
+ node.connections = node.connections.filter(id => id !== nodeId)
260
+ })
261
+
262
+ this.setState('nodes', nodes)
263
+ }
264
+
265
+ // 특정 노드 연결에 대한 속성 설정
266
+ setConnectionProperty(sourceId: string, targetId: string, propertyName: string, value: any): boolean {
267
+ const nodes = [...(this.state.nodes || [])]
268
+ const sourceIndex = this.findNodeIndexById(sourceId)
269
+
270
+ if (sourceIndex >= 0 && nodes[sourceIndex].connections.includes(targetId)) {
271
+ this.setState('nodes', nodes)
272
+ return true
273
+ }
274
+
275
+ return false
276
+ }
277
+
278
+ get controls(): Control[] {
279
+ var { nodes } = this.state
280
+ var controls = [] as Control[]
281
+
282
+ for (let i = 0; i < nodes.length; i++) {
283
+ const { id: sourceId, position: p1, connections = [] } = nodes[i]
284
+ controls.push({
285
+ x: p1.x,
286
+ y: p1.y,
287
+ //@ts-ignore
288
+ id: sourceId,
289
+ handler: controlHandler
290
+ })
291
+
292
+ connections.forEach(targetId => {
293
+ const targetNode = this.findNodeById(targetId)
294
+ const { position: p2 } = targetNode!
295
+
296
+ controls.push({
297
+ x: (p1.x + p2.x) / 2,
298
+ y: (p1.y + p2.y) / 2,
299
+ //@ts-ignore
300
+ from: sourceId,
301
+ //@ts-ignore
302
+ to: targetId,
303
+ handler: controlHandler
304
+ })
305
+ })
306
+ }
307
+
308
+ return controls
309
+ }
310
+ }
311
+
312
+ Component.register('NodePath', NodePath)
@@ -17,6 +17,8 @@ import agvLine from './agv-line'
17
17
  import ohtLine from './oht-line'
18
18
  import conveyor from './conveyor'
19
19
 
20
+ import NodePath from './node-path'
21
+
20
22
  export default [
21
23
  StockerCapacityBar,
22
24
  ZoneCapacityBar,
@@ -35,5 +37,7 @@ export default [
35
37
  Shuttle,
36
38
  Crane,
37
39
  Shelf,
38
- Port
40
+ Port,
41
+
42
+ NodePath
39
43
  ]
@@ -0,0 +1,48 @@
1
+ const nodePath = new URL('../../icons/node-path.png', import.meta.url).href
2
+
3
+ export default {
4
+ type: 'NodePath',
5
+ description: 'node path',
6
+ icon: nodePath,
7
+ group: ['etc'],
8
+ model: {
9
+ type: 'NodePath',
10
+ left: 100,
11
+ top: 100,
12
+ width: 20,
13
+ height: 20,
14
+ fillStyle: '#fff',
15
+ strokeStyle: 'rgb(0, 0, 0, 0.2)',
16
+ alpha: 0.2,
17
+ lineWidth: 12,
18
+ lineDash: 'solid',
19
+ lineCap: 'round',
20
+ joinStyle: 'round',
21
+ nodes: [
22
+ {
23
+ id: '1',
24
+ position: { x: 100, y: 100 },
25
+ name: 'Node 1',
26
+ connections: ['2']
27
+ },
28
+ {
29
+ id: '2',
30
+ position: { x: 200, y: 100 },
31
+ name: 'Node 2',
32
+ connections: ['3']
33
+ },
34
+ {
35
+ id: '3',
36
+ position: { x: 200, y: 200 },
37
+ name: 'Node 2',
38
+ connections: ['4']
39
+ },
40
+ {
41
+ id: '4',
42
+ position: { x: 100, y: 200 },
43
+ name: 'Node 2',
44
+ connections: []
45
+ }
46
+ ]
47
+ }
48
+ }
package/src/types.d.ts ADDED
@@ -0,0 +1,17 @@
1
+ /**
2
+ * 노드의 위치 정보를 표현하는 인터페이스
3
+ */
4
+ export interface Position {
5
+ x: number
6
+ y: number
7
+ }
8
+
9
+ /**
10
+ * 노드 정보를 표현하는 인터페이스
11
+ */
12
+ export interface Node {
13
+ id: string
14
+ position: Position
15
+ name?: string
16
+ connections: string[]
17
+ }
@@ -1,7 +1,9 @@
1
+ import editors from './dist/editors'
1
2
  import templates from './dist/templates'
2
3
  import groups from './dist/groups'
3
4
 
4
5
  export default {
6
+ editors,
5
7
  templates,
6
8
  groups
7
9
  }