@libs-ui/components-draw-line
v0.2.357-11
Published
> Directive vẽ đường nối SVG giữa các điểm với nhiều chế độ đường cong, hỗ trợ né vật cản và kéo thả mũi tên.
Downloads
2,766
Readme
@libs-ui/components-draw-line
Directive vẽ đường nối SVG giữa các điểm với nhiều chế độ đường cong, hỗ trợ né vật cản và kéo thả mũi tên.
Giới thiệu
LibsUiComponentsDrawLineDirective là một Angular standalone directive dùng để vẽ các đường nối SVG giữa hai điểm bất kỳ với nhiều chế độ vẽ khác nhau (quart-in, horizontal, vertical, horizontal-single-curve, vertical-single-curve, s-line). Directive tự động tạo và quản lý SVG element bên trong container host, hỗ trợ né vật cản (obstacle avoidance), kéo thả đầu mũi tên, vùng kết nối target (reachable point range), và tính toán viewBox tự động. Toàn bộ logic render chạy ngoài NgZone để tối ưu hiệu năng.
Tính năng
- ✅ 6 chế độ vẽ đường:
quart-in,horizontal,vertical,horizontal-single-curve,vertical-single-curve,s-line - ✅ Tự động né vật cản (obstacle avoidance) qua
obstacleRect - ✅ Kéo thả mũi tên / circle endpoint bằng chuột (drag-and-drop)
- ✅ Vùng có thể kết nối (reachable point range) với sự kiện
outConnectedkhi kết nối thành công - ✅ Tùy chỉnh style đường (stroke, strokeDasharray, strokeWidth, fill, curve, distancePoint)
- ✅ Tùy chỉnh style mũi tên (fill, stroke, width, height)
- ✅ Hiển thị circle tại điểm đầu/cuối đường (startCircle, endCircle) với style riêng
- ✅ Tự động tính toán và cập nhật viewBox cho SVG
- ✅ Hỗ trợ separatedPoints cho đường nối phức tạp nhiều đoạn
- ✅ Debug mode hiển thị obstacle rect/circle trực quan
- ✅ Chạy ngoài NgZone — tối ưu hiệu năng Angular
Khi nào sử dụng
- Vẽ đường nối giữa các node trong flowchart, diagram, workflow editor
- Hiển thị mối quan hệ trực quan giữa các phần tử trên canvas
- Cho phép người dùng kéo thả để tạo kết nối giữa các phần tử
- Vẽ đường cong SVG né tránh vật cản trên canvas
- Phù hợp cho automation builder, mind map, ERD diagram, pipeline editor, BPMN editor
Cài đặt
npm install @libs-ui/components-draw-lineImport
import {
LibsUiComponentsDrawLineDirective,
IMoDrawLineFunctionControl,
IDrawLineDataInput,
IReachablePointRange,
IViewBoxConfig,
ILineStyle,
IArrowStyle,
ICircleStyle,
IPoints,
IPoint,
IRect,
TYPE_MODE,
TYPE_DIRECTION,
} from '@libs-ui/components-draw-line';
@Component({
standalone: true,
imports: [LibsUiComponentsDrawLineDirective],
// ...
})
export class YourComponent {}Ví dụ sử dụng
1. Basic — Vẽ đường nối đơn giản (quart-in)
<!-- your-component.component.html -->
<div
LibsUiComponentsDrawLineDirective
(outDrawLineFunctionControl)="handlerDrawLineFunctionControl($event)">
</div>// your-component.component.ts
import { AfterViewInit, Component } from '@angular/core';
import {
LibsUiComponentsDrawLineDirective,
IMoDrawLineFunctionControl,
IDrawLineDataInput,
} from '@libs-ui/components-draw-line';
@Component({
selector: 'app-basic-draw-line',
standalone: true,
imports: [LibsUiComponentsDrawLineDirective],
templateUrl: './basic-draw-line.component.html',
})
export class BasicDrawLineComponent implements AfterViewInit {
private drawLineFunctionControl?: IMoDrawLineFunctionControl;
handlerDrawLineFunctionControl(ctrl: IMoDrawLineFunctionControl): void {
this.drawLineFunctionControl = ctrl;
}
ngAfterViewInit(): void {
const lines: IDrawLineDataInput[] = [
{
id: 'line-1',
mode: 'quart-in',
lineStyle: { stroke: '#3B82F6', strokeWidth: '2px' },
arrowStyle: { fill: '#3B82F6' },
points: {
start: { x: 50, y: 150 },
end: { x: 470, y: 50 },
},
},
{
id: 'line-2',
mode: 'quart-in',
lineStyle: { stroke: '#10B981', strokeWidth: '2px' },
arrowStyle: { fill: '#10B981' },
points: {
start: { x: 50, y: 150 },
end: { x: 470, y: 250 },
},
},
];
this.drawLineFunctionControl?.setData(lines);
}
}2. Horizontal / Vertical — Đường vuông góc
<!-- your-component.component.html -->
<div
LibsUiComponentsDrawLineDirective
(outDrawLineFunctionControl)="handlerDrawLineFunctionControl($event)">
</div>// your-component.component.ts
import { AfterViewInit, Component } from '@angular/core';
import {
LibsUiComponentsDrawLineDirective,
IMoDrawLineFunctionControl,
IDrawLineDataInput,
} from '@libs-ui/components-draw-line';
@Component({
selector: 'app-horizontal-draw-line',
standalone: true,
imports: [LibsUiComponentsDrawLineDirective],
templateUrl: './horizontal-draw-line.component.html',
})
export class HorizontalDrawLineComponent implements AfterViewInit {
private drawLineFunctionControl?: IMoDrawLineFunctionControl;
handlerDrawLineFunctionControl(ctrl: IMoDrawLineFunctionControl): void {
this.drawLineFunctionControl = ctrl;
}
ngAfterViewInit(): void {
this.drawLineFunctionControl?.setData([
{
id: 'h-line-1',
mode: 'horizontal',
lineStyle: {
stroke: '#8B5CF6',
strokeWidth: '2px',
curve: 10,
distancePoint: 15,
},
arrowStyle: { fill: '#8B5CF6' },
points: {
start: { x: 50, y: 60 },
end: { x: 400, y: 200 },
},
},
{
id: 'v-line-1',
mode: 'vertical',
lineStyle: {
stroke: '#F97316',
strokeWidth: '2px',
curve: 10,
distancePoint: 15,
},
arrowStyle: { fill: '#F97316' },
points: {
start: { x: 100, y: 40 },
end: { x: 300, y: 260 },
},
},
]);
}
}3. Obstacle Avoidance — Né vật cản
<!-- your-component.component.html -->
<div
LibsUiComponentsDrawLineDirective
[drawRectDebug]="true"
(outDrawLineFunctionControl)="handlerDrawLineFunctionControl($event)">
</div>// your-component.component.ts
import { AfterViewInit, Component } from '@angular/core';
import {
LibsUiComponentsDrawLineDirective,
IMoDrawLineFunctionControl,
IDrawLineDataInput,
} from '@libs-ui/components-draw-line';
@Component({
selector: 'app-obstacle-draw-line',
standalone: true,
imports: [LibsUiComponentsDrawLineDirective],
templateUrl: './obstacle-draw-line.component.html',
})
export class ObstacleDrawLineComponent implements AfterViewInit {
private drawLineFunctionControl?: IMoDrawLineFunctionControl;
handlerDrawLineFunctionControl(ctrl: IMoDrawLineFunctionControl): void {
this.drawLineFunctionControl = ctrl;
}
ngAfterViewInit(): void {
this.drawLineFunctionControl?.setData([
{
id: 'obstacle-line',
mode: 'horizontal',
lineStyle: {
stroke: '#EF4444',
strokeWidth: '2px',
curve: 10,
distancePoint: 10,
},
arrowStyle: { fill: '#EF4444' },
points: {
start: { x: 50, y: 150 },
end: { x: 450, y: 100 },
obstacleRect: [
{ id: 'box-1', x: 190, y: 70, width: 120, height: 80 },
],
},
},
]);
}
}4. Reachable Point Range — Vùng kết nối với kéo thả
<!-- your-component.component.html -->
<div
LibsUiComponentsDrawLineDirective
(outDrawLineFunctionControl)="handlerDrawLineFunctionControl($event)"
(outConnected)="handlerConnected($event)">
</div>// your-component.component.ts
import { AfterViewInit, Component } from '@angular/core';
import {
LibsUiComponentsDrawLineDirective,
IMoDrawLineFunctionControl,
IDrawLineDataInput,
IReachablePointRange,
} from '@libs-ui/components-draw-line';
@Component({
selector: 'app-reachable-draw-line',
standalone: true,
imports: [LibsUiComponentsDrawLineDirective],
templateUrl: './reachable-draw-line.component.html',
})
export class ReachableDrawLineComponent implements AfterViewInit {
private drawLineFunctionControl?: IMoDrawLineFunctionControl;
handlerDrawLineFunctionControl(ctrl: IMoDrawLineFunctionControl): void {
this.drawLineFunctionControl = ctrl;
}
ngAfterViewInit(): void {
// Đăng ký các vùng có thể kết nối (target circles)
this.drawLineFunctionControl?.setReachablePointRange([
{
id: 'target-1',
x: 400,
y: 100,
style: { r: 8, fill: '#10B981', stroke: '#059669', strokeWidth: '2px' },
},
{
id: 'target-2',
x: 400,
y: 220,
style: { r: 8, fill: '#10B981', stroke: '#059669', strokeWidth: '2px' },
},
]);
// Thêm đường nối có endCircle để kéo thả
this.drawLineFunctionControl?.setData([
{
id: 'drag-line',
mode: 'quart-in',
endCircle: true,
endCircleStyle: { r: 6, fill: '#3B82F6', stroke: '#2563EB', strokeWidth: '2px' },
ignoreDrawArrow: true,
lineStyle: { stroke: '#3B82F6', strokeWidth: '2px' },
points: {
start: { x: 80, y: 160 },
end: { x: 220, y: 160 },
},
},
]);
}
handlerConnected(event: {
dataLine: IDrawLineDataInput;
dataReachablePointRange: IReachablePointRange;
}): void {
event;
// Xử lý khi kết nối thành công
console.log('Kết nối thành công:', event.dataLine.id, '->', event.dataReachablePointRange.id);
}
}5. Đường nét đứt và Circle start/end
import { AfterViewInit, Component } from '@angular/core';
import {
LibsUiComponentsDrawLineDirective,
IMoDrawLineFunctionControl,
IDrawLineDataInput,
} from '@libs-ui/components-draw-line';
@Component({
selector: 'app-styled-draw-line',
standalone: true,
imports: [LibsUiComponentsDrawLineDirective],
template: `
<div
LibsUiComponentsDrawLineDirective
(outDrawLineFunctionControl)="handlerDrawLineFunctionControl($event)">
</div>
`,
})
export class StyledDrawLineComponent implements AfterViewInit {
private drawLineFunctionControl?: IMoDrawLineFunctionControl;
handlerDrawLineFunctionControl(ctrl: IMoDrawLineFunctionControl): void {
this.drawLineFunctionControl = ctrl;
}
ngAfterViewInit(): void {
this.drawLineFunctionControl?.setData([
// Đường nét đứt
{
id: 'dashed-line',
mode: 'quart-in',
lineStyle: {
stroke: '#6366F1',
strokeWidth: '2px',
strokeDasharray: '8 4',
},
arrowStyle: { fill: '#6366F1' },
points: { start: { x: 30, y: 80 }, end: { x: 470, y: 60 } },
},
// Đường có circle đầu/cuối, không mũi tên
{
id: 'circle-line',
mode: 'quart-in',
startCircle: true,
endCircle: true,
ignoreDrawArrow: true,
startCircleStyle: { fill: '#3B82F6', r: 6, strokeWidth: '2px' },
endCircleStyle: { fill: '#EF4444', r: 6, strokeWidth: '2px' },
lineStyle: { stroke: '#6B7280', strokeWidth: '2px' },
points: { start: { x: 30, y: 200 }, end: { x: 470, y: 200 } },
},
]);
}
}6. Xóa và cập nhật đường nối
import { Component } from '@angular/core';
import {
LibsUiComponentsDrawLineDirective,
IMoDrawLineFunctionControl,
IViewBoxConfig,
} from '@libs-ui/components-draw-line';
@Component({
selector: 'app-control-draw-line',
standalone: true,
imports: [LibsUiComponentsDrawLineDirective],
template: `
<div
LibsUiComponentsDrawLineDirective
(outDrawLineFunctionControl)="handlerDrawLineFunctionControl($event)">
</div>
<button (click)="handlerRemoveLine()">Xóa đường nối</button>
<button (click)="handlerUpdateViewBox()">Cập nhật ViewBox</button>
`,
})
export class ControlDrawLineComponent {
private drawLineFunctionControl?: IMoDrawLineFunctionControl;
handlerDrawLineFunctionControl(ctrl: IMoDrawLineFunctionControl): void {
this.drawLineFunctionControl = ctrl;
}
handlerRemoveLine(): void {
// Xóa toàn bộ đường nối theo id
this.drawLineFunctionControl?.removeLine('line-1');
// Xóa một đoạn cụ thể theo points
this.drawLineFunctionControl?.removeLine('line-2', [
{ start: { x: 50, y: 100 }, end: { x: 300, y: 200 } },
]);
}
handlerUpdateViewBox(): void {
const config: IViewBoxConfig = {
minX: 0,
minY: 0,
width: 1200,
height: 800,
};
this.drawLineFunctionControl?.updateViewBox(config);
}
}@Input()
| Input | Type | Default | Mô tả | Ví dụ |
|---|---|---|---|---|
| [drawRectDebug] | boolean | false | Hiển thị debug rect/circle cho obstacle và điểm kiểm tra trên SVG | [drawRectDebug]="true" |
| [svgElement] | SVGSVGElement | auto-created | SVG element để vẽ vào. Directive tự tạo <svg> nếu không truyền | [svgElement]="mySvgRef" |
| [viewBoxConfig] | IViewBoxConfig | undefined | Cấu hình viewBox tĩnh cho SVG. Nếu không truyền, directive tự tính toán từ tọa độ các điểm | [viewBoxConfig]="{ minX: 0, minY: 0, width: 800, height: 600 }" |
@Output()
| Output | Type | Mô tả | Handler TS | Binding HTML |
|---|---|---|---|---|
| (outConnected) | { dataLine: IDrawLineDataInput; dataReachablePointRange: IReachablePointRange } | Emit khi người dùng kéo thả mũi tên/endCircle đến một reachable point thành công | handlerConnected(e: { dataLine: IDrawLineDataInput; dataReachablePointRange: IReachablePointRange }): void { e; } | (outConnected)="handlerConnected($event)" |
| (outDrawLineFunctionControl) | IMoDrawLineFunctionControl | Emit object chứa toàn bộ methods điều khiển draw-line ngay sau ngAfterViewInit | handlerDrawLineFunctionControl(ctrl: IMoDrawLineFunctionControl): void { this.ctrl = ctrl; } | (outDrawLineFunctionControl)="handlerDrawLineFunctionControl($event)" |
FunctionsControl (IMoDrawLineFunctionControl)
Nhận object này qua (outDrawLineFunctionControl) để gọi các method điều khiển directive từ component cha.
| Method | Signature | Mô tả |
|---|---|---|
| removeLine | (id: string, points?: Array<IPoints>) => void | Xóa đường nối theo id. Nếu truyền points, chỉ xóa đoạn khớp với tọa độ. Nếu không truyền points, xóa toàn bộ đường có id đó |
| removeReachablePointRange | (ids: Array<string>) => void | Xóa các reachable point range theo danh sách id. Truyền mảng rỗng để xóa toàn bộ |
| setData | (data: Array<IDrawLineDataInput>) => void | Thêm dữ liệu đường nối để vẽ. Directive tự bỏ qua item trùng id+tọa độ (idempotent) |
| setReachablePointRange | (data: Array<IReachablePointRange>) => void | Thêm các vùng có thể kết nối (target circle). Bỏ qua item trùng id hoặc tọa độ |
| updateViewBox | (viewBoxConfig?: IViewBoxConfig) => void | Cập nhật viewBox cho SVG. Nếu không truyền tham số, tính toán lại tự động từ tọa độ điểm |
Types & Interfaces
import {
TYPE_MODE,
TYPE_DIRECTION,
IDrawLineDataInput,
IPoints,
IPoint,
IRect,
ILineStyle,
IArrowStyle,
ICircleStyle,
IViewBoxConfig,
IReachablePointRange,
IMoDrawLineFunctionControl,
} from '@libs-ui/components-draw-line';TYPE_MODE
// Chế độ vẽ đường
type TYPE_MODE =
| 'quart-in' // Đường cong cubic bezier tự nhiên (mặc định)
| 's-line' // Đường S-curve (cong hình chữ S)
| 'horizontal' // Đường vuông góc ngang đầy đủ (2 corner)
| 'horizontal-single-curve'// Đường cong đơn ngang (1 corner)
| 'vertical' // Đường vuông góc dọc đầy đủ (2 corner)
| 'vertical-single-curve'; // Đường cong đơn dọc (1 corner)TYPE_DIRECTION
// Hướng của mũi tên tại điểm cuối
type TYPE_DIRECTION = 'right' | 'left' | 'top' | 'bottom';IDrawLineDataInput
interface IDrawLineDataInput {
id: string; // ID duy nhất của đường nối (bắt buộc)
points: IPoints; // Tọa độ điểm đầu và cuối (bắt buộc)
mode: TYPE_MODE; // Chế độ vẽ (bắt buộc)
idConnected?: string; // ID reachable point đã kết nối (tự điền sau khi connect)
lineStyle?: ILineStyle; // Style cho đường nối
arrowStyle?: IArrowStyle; // Style cho mũi tên
ignoreDrawArrow?: boolean; // Bỏ qua vẽ mũi tên (dùng khi có separatedPoints)
startCircle?: boolean; // Hiển thị circle tại điểm đầu
startCircleStyle?: ICircleStyle; // Style circle điểm đầu
endCircle?: boolean; // Hiển thị circle tại điểm cuối (hỗ trợ drag)
endCircleStyle?: ICircleStyle; // Style circle điểm cuối
arrowDirection?: TYPE_DIRECTION; // Ghi đè hướng mũi tên (tự tính nếu không truyền)
startEndMode?: 'right-left' | 'bottom-top' | 'bottom-left'; // Gợi ý chiều kết nối
isRemove?: boolean; // Internal flag — không tự điền
ref?: IDrawLineDataInput; // Internal ref — không tự điền
}IPoints
interface IPoints {
start: IPoint; // Tọa độ điểm bắt đầu (bắt buộc)
end: IPoint; // Tọa độ điểm kết thúc (bắt buộc)
obstacleRect?: Array<IRect>; // Danh sách vật cản cần né tránh
separatedPoints?: Array<IPoints>; // Các đoạn con cho đường phức tạp
// Internal fields — không tự điền
initialized?: boolean;
pathElement?: SVGPathElement;
arrowElement?: SVGPolylineElement;
circleStartElement?: SVGCircleElement;
circleEndElement?: SVGCircleElement;
onDestroyEvent?: Subject<void>;
mode?: TYPE_MODE;
id?: string;
name?: string;
}IPoint
interface IPoint {
x: number; // Tọa độ X (pixel, tương đối với SVG viewBox)
y: number; // Tọa độ Y (pixel, tương đối với SVG viewBox)
}IRect
interface IRect {
id?: string; // Tên debug cho rect
x: number; // Tọa độ X góc trên trái
y: number; // Tọa độ Y góc trên trái
width: number; // Chiều rộng
height: number; // Chiều cao
gapXObstacleRect?: number; // Khoảng đệm ngang quanh obstacle (mặc định 8px)
gapYObstacleRect?: number; // Khoảng đệm dọc quanh obstacle (mặc định 8px)
}ILineStyle
interface ILineStyle {
stroke?: string; // Màu đường (CSS color, mặc định '#9CA2AD')
strokeDasharray?: string; // Nét đứt, vd: '8 4' hoặc '4 4'
strokeWidth?: string; // Độ dày đường (CSS, mặc định '1px')
fill?: string; // Fill của path (mặc định 'none')
curve?: number; // Bán kính cong cho mode horizontal/vertical (mặc định 10)
distancePoint?: number; // Khoảng cách từ điểm mút đến đường cong (mặc định 10)
}IArrowStyle
interface IArrowStyle {
stroke?: string; // Màu viền mũi tên (mặc định 'none')
strokeWidth?: string; // Độ dày viền (mặc định '0')
width?: number; // Chiều rộng mũi tên (mặc định 6)
height?: number; // Chiều cao mũi tên (mặc định 8)
fill?: string; // Màu fill mũi tên (mặc định '#9CA2AD')
}ICircleStyle
interface ICircleStyle {
stroke?: string; // Màu viền circle (mặc định '#9CA2AD')
strokeWidth?: string; // Độ dày viền (mặc định '1px')
fill?: string; // Màu fill circle (mặc định 'green')
r?: number; // Bán kính circle (mặc định 2)
}IViewBoxConfig
interface IViewBoxConfig {
minX?: number; // Giá trị minX của viewBox (mặc định: x của SVG element)
minY?: number; // Giá trị minY của viewBox (mặc định: y của SVG element)
width?: number; // Chiều rộng viewBox cố định (mặc định: tự tính từ điểm xa nhất)
height?: number; // Chiều cao viewBox cố định (mặc định: tự tính từ điểm xa nhất)
ignoreViewBox?: boolean; // Bỏ qua cập nhật thuộc tính viewBox, chỉ set width/height
marginBetweenElement?: number; // Khoảng cách giữa các element (chưa dùng trong v hiện tại)
}IReachablePointRange
interface IReachablePointRange {
id: string; // ID duy nhất của vùng kết nối (bắt buộc)
x: number; // Tọa độ X của circle target
y: number; // Tọa độ Y của circle target
style: ICircleStyle; // Style hiển thị circle target (bắt buộc)
idConnected?: string; // ID đường nối đã kết nối vào đây (tự điền sau khi connect)
// Internal fields — không tự điền
ref?: IReachablePointRange;
element?: SVGCircleElement;
onDestroyEvent?: Subject<void>;
}IMoDrawLineFunctionControl
interface IMoDrawLineFunctionControl {
setData: (data: Array<IDrawLineDataInput>) => void;
removeLine: (id: string, points?: Array<IPoints>) => void;
setReachablePointRange: (data: Array<IReachablePointRange>) => void;
removeReachablePointRange: (ids: Array<string>) => void;
updateViewBox: (viewBoxConfig?: IViewBoxConfig) => void;
}Lưu ý quan trọng
⚠️ Tọa độ SVG, không phải DOM: Tọa độ x, y trong IPoint là tọa độ trong hệ tọa độ SVG (viewBox), không phải pixel màn hình. Cần convert từ DOM coordinates sang SVG coordinates khi lấy vị trí từ getBoundingClientRect().
⚠️ setData là idempotent: Directive tự bỏ qua item đã tồn tại (cùng id + cùng tọa độ). Để cập nhật vị trí đường, phải gọi removeLine(id) trước rồi gọi setData lại.
⚠️ outDrawLineFunctionControl emit một lần duy nhất: Output này chỉ emit một lần trong ngAfterViewInit. Cần lưu lại reference vào property của component cha để tái sử dụng.
⚠️ Kéo thả chỉ hoạt động với endCircle: true hoặc có arrowElement: Drag-and-drop endpoint chỉ được khởi tạo khi đường nối có endCircle: true hoặc có mũi tên (ignoreDrawArrow không phải true).
⚠️ obstacleRect chỉ né vật cản khi kéo thả: Obstacle avoidance chỉ ngăn chặn di chuyển endpoint vào vùng rect khi kéo thả, không tự động re-route đường nối vòng qua vật cản khi render tĩnh.
⚠️ Directive gắn vào container element: LibsUiComponentsDrawLineDirective áp dụng lên element bọc ngoài và tự append <svg> vào bên trong. Element host cần có position: relative hoặc đủ kích thước để chứa SVG.
⚠️ Cleanup khi component destroy: Directive dùng takeUntilDestroyed(destroyRef) để tự cleanup tất cả subscriptions. Không cần ngOnDestroy thủ công trong component cha.
