/* * Copyright © HatioLab Inc. All rights reserved. */ import { Line, Control, sceneComponent } from '@hatiolab/things-scene' import { Node, Position } from './types' var controlHandler = { currentIndex: 0, ondragstart: function (point, index, component) { component.mutatePath(null, function (path) { const nodes = component.state.nodes as Node[] const control = component.controls[index] const { from, to, id } = control as any const newId = `${Date.now()}` if (id) { const fromNode = nodes.find(node => node.id === id) const fromIndex = nodes.indexOf(fromNode!) controlHandler.currentIndex = fromIndex == 0 ? 0 : fromIndex + 1 nodes.splice(controlHandler.currentIndex, 0, { id: newId, position: point, name: '', connections: [] }) fromNode!.connections.push(newId) path.splice(controlHandler.currentIndex, 0, point) } else { const fromNode = nodes.find(node => node.id === from) controlHandler.currentIndex = nodes.indexOf(fromNode!) + 1 nodes.splice(controlHandler.currentIndex, 0, { id: newId, position: point, name: '', connections: [to] }) const connections = fromNode!.connections connections.splice(connections.indexOf(to), 1, newId) path.splice(controlHandler.currentIndex, 0, point) } }) }, ondragmove: function (point, index, component) { component.mutatePath(null, function (path) { path[controlHandler.currentIndex] = point }) }, ondragend: function (point, index, component) {} } const NATURE = { mutable: false, resizable: true, rotatable: true, properties: [ { type: 'nodes', label: 'nodes', name: 'nodes' } ], help: 'scene/component/node-path' } @sceneComponent('NodePath') export default class NodePath extends Line { static get nature() { return NATURE } get path(): Position[] { return this.state.nodes.map(node => node.position) } set path(path: Position[]) { const nodes = this.state.nodes || [] this.set( 'nodes', nodes.map((node, index) => ({ ...node, position: path[index] })) ) } contains(x: number, y: number): boolean { var path = this.path var result = false path.forEach((p, idx) => { let j = (idx + path.length + 1) % path.length let x1 = p.x let y1 = p.y let x2 = path[j].x let y2 = path[j].y if (y1 > y != y2 > y && x < ((x2 - x1) * (y - y1)) / (y2 - y1) + x1) result = !result }) return result } render(ctx: CanvasRenderingContext2D): void { // 모든 연결 렌더링 this.renderConnections(ctx) // 모든 노드 렌더링 this.renderNodes(ctx) } renderConnections(ctx: CanvasRenderingContext2D): void { const { nodes = [], lineColor = '#000', lineWidth = 1 } = this.state nodes.forEach(node => { const { x: x1, y: y1 } = node.position const connections = node.connections || [] connections.forEach(targetId => { const targetNode = this.findNodeById(targetId) if (targetNode) { const { x: x2, y: y2 } = targetNode.position ctx.beginPath() ctx.moveTo(x1, y1) ctx.lineTo(x2, y2) ctx.strokeStyle = lineColor ctx.lineWidth = lineWidth ctx.stroke() } }) }) } renderNodes(ctx: CanvasRenderingContext2D): void { const { nodes = [], fillStyle = '#fff', strokeStyle = '#000' } = this.state nodes.forEach(node => { const { x, y } = node.position ctx.beginPath() ctx.arc(x, y, 10, 0, Math.PI * 2) ctx.fillStyle = fillStyle ctx.strokeStyle = strokeStyle ctx.fill() }) } findNodeById(id: string): Node | undefined { const nodes = this.state.nodes || [] return nodes.find(node => node.id === id) } findNodeIndexById(id: string): number { const nodes = this.state.nodes || [] return nodes.findIndex(node => node.id === id) } addNode(x: number, y: number, name: string = '', type: string = 'normal'): string { const nodes = [...(this.state.nodes || [])] const nodeId = `${Date.now()}` const node: Node = { id: nodeId, position: { x, y }, name, connections: [] } nodes.push(node) this.setState('nodes', nodes) return nodeId } moveNode(index: number, position: Position): void { const nodes = [...(this.state.nodes || [])] if (nodes[index]) { nodes[index].position = position this.setState('nodes', nodes) } } connectNodes(sourceId: string, targetId: string): boolean { if (sourceId === targetId) return false const nodes = [...(this.state.nodes || [])] const sourceIndex = this.findNodeIndexById(sourceId) const targetIndex = this.findNodeIndexById(targetId) if (sourceIndex >= 0 && targetIndex >= 0) { // 이미 연결되어 있는지 확인 if (!nodes[sourceIndex].connections.includes(targetId)) { nodes[sourceIndex].connections.push(targetId) } // 양방향 연결 (선택적) // if (!nodes[targetIndex].connections.includes(sourceId)) { // nodes[targetIndex].connections.push(sourceId) // } this.setState('nodes', nodes) return true } return false } disconnectNodes(sourceId: string, targetId: string): void { const nodes = [...(this.state.nodes || [])] const sourceIndex = this.findNodeIndexById(sourceId) const targetIndex = this.findNodeIndexById(targetId) if (sourceIndex >= 0) { nodes[sourceIndex].connections = nodes[sourceIndex].connections.filter(id => id !== targetId) } // 양방향 연결 해제 (선택적) // if (targetIndex >= 0) { // nodes[targetIndex].connections = nodes[targetIndex].connections.filter(id => id !== sourceId) // } this.setState('nodes', nodes) } removeNode(nodeId: string): void { let nodes = [...(this.state.nodes || [])] // 해당 노드 제거 nodes = nodes.filter(node => node.id !== nodeId) // 다른 노드들의 연결에서도 제거 nodes.forEach(node => { node.connections = node.connections.filter(id => id !== nodeId) }) this.setState('nodes', nodes) } // 특정 노드 연결에 대한 속성 설정 setConnectionProperty(sourceId: string, targetId: string, propertyName: string, value: any): boolean { const nodes = [...(this.state.nodes || [])] const sourceIndex = this.findNodeIndexById(sourceId) if (sourceIndex >= 0 && nodes[sourceIndex].connections.includes(targetId)) { this.setState('nodes', nodes) return true } return false } get controls(): Control[] { var { nodes } = this.state var controls = [] as Control[] for (let i = 0; i < nodes.length; i++) { const { id: sourceId, position: p1, connections = [] } = nodes[i] controls.push({ x: p1.x, y: p1.y, //@ts-ignore id: sourceId, handler: controlHandler }) connections.forEach(targetId => { const targetNode = this.findNodeById(targetId) const { position: p2 } = targetNode! controls.push({ x: (p1.x + p2.x) / 2, y: (p1.y + p2.y) / 2, //@ts-ignore from: sourceId, //@ts-ignore to: targetId, handler: controlHandler }) }) } return controls } }