@knotx/plugins-guideline 0.0.1
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/LICENSE +21 -0
- package/README.en.md +294 -0
- package/README.md +294 -0
- package/dist/index.cjs +262 -0
- package/dist/index.d.cts +60 -0
- package/dist/index.d.mts +60 -0
- package/dist/index.d.ts +60 -0
- package/dist/index.js +260 -0
- package/package.json +52 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 格桑
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.en.md
ADDED
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
# @knotx/plugins-guideline
|
|
2
|
+
|
|
3
|
+
English | [中文](./README.md)
|
|
4
|
+
|
|
5
|
+
Guideline plugin that provides alignment hints for node dragging in KnotX.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @knotx/plugins-guideline
|
|
11
|
+
# or
|
|
12
|
+
pnpm add @knotx/plugins-guideline
|
|
13
|
+
# or
|
|
14
|
+
yarn add @knotx/plugins-guideline
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Overview
|
|
18
|
+
|
|
19
|
+
The Guideline plugin provides intelligent node alignment assistance for KnotX, displaying alignment guidelines in real-time during node dragging. This plugin supports multiple alignment methods, automatic snapping functionality, and delivers excellent performance and user experience.
|
|
20
|
+
|
|
21
|
+
## Features
|
|
22
|
+
|
|
23
|
+
- 🎯 **Smart Alignment Detection** - Real-time detection of alignment relationships between nodes
|
|
24
|
+
- 📏 **Multiple Alignment Types** - Support for left, right, center, top, bottom alignment
|
|
25
|
+
- 🧲 **Auto-Snap Functionality** - Automatically snap nodes to precise positions when close to alignment
|
|
26
|
+
- ⚡ **Performance Optimized** - Guidelines calculated and rendered only during dragging
|
|
27
|
+
- 🎨 **Customizable** - Support for custom colors, thresholds, and other configurations
|
|
28
|
+
- 🔄 **Reactive Updates** - Reactive state management based on RxJS
|
|
29
|
+
|
|
30
|
+
## Implementation Principle
|
|
31
|
+
|
|
32
|
+
The core implementation principles of the Guideline plugin:
|
|
33
|
+
|
|
34
|
+
1. **Event Listening**: Listens to node `draftOperation` events to capture position changes during dragging
|
|
35
|
+
2. **Distance Calculation**: Real-time calculation of distance relationships between dragged nodes and other nodes
|
|
36
|
+
3. **Alignment Detection**: Detects various alignment possibilities within the set threshold range
|
|
37
|
+
4. **Guideline Generation**: Generates corresponding vertical and horizontal guidelines based on detection results
|
|
38
|
+
5. **Auto-Snap**: When snap functionality is enabled, automatically adjusts node positions to precise alignment positions
|
|
39
|
+
6. **Render Optimization**: Renders guidelines on the foreground layer to ensure correct visual hierarchy
|
|
40
|
+
|
|
41
|
+
## Dependencies
|
|
42
|
+
|
|
43
|
+
### Core Dependencies
|
|
44
|
+
- `@knotx/core`: Provides base plugin architecture and data management
|
|
45
|
+
- `@knotx/decorators`: Provides decorator support
|
|
46
|
+
- `rxjs`: Provides reactive programming support
|
|
47
|
+
|
|
48
|
+
## API Documentation
|
|
49
|
+
|
|
50
|
+
### Main Classes
|
|
51
|
+
|
|
52
|
+
#### Guideline
|
|
53
|
+
|
|
54
|
+
The main class of the Guideline plugin, extending `BasePlugin`.
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
export class Guideline extends BasePlugin {
|
|
58
|
+
name = 'guideline' as const
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Configuration Options
|
|
63
|
+
|
|
64
|
+
#### GuidelineConfig
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
export interface GuidelineConfig {
|
|
68
|
+
/** Alignment threshold (pixels), guidelines will be displayed when nodes are within this range */
|
|
69
|
+
threshold?: number
|
|
70
|
+
/** Guideline color */
|
|
71
|
+
color?: string
|
|
72
|
+
/** Guideline width (pixels) */
|
|
73
|
+
lineWidth?: number
|
|
74
|
+
/** Whether to enable the plugin */
|
|
75
|
+
enabled?: boolean
|
|
76
|
+
/** Whether to enable auto-snap functionality */
|
|
77
|
+
enableSnap?: boolean
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Type Definitions
|
|
82
|
+
|
|
83
|
+
#### GuidelineItem
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
interface GuidelineItem {
|
|
87
|
+
/** Guideline type */
|
|
88
|
+
type: 'vertical' | 'horizontal'
|
|
89
|
+
/** Guideline position coordinate */
|
|
90
|
+
position: number
|
|
91
|
+
/** Optional label information */
|
|
92
|
+
label?: string
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Usage Examples
|
|
97
|
+
|
|
98
|
+
### Basic Usage
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
import { Engine } from '@knotx/core'
|
|
102
|
+
import { Guideline } from '@knotx/plugins-guideline'
|
|
103
|
+
|
|
104
|
+
const engine = new Engine({
|
|
105
|
+
container: { width: 800, height: 600, direction: 'horizontal' },
|
|
106
|
+
plugins: [
|
|
107
|
+
Guideline,
|
|
108
|
+
// Other plugins...
|
|
109
|
+
],
|
|
110
|
+
})
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Custom Configuration
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
const engine = new Engine({
|
|
117
|
+
plugins: [Guideline],
|
|
118
|
+
pluginConfig: {
|
|
119
|
+
guideline: {
|
|
120
|
+
threshold: 5,
|
|
121
|
+
color: '#007ACC',
|
|
122
|
+
lineWidth: 1,
|
|
123
|
+
enabled: true,
|
|
124
|
+
enableSnap: true,
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
})
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Integration with Drag Plugin
|
|
131
|
+
|
|
132
|
+
```typescript
|
|
133
|
+
import { Drag } from '@knotx/plugins-drag'
|
|
134
|
+
import { Guideline } from '@knotx/plugins-guideline'
|
|
135
|
+
|
|
136
|
+
const engine = new Engine({
|
|
137
|
+
plugins: [Drag, Guideline],
|
|
138
|
+
pluginConfig: {
|
|
139
|
+
guideline: {
|
|
140
|
+
threshold: 8,
|
|
141
|
+
color: '#FF6B6B',
|
|
142
|
+
enableSnap: true,
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
})
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Dynamic Configuration
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
const engine = new Engine({
|
|
152
|
+
plugins: [Guideline],
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
// Modify configuration at runtime
|
|
156
|
+
engine.updatePluginConfig('guideline', {
|
|
157
|
+
threshold: 10,
|
|
158
|
+
color: '#4ECDC4',
|
|
159
|
+
enableSnap: false,
|
|
160
|
+
})
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## Configuration Options
|
|
164
|
+
|
|
165
|
+
| Option | Type | Default | Description |
|
|
166
|
+
|--------|------|---------|-------------|
|
|
167
|
+
| `threshold` | `number` | `5` | Alignment threshold (pixels), guidelines will be displayed when nodes are within this range |
|
|
168
|
+
| `color` | `string` | `'#007ACC'` | Guideline color |
|
|
169
|
+
| `lineWidth` | `number` | `1` | Guideline width (pixels) |
|
|
170
|
+
| `enabled` | `boolean` | `true` | Whether to enable the plugin |
|
|
171
|
+
| `enableSnap` | `boolean` | `true` | Whether to enable auto-snap functionality |
|
|
172
|
+
|
|
173
|
+
## Alignment Types
|
|
174
|
+
|
|
175
|
+
The plugin supports the following alignment types:
|
|
176
|
+
|
|
177
|
+
### Vertical Alignment
|
|
178
|
+
- **Left Alignment** - Align node left edges
|
|
179
|
+
- **Right Alignment** - Align node right edges
|
|
180
|
+
- **Center Alignment** - Align node horizontal centers
|
|
181
|
+
- **Edge Alignment** - Align one node's edge with another node's opposite edge
|
|
182
|
+
|
|
183
|
+
### Horizontal Alignment
|
|
184
|
+
- **Top Alignment** - Align node top edges
|
|
185
|
+
- **Bottom Alignment** - Align node bottom edges
|
|
186
|
+
- **Center Alignment** - Align node vertical centers
|
|
187
|
+
- **Edge Alignment** - Align one node's edge with another node's opposite edge
|
|
188
|
+
|
|
189
|
+
## How It Works
|
|
190
|
+
|
|
191
|
+
1. Listen to node `draftOperation` events
|
|
192
|
+
2. When node position changes, calculate distances to other nodes
|
|
193
|
+
3. Generate corresponding guidelines when within threshold range
|
|
194
|
+
4. **Snap Feature**: If enabled, automatically adjust node position to the nearest alignment position
|
|
195
|
+
5. Render guidelines on the `Layer.Foreground` layer
|
|
196
|
+
6. Automatically clear guidelines when dragging ends
|
|
197
|
+
|
|
198
|
+
## Advanced Features
|
|
199
|
+
|
|
200
|
+
### Custom Guideline Styling
|
|
201
|
+
|
|
202
|
+
```typescript
|
|
203
|
+
class CustomGuidelinePlugin extends BasePlugin {
|
|
204
|
+
@OnInit
|
|
205
|
+
init() {
|
|
206
|
+
// Customize guideline rendering style through inheritance or configuration
|
|
207
|
+
const config = this.engine.getPluginConfig('guideline')
|
|
208
|
+
config.color = this.getThemeColor()
|
|
209
|
+
config.lineWidth = this.getLineWidth()
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
private getThemeColor(): string {
|
|
213
|
+
// Return different colors based on theme
|
|
214
|
+
return document.body.classList.contains('dark-theme') ? '#64FFDA' : '#007ACC'
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
private getLineWidth(): number {
|
|
218
|
+
// Adjust line width based on canvas zoom level
|
|
219
|
+
const scale = this.engine.getPluginData('canvas')?.transform?.scale || 1
|
|
220
|
+
return Math.max(1, Math.round(2 / scale))
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### Performance Optimization Configuration
|
|
226
|
+
|
|
227
|
+
```typescript
|
|
228
|
+
const engine = new Engine({
|
|
229
|
+
plugins: [Guideline],
|
|
230
|
+
pluginConfig: {
|
|
231
|
+
guideline: {
|
|
232
|
+
// Increase threshold appropriately to reduce computation in large node scenarios
|
|
233
|
+
threshold: 10,
|
|
234
|
+
// Disable auto-snap to improve performance
|
|
235
|
+
enableSnap: false,
|
|
236
|
+
},
|
|
237
|
+
},
|
|
238
|
+
})
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
## File Directory Structure
|
|
242
|
+
|
|
243
|
+
```
|
|
244
|
+
packages/plugins-guideline/
|
|
245
|
+
├── src/
|
|
246
|
+
│ ├── index.ts # Export file
|
|
247
|
+
│ └── plugin.tsx # Main implementation file
|
|
248
|
+
├── dist/ # Build output directory
|
|
249
|
+
├── package.json # Package configuration
|
|
250
|
+
├── build.config.ts # Build configuration
|
|
251
|
+
├── tsconfig.json # TypeScript configuration
|
|
252
|
+
├── eslint.config.mjs # ESLint configuration
|
|
253
|
+
└── CHANGELOG.md # Changelog
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
### Core Files Description
|
|
257
|
+
|
|
258
|
+
- **plugin.tsx**: Contains the main implementation of the Guideline plugin, including alignment detection algorithms, guideline rendering, and auto-snap functionality
|
|
259
|
+
- **index.ts**: Exports the Guideline class and related type definitions
|
|
260
|
+
|
|
261
|
+
## Best Practices
|
|
262
|
+
|
|
263
|
+
### Performance Optimization
|
|
264
|
+
|
|
265
|
+
1. **Set Reasonable Threshold**: Adjust the `threshold` value based on usage scenarios; too small thresholds increase computational burden
|
|
266
|
+
2. **Enable Snap As Needed**: Enable `enableSnap` in scenarios where precise positioning is important, otherwise disable it to improve performance
|
|
267
|
+
3. **Consider Node Count**: In scenarios with many nodes, consider appropriately increasing the threshold or optimizing rendering strategy
|
|
268
|
+
|
|
269
|
+
### User Experience
|
|
270
|
+
|
|
271
|
+
1. **Visual Consistency**: Ensure guideline colors are consistent with the overall design style
|
|
272
|
+
2. **Responsive Configuration**: Dynamically adjust line width and threshold based on canvas zoom level
|
|
273
|
+
3. **Progressive Enhancement**: Consider disabling or simplifying guideline functionality on mobile devices
|
|
274
|
+
|
|
275
|
+
### Integration Recommendations
|
|
276
|
+
|
|
277
|
+
1. **Pair with Drag Plugin**: Recommended to use with `@knotx/plugins-drag` plugin for the best experience
|
|
278
|
+
2. **Theme Adaptation**: Dynamically adjust guideline colors when supporting light/dark theme switching
|
|
279
|
+
3. **Keyboard Support**: Consider adding keyboard shortcuts to temporarily disable/enable guideline functionality
|
|
280
|
+
|
|
281
|
+
## Examples
|
|
282
|
+
|
|
283
|
+
Check the example code in `apps/playground/src/examples/basic/guideline.tsx` to learn how to use the guideline plugin.
|
|
284
|
+
|
|
285
|
+
## Notes
|
|
286
|
+
|
|
287
|
+
1. **Performance Considerations**: Frequent alignment calculations in scenarios with many nodes may impact performance
|
|
288
|
+
2. **Event Priority**: Ensure correct event handling order with other interaction plugins
|
|
289
|
+
3. **Memory Management**: The plugin automatically cleans up unneeded guidelines; no manual management required
|
|
290
|
+
4. **Browser Compatibility**: Uses modern browser features; ensure target browsers support them
|
|
291
|
+
|
|
292
|
+
## License
|
|
293
|
+
|
|
294
|
+
MIT © boenfu
|
package/README.md
ADDED
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
# @knotx/plugins-guideline
|
|
2
|
+
|
|
3
|
+
[English](./README.en.md) | 中文
|
|
4
|
+
|
|
5
|
+
辅助线插件,为 KnotX 提供节点拖拽时的对齐提示功能。
|
|
6
|
+
|
|
7
|
+
## 安装
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @knotx/plugins-guideline
|
|
11
|
+
# or
|
|
12
|
+
pnpm add @knotx/plugins-guideline
|
|
13
|
+
# or
|
|
14
|
+
yarn add @knotx/plugins-guideline
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## 概述
|
|
18
|
+
|
|
19
|
+
Guideline 插件为 KnotX 提供了智能的节点对齐辅助功能,在节点拖拽过程中实时检测并显示对齐辅助线。该插件支持多种对齐方式、自动吸附功能,并提供了优秀的性能和用户体验。
|
|
20
|
+
|
|
21
|
+
## 功能特性
|
|
22
|
+
|
|
23
|
+
- 🎯 **智能对齐检测** - 实时检测节点之间的对齐关系
|
|
24
|
+
- 📏 **多种对齐方式** - 支持左、右、中心、顶部、底部对齐
|
|
25
|
+
- 🧲 **自动吸附功能** - 节点接近对齐位置时自动吸附到精确位置
|
|
26
|
+
- ⚡ **性能优化** - 仅在拖拽时计算和渲染辅助线
|
|
27
|
+
- 🎨 **可自定义** - 支持自定义颜色、阈值等配置
|
|
28
|
+
- 🔄 **响应式更新** - 基于 RxJS 实现响应式的状态管理
|
|
29
|
+
|
|
30
|
+
## 实现原理
|
|
31
|
+
|
|
32
|
+
Guideline 插件的核心实现原理:
|
|
33
|
+
|
|
34
|
+
1. **事件监听**:监听节点的 `draftOperation` 事件,获取拖拽中的节点位置变化
|
|
35
|
+
2. **距离计算**:实时计算拖拽节点与其他节点之间的距离关系
|
|
36
|
+
3. **对齐检测**:在设定的阈值范围内检测各种对齐可能性
|
|
37
|
+
4. **辅助线生成**:根据检测结果生成相应的垂直和水平辅助线
|
|
38
|
+
5. **自动吸附**:当启用 snap 功能时,自动调整节点位置到精确的对齐位置
|
|
39
|
+
6. **渲染优化**:在前景层渲染辅助线,确保视觉层级正确
|
|
40
|
+
|
|
41
|
+
## 依赖关系
|
|
42
|
+
|
|
43
|
+
### 核心依赖
|
|
44
|
+
- `@knotx/core`:提供基础插件架构和数据管理
|
|
45
|
+
- `@knotx/decorators`:提供装饰器支持
|
|
46
|
+
- `rxjs`:提供响应式编程支持
|
|
47
|
+
|
|
48
|
+
## API 文档
|
|
49
|
+
|
|
50
|
+
### 主要类
|
|
51
|
+
|
|
52
|
+
#### Guideline
|
|
53
|
+
|
|
54
|
+
辅助线插件的主要类,继承自 `BasePlugin`。
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
export class Guideline extends BasePlugin {
|
|
58
|
+
name = 'guideline' as const
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### 配置选项
|
|
63
|
+
|
|
64
|
+
#### GuidelineConfig
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
export interface GuidelineConfig {
|
|
68
|
+
/** 对齐阈值(像素),节点在此范围内将显示辅助线 */
|
|
69
|
+
threshold?: number
|
|
70
|
+
/** 辅助线颜色 */
|
|
71
|
+
color?: string
|
|
72
|
+
/** 辅助线宽度(像素) */
|
|
73
|
+
lineWidth?: number
|
|
74
|
+
/** 是否启用插件 */
|
|
75
|
+
enabled?: boolean
|
|
76
|
+
/** 是否启用自动吸附功能 */
|
|
77
|
+
enableSnap?: boolean
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### 类型定义
|
|
82
|
+
|
|
83
|
+
#### GuidelineItem
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
interface GuidelineItem {
|
|
87
|
+
/** 辅助线类型 */
|
|
88
|
+
type: 'vertical' | 'horizontal'
|
|
89
|
+
/** 辅助线位置坐标 */
|
|
90
|
+
position: number
|
|
91
|
+
/** 可选的标签信息 */
|
|
92
|
+
label?: string
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## 使用示例
|
|
97
|
+
|
|
98
|
+
### 基本用法
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
import { Engine } from '@knotx/core'
|
|
102
|
+
import { Guideline } from '@knotx/plugins-guideline'
|
|
103
|
+
|
|
104
|
+
const engine = new Engine({
|
|
105
|
+
container: { width: 800, height: 600, direction: 'horizontal' },
|
|
106
|
+
plugins: [
|
|
107
|
+
Guideline,
|
|
108
|
+
// 其他插件...
|
|
109
|
+
],
|
|
110
|
+
})
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### 自定义配置
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
const engine = new Engine({
|
|
117
|
+
plugins: [Guideline],
|
|
118
|
+
pluginConfig: {
|
|
119
|
+
guideline: {
|
|
120
|
+
threshold: 5,
|
|
121
|
+
color: '#007ACC',
|
|
122
|
+
lineWidth: 1,
|
|
123
|
+
enabled: true,
|
|
124
|
+
enableSnap: true,
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
})
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### 与拖拽插件配合使用
|
|
131
|
+
|
|
132
|
+
```typescript
|
|
133
|
+
import { Drag } from '@knotx/plugins-drag'
|
|
134
|
+
import { Guideline } from '@knotx/plugins-guideline'
|
|
135
|
+
|
|
136
|
+
const engine = new Engine({
|
|
137
|
+
plugins: [Drag, Guideline],
|
|
138
|
+
pluginConfig: {
|
|
139
|
+
guideline: {
|
|
140
|
+
threshold: 8,
|
|
141
|
+
color: '#FF6B6B',
|
|
142
|
+
enableSnap: true,
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
})
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### 动态配置
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
const engine = new Engine({
|
|
152
|
+
plugins: [Guideline],
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
// 运行时修改配置
|
|
156
|
+
engine.updatePluginConfig('guideline', {
|
|
157
|
+
threshold: 10,
|
|
158
|
+
color: '#4ECDC4',
|
|
159
|
+
enableSnap: false,
|
|
160
|
+
})
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## 配置选项
|
|
164
|
+
|
|
165
|
+
| 选项 | 类型 | 默认值 | 描述 |
|
|
166
|
+
|------|------|--------|------|
|
|
167
|
+
| `threshold` | `number` | `5` | 对齐阈值(像素),节点在此范围内将显示辅助线 |
|
|
168
|
+
| `color` | `string` | `'#007ACC'` | 辅助线颜色 |
|
|
169
|
+
| `lineWidth` | `number` | `1` | 辅助线宽度(像素) |
|
|
170
|
+
| `enabled` | `boolean` | `true` | 是否启用插件 |
|
|
171
|
+
| `enableSnap` | `boolean` | `true` | 是否启用自动吸附功能 |
|
|
172
|
+
|
|
173
|
+
## 对齐类型
|
|
174
|
+
|
|
175
|
+
插件支持以下对齐类型:
|
|
176
|
+
|
|
177
|
+
### 垂直对齐
|
|
178
|
+
- **左边对齐** - 节点左边缘对齐
|
|
179
|
+
- **右边对齐** - 节点右边缘对齐
|
|
180
|
+
- **中心对齐** - 节点水平中心对齐
|
|
181
|
+
- **边缘对齐** - 一个节点的边缘与另一个节点的相对边缘对齐
|
|
182
|
+
|
|
183
|
+
### 水平对齐
|
|
184
|
+
- **顶部对齐** - 节点顶部对齐
|
|
185
|
+
- **底部对齐** - 节点底部对齐
|
|
186
|
+
- **中心对齐** - 节点垂直中心对齐
|
|
187
|
+
- **边缘对齐** - 一个节点的边缘与另一个节点的相对边缘对齐
|
|
188
|
+
|
|
189
|
+
## 工作原理
|
|
190
|
+
|
|
191
|
+
1. 监听节点的 `draftOperation` 事件
|
|
192
|
+
2. 当节点位置发生变化时,计算与其他节点的距离
|
|
193
|
+
3. 在阈值范围内时,生成相应的辅助线
|
|
194
|
+
4. **Snap功能**:如果启用,自动将节点位置调整到最近的对齐位置
|
|
195
|
+
5. 在 `Layer.Foreground` 层渲染辅助线
|
|
196
|
+
6. 拖拽结束时自动清除辅助线
|
|
197
|
+
|
|
198
|
+
## 高级功能
|
|
199
|
+
|
|
200
|
+
### 自定义辅助线样式
|
|
201
|
+
|
|
202
|
+
```typescript
|
|
203
|
+
class CustomGuidelinePlugin extends BasePlugin {
|
|
204
|
+
@OnInit
|
|
205
|
+
init() {
|
|
206
|
+
// 可以通过继承或配置来自定义辅助线的渲染样式
|
|
207
|
+
const config = this.engine.getPluginConfig('guideline')
|
|
208
|
+
config.color = this.getThemeColor()
|
|
209
|
+
config.lineWidth = this.getLineWidth()
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
private getThemeColor(): string {
|
|
213
|
+
// 根据主题返回不同的颜色
|
|
214
|
+
return document.body.classList.contains('dark-theme') ? '#64FFDA' : '#007ACC'
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
private getLineWidth(): number {
|
|
218
|
+
// 根据画布缩放级别调整线宽
|
|
219
|
+
const scale = this.engine.getPluginData('canvas')?.transform?.scale || 1
|
|
220
|
+
return Math.max(1, Math.round(2 / scale))
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### 性能优化配置
|
|
226
|
+
|
|
227
|
+
```typescript
|
|
228
|
+
const engine = new Engine({
|
|
229
|
+
plugins: [Guideline],
|
|
230
|
+
pluginConfig: {
|
|
231
|
+
guideline: {
|
|
232
|
+
// 在大量节点场景下,适当增加阈值以减少计算
|
|
233
|
+
threshold: 10,
|
|
234
|
+
// 禁用自动吸附以提升性能
|
|
235
|
+
enableSnap: false,
|
|
236
|
+
},
|
|
237
|
+
},
|
|
238
|
+
})
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
## 文件目录结构
|
|
242
|
+
|
|
243
|
+
```
|
|
244
|
+
packages/plugins-guideline/
|
|
245
|
+
├── src/
|
|
246
|
+
│ ├── index.ts # 导出文件
|
|
247
|
+
│ └── plugin.tsx # 主要实现文件
|
|
248
|
+
├── dist/ # 构建输出目录
|
|
249
|
+
├── package.json # 包配置文件
|
|
250
|
+
├── build.config.ts # 构建配置
|
|
251
|
+
├── tsconfig.json # TypeScript 配置
|
|
252
|
+
├── eslint.config.mjs # ESLint 配置
|
|
253
|
+
└── CHANGELOG.md # 更新日志
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
### 核心文件说明
|
|
257
|
+
|
|
258
|
+
- **plugin.tsx**:包含 Guideline 插件的主要实现,包括对齐检测算法、辅助线渲染和自动吸附功能
|
|
259
|
+
- **index.ts**:导出 Guideline 类和相关类型定义
|
|
260
|
+
|
|
261
|
+
## 最佳实践
|
|
262
|
+
|
|
263
|
+
### 性能优化
|
|
264
|
+
|
|
265
|
+
1. **合理设置阈值**:根据使用场景调整 `threshold` 值,过小的阈值会增加计算负担
|
|
266
|
+
2. **按需启用吸附**:在精确定位重要的场景启用 `enableSnap`,否则可以禁用以提升性能
|
|
267
|
+
3. **节点数量考虑**:在节点数量较多的场景下,考虑适当增加阈值或优化渲染策略
|
|
268
|
+
|
|
269
|
+
### 用户体验
|
|
270
|
+
|
|
271
|
+
1. **视觉一致性**:确保辅助线颜色与整体设计风格保持一致
|
|
272
|
+
2. **响应性配置**:根据画布缩放级别动态调整线宽和阈值
|
|
273
|
+
3. **渐进式增强**:在移动设备上可以考虑禁用或简化辅助线功能
|
|
274
|
+
|
|
275
|
+
### 集成建议
|
|
276
|
+
|
|
277
|
+
1. **与拖拽插件配合**:建议与 `@knotx/plugins-drag` 插件一起使用以获得最佳体验
|
|
278
|
+
2. **主题适配**:支持明暗主题切换时,动态调整辅助线颜色
|
|
279
|
+
3. **快捷键支持**:可以考虑添加快捷键来临时禁用/启用辅助线功能
|
|
280
|
+
|
|
281
|
+
## 示例
|
|
282
|
+
|
|
283
|
+
查看 `apps/playground/src/examples/basic/guideline.tsx` 中的示例代码,了解如何使用辅助线插件。
|
|
284
|
+
|
|
285
|
+
## 注意事项
|
|
286
|
+
|
|
287
|
+
1. **性能考虑**:在大量节点的场景下,频繁的对齐计算可能影响性能
|
|
288
|
+
2. **事件优先级**:确保与其他交互插件的事件处理顺序正确
|
|
289
|
+
3. **内存管理**:插件会自动清理不再需要的辅助线,无需手动管理
|
|
290
|
+
4. **浏览器兼容性**:使用了现代浏览器特性,确保目标浏览器支持
|
|
291
|
+
|
|
292
|
+
## 许可证
|
|
293
|
+
|
|
294
|
+
MIT © boenfu
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const jsxRuntime = require('@knotx/jsx/jsx-runtime');
|
|
4
|
+
const core = require('@knotx/core');
|
|
5
|
+
const decorators = require('@knotx/decorators');
|
|
6
|
+
const operators = require('rxjs/operators');
|
|
7
|
+
|
|
8
|
+
var __create = Object.create;
|
|
9
|
+
var __defProp = Object.defineProperty;
|
|
10
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
11
|
+
var __getOwnPropSymbols = Object.getOwnPropertySymbols;
|
|
12
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
13
|
+
var __propIsEnum = Object.prototype.propertyIsEnumerable;
|
|
14
|
+
var __knownSymbol = (name, symbol) => (symbol = Symbol[name]) ? symbol : Symbol.for("Symbol." + name);
|
|
15
|
+
var __typeError = (msg) => {
|
|
16
|
+
throw TypeError(msg);
|
|
17
|
+
};
|
|
18
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
19
|
+
var __spreadValues = (a, b) => {
|
|
20
|
+
for (var prop in b || (b = {}))
|
|
21
|
+
if (__hasOwnProp.call(b, prop))
|
|
22
|
+
__defNormalProp(a, prop, b[prop]);
|
|
23
|
+
if (__getOwnPropSymbols)
|
|
24
|
+
for (var prop of __getOwnPropSymbols(b)) {
|
|
25
|
+
if (__propIsEnum.call(b, prop))
|
|
26
|
+
__defNormalProp(a, prop, b[prop]);
|
|
27
|
+
}
|
|
28
|
+
return a;
|
|
29
|
+
};
|
|
30
|
+
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
31
|
+
var __decoratorStart = (base) => {
|
|
32
|
+
var _a2;
|
|
33
|
+
return [, , , __create((_a2 = base == null ? void 0 : base[__knownSymbol("metadata")]) != null ? _a2 : null)];
|
|
34
|
+
};
|
|
35
|
+
var __decoratorStrings = ["class", "method", "getter", "setter", "accessor", "field", "value", "get", "set"];
|
|
36
|
+
var __expectFn = (fn) => fn !== void 0 && typeof fn !== "function" ? __typeError("Function expected") : fn;
|
|
37
|
+
var __decoratorContext = (kind, name, done, metadata, fns) => ({ kind: __decoratorStrings[kind], name, metadata, addInitializer: (fn) => done._ ? __typeError("Already initialized") : fns.push(__expectFn(fn || null)) });
|
|
38
|
+
var __decoratorMetadata = (array, target) => __defNormalProp(target, __knownSymbol("metadata"), array[3]);
|
|
39
|
+
var __runInitializers = (array, flags, self, value) => {
|
|
40
|
+
for (var i = 0, fns = array[flags >> 1], n = fns && fns.length; i < n; i++) flags & 1 ? fns[i].call(self) : value = fns[i].call(self, value);
|
|
41
|
+
return value;
|
|
42
|
+
};
|
|
43
|
+
var __decorateElement = (array, flags, name, decorators, target, extra) => {
|
|
44
|
+
var fn, it, done, ctx, access, k = flags & 7, s = !!(flags & 8), p = !!(flags & 16);
|
|
45
|
+
var j = k > 3 ? array.length + 1 : k ? s ? 1 : 2 : 0, key = __decoratorStrings[k + 5];
|
|
46
|
+
var initializers = k > 3 && (array[j - 1] = []), extraInitializers = array[j] || (array[j] = []);
|
|
47
|
+
var desc = k && (!p && !s && (target = target.prototype), k < 5 && (k > 3 || !p) && __getOwnPropDesc(k < 4 ? target : { get [name]() {
|
|
48
|
+
return __privateGet(this, extra);
|
|
49
|
+
}, set [name](x) {
|
|
50
|
+
return __privateSet(this, extra, x);
|
|
51
|
+
} }, name));
|
|
52
|
+
k ? p && k < 4 && __name(extra, (k > 2 ? "set " : k > 1 ? "get " : "") + name) : __name(target, name);
|
|
53
|
+
for (var i = decorators.length - 1; i >= 0; i--) {
|
|
54
|
+
ctx = __decoratorContext(k, name, done = {}, array[3], extraInitializers);
|
|
55
|
+
if (k) {
|
|
56
|
+
ctx.static = s, ctx.private = p, access = ctx.access = { has: p ? (x) => __privateIn(target, x) : (x) => name in x };
|
|
57
|
+
if (k ^ 3) access.get = p ? (x) => (k ^ 1 ? __privateGet : __privateMethod)(x, target, k ^ 4 ? extra : desc.get) : (x) => x[name];
|
|
58
|
+
if (k > 2) access.set = p ? (x, y) => __privateSet(x, target, y, k ^ 4 ? extra : desc.set) : (x, y) => x[name] = y;
|
|
59
|
+
}
|
|
60
|
+
it = (0, decorators[i])(k ? k < 4 ? p ? extra : desc[key] : k > 4 ? void 0 : { get: desc.get, set: desc.set } : target, ctx), done._ = 1;
|
|
61
|
+
if (k ^ 4 || it === void 0) __expectFn(it) && (k > 4 ? initializers.unshift(it) : k ? p ? extra = it : desc[key] = it : target = it);
|
|
62
|
+
else if (typeof it !== "object" || it === null) __typeError("Object expected");
|
|
63
|
+
else __expectFn(fn = it.get) && (desc.get = fn), __expectFn(fn = it.set) && (desc.set = fn), __expectFn(fn = it.init) && initializers.unshift(fn);
|
|
64
|
+
}
|
|
65
|
+
return k || __decoratorMetadata(array, target), desc && __defProp(target, name, desc), p ? k ^ 4 ? extra : desc : target;
|
|
66
|
+
};
|
|
67
|
+
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
68
|
+
var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot " + msg);
|
|
69
|
+
var __privateIn = (member, obj) => Object(obj) !== obj ? __typeError('Cannot use the "in" operator on this value') : member.has(obj);
|
|
70
|
+
var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj));
|
|
71
|
+
var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), setter ? setter.call(obj, value) : member.set(obj, value), value);
|
|
72
|
+
var __privateMethod = (obj, member, method) => (__accessCheck(obj, member, "access private method"), method);
|
|
73
|
+
var _render_dec, _init_dec, _draggingNodeId_dec, _guidelines_dec, _transform_dec, _addNodePipe_dec, _getNode_dec, _container_dec, _nodes_dec, _a, _init;
|
|
74
|
+
class Guideline extends (_a = core.BasePlugin, _nodes_dec = [decorators.inject.nodes()], _container_dec = [decorators.inject.container()], _getNode_dec = [decorators.inject.getNode()], _addNodePipe_dec = [decorators.inject.addNodePipe()], _transform_dec = [decorators.inject.canvas.transform()], _guidelines_dec = [decorators.observable()], _draggingNodeId_dec = [decorators.observable()], _init_dec = [decorators.OnInit], _render_dec = [decorators.layer(core.Layer.Foreground)], _a) {
|
|
75
|
+
constructor() {
|
|
76
|
+
super(...arguments);
|
|
77
|
+
__runInitializers(_init, 5, this);
|
|
78
|
+
__publicField(this, "name", "guideline");
|
|
79
|
+
__publicField(this, "config", {
|
|
80
|
+
threshold: 4,
|
|
81
|
+
color: "#007ACC",
|
|
82
|
+
lineWidth: 1,
|
|
83
|
+
enabled: true,
|
|
84
|
+
enableSnap: true
|
|
85
|
+
});
|
|
86
|
+
__publicField(this, "nodes", __runInitializers(_init, 8, this, [])), __runInitializers(_init, 11, this);
|
|
87
|
+
__publicField(this, "container", __runInitializers(_init, 12, this)), __runInitializers(_init, 15, this);
|
|
88
|
+
__publicField(this, "getNode", __runInitializers(_init, 16, this)), __runInitializers(_init, 19, this);
|
|
89
|
+
__publicField(this, "addNodePipe", __runInitializers(_init, 20, this)), __runInitializers(_init, 23, this);
|
|
90
|
+
__publicField(this, "transform", __runInitializers(_init, 24, this)), __runInitializers(_init, 27, this);
|
|
91
|
+
__publicField(this, "guidelines", __runInitializers(_init, 28, this, [])), __runInitializers(_init, 31, this);
|
|
92
|
+
__publicField(this, "draggingNodeId", __runInitializers(_init, 32, this, null)), __runInitializers(_init, 35, this);
|
|
93
|
+
}
|
|
94
|
+
init(config = {}) {
|
|
95
|
+
this.config = __spreadValues(__spreadValues({}, this.config), config);
|
|
96
|
+
this.subscriptions.push(this.addNodePipe({
|
|
97
|
+
transform: () => (operation$) => {
|
|
98
|
+
return operation$.pipe(
|
|
99
|
+
operators.tap((operation) => {
|
|
100
|
+
if (core.isDraftOperation(operation) && operation.type === "draftOperation") {
|
|
101
|
+
this.handleDraftOperation(operation);
|
|
102
|
+
} else if (operation.type === "commitDraft" || operation.type === "discardDraft") {
|
|
103
|
+
this.clearGuidelines();
|
|
104
|
+
}
|
|
105
|
+
})
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
}));
|
|
109
|
+
}
|
|
110
|
+
handleDraftOperation(operation) {
|
|
111
|
+
if (!this.config.enabled) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (operation.operation.type === "update" && operation.operation.data.position) {
|
|
115
|
+
const nodeId = operation.operation.id;
|
|
116
|
+
const newPosition = operation.operation.data.position;
|
|
117
|
+
this.draggingNodeId = nodeId;
|
|
118
|
+
const snapResult = this.calculateGuidelines(nodeId, newPosition);
|
|
119
|
+
if (this.config.enableSnap && snapResult.snapPosition) {
|
|
120
|
+
if (snapResult.snapPosition.x !== void 0) {
|
|
121
|
+
operation.operation.data.position.x = snapResult.snapPosition.x;
|
|
122
|
+
}
|
|
123
|
+
if (snapResult.snapPosition.y !== void 0) {
|
|
124
|
+
operation.operation.data.position.y = snapResult.snapPosition.y;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
calculateGuidelines(draggingNodeId, newPosition) {
|
|
130
|
+
var _a2, _b, _c;
|
|
131
|
+
const guidelines = [];
|
|
132
|
+
const threshold = (_a2 = this.config.threshold) != null ? _a2 : 5;
|
|
133
|
+
const snapPosition = {};
|
|
134
|
+
const draggingNode = this.getNode({ id: draggingNodeId });
|
|
135
|
+
if (!draggingNode)
|
|
136
|
+
return { guidelines: [] };
|
|
137
|
+
const draggingWidth = ((_b = draggingNode.measured) == null ? void 0 : _b.width) || 0;
|
|
138
|
+
const draggingHeight = ((_c = draggingNode.measured) == null ? void 0 : _c.height) || 0;
|
|
139
|
+
const draggingLeft = newPosition.x;
|
|
140
|
+
const draggingRight = newPosition.x + draggingWidth;
|
|
141
|
+
const draggingCenterX = newPosition.x + draggingWidth / 2;
|
|
142
|
+
const draggingTop = newPosition.y;
|
|
143
|
+
const draggingBottom = newPosition.y + draggingHeight;
|
|
144
|
+
const draggingCenterY = newPosition.y + draggingHeight / 2;
|
|
145
|
+
let nearestXSnap;
|
|
146
|
+
let nearestYSnap;
|
|
147
|
+
this.nodes.forEach((node) => {
|
|
148
|
+
var _a3, _b2, _c2, _d;
|
|
149
|
+
if (node.id === draggingNodeId)
|
|
150
|
+
return;
|
|
151
|
+
const nodeLeft = node.position.x;
|
|
152
|
+
const nodeRight = node.position.x + (((_a3 = node.measured) == null ? void 0 : _a3.width) || 0);
|
|
153
|
+
const nodeCenterX = node.position.x + (((_b2 = node.measured) == null ? void 0 : _b2.width) || 0) / 2;
|
|
154
|
+
const nodeTop = node.position.y;
|
|
155
|
+
const nodeBottom = node.position.y + (((_c2 = node.measured) == null ? void 0 : _c2.height) || 0);
|
|
156
|
+
const nodeCenterY = node.position.y + (((_d = node.measured) == null ? void 0 : _d.height) || 0) / 2;
|
|
157
|
+
const checkVerticalSnap = (targetX, label, draggedX) => {
|
|
158
|
+
const distance = Math.abs(draggedX - targetX);
|
|
159
|
+
if (distance <= threshold) {
|
|
160
|
+
guidelines.push({
|
|
161
|
+
type: "vertical",
|
|
162
|
+
position: targetX,
|
|
163
|
+
label
|
|
164
|
+
});
|
|
165
|
+
const newX = targetX - (draggedX - newPosition.x);
|
|
166
|
+
if (!nearestXSnap || distance < nearestXSnap.distance) {
|
|
167
|
+
nearestXSnap = { distance, position: targetX, newX };
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
const checkHorizontalSnap = (targetY, label, draggedY) => {
|
|
172
|
+
const distance = Math.abs(draggedY - targetY);
|
|
173
|
+
if (distance <= threshold) {
|
|
174
|
+
guidelines.push({
|
|
175
|
+
type: "horizontal",
|
|
176
|
+
position: targetY,
|
|
177
|
+
label
|
|
178
|
+
});
|
|
179
|
+
const newY = targetY - (draggedY - newPosition.y);
|
|
180
|
+
if (!nearestYSnap || distance < nearestYSnap.distance) {
|
|
181
|
+
nearestYSnap = { distance, position: targetY, newY };
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
checkVerticalSnap(nodeLeft, "left", draggingLeft);
|
|
186
|
+
checkVerticalSnap(nodeRight, "right", draggingRight);
|
|
187
|
+
checkVerticalSnap(nodeCenterX, "center", draggingCenterX);
|
|
188
|
+
checkVerticalSnap(nodeRight, "edge", draggingLeft);
|
|
189
|
+
checkVerticalSnap(nodeLeft, "edge", draggingRight);
|
|
190
|
+
checkHorizontalSnap(nodeTop, "top", draggingTop);
|
|
191
|
+
checkHorizontalSnap(nodeBottom, "bottom", draggingBottom);
|
|
192
|
+
checkHorizontalSnap(nodeCenterY, "center", draggingCenterY);
|
|
193
|
+
checkHorizontalSnap(nodeBottom, "edge", draggingTop);
|
|
194
|
+
checkHorizontalSnap(nodeTop, "edge", draggingBottom);
|
|
195
|
+
});
|
|
196
|
+
const uniqueGuidelines = guidelines.filter((guideline, index) => {
|
|
197
|
+
return guidelines.findIndex(
|
|
198
|
+
(g) => g.type === guideline.type && Math.abs(g.position - guideline.position) < 1
|
|
199
|
+
) === index;
|
|
200
|
+
});
|
|
201
|
+
if (nearestXSnap) {
|
|
202
|
+
snapPosition.x = nearestXSnap.newX;
|
|
203
|
+
}
|
|
204
|
+
if (nearestYSnap) {
|
|
205
|
+
snapPosition.y = nearestYSnap.newY;
|
|
206
|
+
}
|
|
207
|
+
this.guidelines = uniqueGuidelines;
|
|
208
|
+
return {
|
|
209
|
+
guidelines: uniqueGuidelines,
|
|
210
|
+
snapPosition: Object.keys(snapPosition).length > 0 ? snapPosition : void 0
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
clearGuidelines() {
|
|
214
|
+
this.guidelines = [];
|
|
215
|
+
this.draggingNodeId = null;
|
|
216
|
+
}
|
|
217
|
+
render() {
|
|
218
|
+
const { guidelines, transform, config } = core.use(() => ({
|
|
219
|
+
guidelines: this.guidelines,
|
|
220
|
+
transform: this.transform,
|
|
221
|
+
config: this.config
|
|
222
|
+
}));
|
|
223
|
+
if (!config.enabled || guidelines.length === 0) {
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: core.bem("guideline", "container"), children: guidelines.map((guideline, index) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
227
|
+
"div",
|
|
228
|
+
{
|
|
229
|
+
className: core.bem("guideline", "line", guideline.type),
|
|
230
|
+
style: __spreadValues({
|
|
231
|
+
position: "absolute",
|
|
232
|
+
backgroundColor: config.color,
|
|
233
|
+
pointerEvents: "none"
|
|
234
|
+
}, guideline.type === "vertical" ? {
|
|
235
|
+
left: guideline.position * transform.scale + transform.positionX,
|
|
236
|
+
top: 0,
|
|
237
|
+
width: config.lineWidth,
|
|
238
|
+
height: this.container.height
|
|
239
|
+
} : {
|
|
240
|
+
left: 0,
|
|
241
|
+
top: guideline.position * transform.scale + transform.positionY,
|
|
242
|
+
width: this.container.width,
|
|
243
|
+
height: config.lineWidth
|
|
244
|
+
})
|
|
245
|
+
},
|
|
246
|
+
`${guideline.type}-${guideline.position}-${index}`
|
|
247
|
+
)) });
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
_init = __decoratorStart(_a);
|
|
251
|
+
__decorateElement(_init, 1, "init", _init_dec, Guideline);
|
|
252
|
+
__decorateElement(_init, 1, "render", _render_dec, Guideline);
|
|
253
|
+
__decorateElement(_init, 5, "nodes", _nodes_dec, Guideline);
|
|
254
|
+
__decorateElement(_init, 5, "container", _container_dec, Guideline);
|
|
255
|
+
__decorateElement(_init, 5, "getNode", _getNode_dec, Guideline);
|
|
256
|
+
__decorateElement(_init, 5, "addNodePipe", _addNodePipe_dec, Guideline);
|
|
257
|
+
__decorateElement(_init, 5, "transform", _transform_dec, Guideline);
|
|
258
|
+
__decorateElement(_init, 5, "guidelines", _guidelines_dec, Guideline);
|
|
259
|
+
__decorateElement(_init, 5, "draggingNodeId", _draggingNodeId_dec, Guideline);
|
|
260
|
+
__decoratorMetadata(_init, Guideline);
|
|
261
|
+
|
|
262
|
+
exports.Guideline = Guideline;
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { BasePlugin } from '@knotx/core';
|
|
2
|
+
|
|
3
|
+
interface GuidelineConfig {
|
|
4
|
+
/**
|
|
5
|
+
* 对齐阈值(像素)
|
|
6
|
+
* @default 4
|
|
7
|
+
*/
|
|
8
|
+
threshold?: number;
|
|
9
|
+
/**
|
|
10
|
+
* 辅助线颜色
|
|
11
|
+
* @default '#007ACC'
|
|
12
|
+
*/
|
|
13
|
+
color?: string;
|
|
14
|
+
/**
|
|
15
|
+
* 辅助线宽度
|
|
16
|
+
* @default 1
|
|
17
|
+
*/
|
|
18
|
+
lineWidth?: number;
|
|
19
|
+
/**
|
|
20
|
+
* 是否启用
|
|
21
|
+
* @default true
|
|
22
|
+
*/
|
|
23
|
+
enabled?: boolean;
|
|
24
|
+
/**
|
|
25
|
+
* 是否启用自动吸附
|
|
26
|
+
* @default true
|
|
27
|
+
*/
|
|
28
|
+
enableSnap?: boolean;
|
|
29
|
+
}
|
|
30
|
+
interface GuidelineItem {
|
|
31
|
+
type: 'vertical' | 'horizontal';
|
|
32
|
+
position: number;
|
|
33
|
+
label?: string;
|
|
34
|
+
}
|
|
35
|
+
declare module '@knotx/core' {
|
|
36
|
+
interface PluginData {
|
|
37
|
+
guideline: {
|
|
38
|
+
guidelines: GuidelineItem[];
|
|
39
|
+
enabled: boolean;
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
declare class Guideline extends BasePlugin<'guideline', GuidelineConfig> {
|
|
44
|
+
name: "guideline";
|
|
45
|
+
private config;
|
|
46
|
+
private nodes;
|
|
47
|
+
private container;
|
|
48
|
+
private getNode;
|
|
49
|
+
private addNodePipe;
|
|
50
|
+
private transform;
|
|
51
|
+
guidelines: GuidelineItem[];
|
|
52
|
+
draggingNodeId: string | null;
|
|
53
|
+
init(config?: GuidelineConfig): void;
|
|
54
|
+
private handleDraftOperation;
|
|
55
|
+
private calculateGuidelines;
|
|
56
|
+
private clearGuidelines;
|
|
57
|
+
render(): JSX.Element | null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export { Guideline, type GuidelineConfig };
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { BasePlugin } from '@knotx/core';
|
|
2
|
+
|
|
3
|
+
interface GuidelineConfig {
|
|
4
|
+
/**
|
|
5
|
+
* 对齐阈值(像素)
|
|
6
|
+
* @default 4
|
|
7
|
+
*/
|
|
8
|
+
threshold?: number;
|
|
9
|
+
/**
|
|
10
|
+
* 辅助线颜色
|
|
11
|
+
* @default '#007ACC'
|
|
12
|
+
*/
|
|
13
|
+
color?: string;
|
|
14
|
+
/**
|
|
15
|
+
* 辅助线宽度
|
|
16
|
+
* @default 1
|
|
17
|
+
*/
|
|
18
|
+
lineWidth?: number;
|
|
19
|
+
/**
|
|
20
|
+
* 是否启用
|
|
21
|
+
* @default true
|
|
22
|
+
*/
|
|
23
|
+
enabled?: boolean;
|
|
24
|
+
/**
|
|
25
|
+
* 是否启用自动吸附
|
|
26
|
+
* @default true
|
|
27
|
+
*/
|
|
28
|
+
enableSnap?: boolean;
|
|
29
|
+
}
|
|
30
|
+
interface GuidelineItem {
|
|
31
|
+
type: 'vertical' | 'horizontal';
|
|
32
|
+
position: number;
|
|
33
|
+
label?: string;
|
|
34
|
+
}
|
|
35
|
+
declare module '@knotx/core' {
|
|
36
|
+
interface PluginData {
|
|
37
|
+
guideline: {
|
|
38
|
+
guidelines: GuidelineItem[];
|
|
39
|
+
enabled: boolean;
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
declare class Guideline extends BasePlugin<'guideline', GuidelineConfig> {
|
|
44
|
+
name: "guideline";
|
|
45
|
+
private config;
|
|
46
|
+
private nodes;
|
|
47
|
+
private container;
|
|
48
|
+
private getNode;
|
|
49
|
+
private addNodePipe;
|
|
50
|
+
private transform;
|
|
51
|
+
guidelines: GuidelineItem[];
|
|
52
|
+
draggingNodeId: string | null;
|
|
53
|
+
init(config?: GuidelineConfig): void;
|
|
54
|
+
private handleDraftOperation;
|
|
55
|
+
private calculateGuidelines;
|
|
56
|
+
private clearGuidelines;
|
|
57
|
+
render(): JSX.Element | null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export { Guideline, type GuidelineConfig };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { BasePlugin } from '@knotx/core';
|
|
2
|
+
|
|
3
|
+
interface GuidelineConfig {
|
|
4
|
+
/**
|
|
5
|
+
* 对齐阈值(像素)
|
|
6
|
+
* @default 4
|
|
7
|
+
*/
|
|
8
|
+
threshold?: number;
|
|
9
|
+
/**
|
|
10
|
+
* 辅助线颜色
|
|
11
|
+
* @default '#007ACC'
|
|
12
|
+
*/
|
|
13
|
+
color?: string;
|
|
14
|
+
/**
|
|
15
|
+
* 辅助线宽度
|
|
16
|
+
* @default 1
|
|
17
|
+
*/
|
|
18
|
+
lineWidth?: number;
|
|
19
|
+
/**
|
|
20
|
+
* 是否启用
|
|
21
|
+
* @default true
|
|
22
|
+
*/
|
|
23
|
+
enabled?: boolean;
|
|
24
|
+
/**
|
|
25
|
+
* 是否启用自动吸附
|
|
26
|
+
* @default true
|
|
27
|
+
*/
|
|
28
|
+
enableSnap?: boolean;
|
|
29
|
+
}
|
|
30
|
+
interface GuidelineItem {
|
|
31
|
+
type: 'vertical' | 'horizontal';
|
|
32
|
+
position: number;
|
|
33
|
+
label?: string;
|
|
34
|
+
}
|
|
35
|
+
declare module '@knotx/core' {
|
|
36
|
+
interface PluginData {
|
|
37
|
+
guideline: {
|
|
38
|
+
guidelines: GuidelineItem[];
|
|
39
|
+
enabled: boolean;
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
declare class Guideline extends BasePlugin<'guideline', GuidelineConfig> {
|
|
44
|
+
name: "guideline";
|
|
45
|
+
private config;
|
|
46
|
+
private nodes;
|
|
47
|
+
private container;
|
|
48
|
+
private getNode;
|
|
49
|
+
private addNodePipe;
|
|
50
|
+
private transform;
|
|
51
|
+
guidelines: GuidelineItem[];
|
|
52
|
+
draggingNodeId: string | null;
|
|
53
|
+
init(config?: GuidelineConfig): void;
|
|
54
|
+
private handleDraftOperation;
|
|
55
|
+
private calculateGuidelines;
|
|
56
|
+
private clearGuidelines;
|
|
57
|
+
render(): JSX.Element | null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export { Guideline, type GuidelineConfig };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import { jsx } from '@knotx/jsx/jsx-runtime';
|
|
2
|
+
import { Layer, isDraftOperation, use, bem, BasePlugin } from '@knotx/core';
|
|
3
|
+
import { inject, observable, layer, OnInit } from '@knotx/decorators';
|
|
4
|
+
import { tap } from 'rxjs/operators';
|
|
5
|
+
|
|
6
|
+
var __create = Object.create;
|
|
7
|
+
var __defProp = Object.defineProperty;
|
|
8
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
9
|
+
var __getOwnPropSymbols = Object.getOwnPropertySymbols;
|
|
10
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
11
|
+
var __propIsEnum = Object.prototype.propertyIsEnumerable;
|
|
12
|
+
var __knownSymbol = (name, symbol) => (symbol = Symbol[name]) ? symbol : Symbol.for("Symbol." + name);
|
|
13
|
+
var __typeError = (msg) => {
|
|
14
|
+
throw TypeError(msg);
|
|
15
|
+
};
|
|
16
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
17
|
+
var __spreadValues = (a, b) => {
|
|
18
|
+
for (var prop in b || (b = {}))
|
|
19
|
+
if (__hasOwnProp.call(b, prop))
|
|
20
|
+
__defNormalProp(a, prop, b[prop]);
|
|
21
|
+
if (__getOwnPropSymbols)
|
|
22
|
+
for (var prop of __getOwnPropSymbols(b)) {
|
|
23
|
+
if (__propIsEnum.call(b, prop))
|
|
24
|
+
__defNormalProp(a, prop, b[prop]);
|
|
25
|
+
}
|
|
26
|
+
return a;
|
|
27
|
+
};
|
|
28
|
+
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
29
|
+
var __decoratorStart = (base) => {
|
|
30
|
+
var _a2;
|
|
31
|
+
return [, , , __create((_a2 = base == null ? void 0 : base[__knownSymbol("metadata")]) != null ? _a2 : null)];
|
|
32
|
+
};
|
|
33
|
+
var __decoratorStrings = ["class", "method", "getter", "setter", "accessor", "field", "value", "get", "set"];
|
|
34
|
+
var __expectFn = (fn) => fn !== void 0 && typeof fn !== "function" ? __typeError("Function expected") : fn;
|
|
35
|
+
var __decoratorContext = (kind, name, done, metadata, fns) => ({ kind: __decoratorStrings[kind], name, metadata, addInitializer: (fn) => done._ ? __typeError("Already initialized") : fns.push(__expectFn(fn || null)) });
|
|
36
|
+
var __decoratorMetadata = (array, target) => __defNormalProp(target, __knownSymbol("metadata"), array[3]);
|
|
37
|
+
var __runInitializers = (array, flags, self, value) => {
|
|
38
|
+
for (var i = 0, fns = array[flags >> 1], n = fns && fns.length; i < n; i++) flags & 1 ? fns[i].call(self) : value = fns[i].call(self, value);
|
|
39
|
+
return value;
|
|
40
|
+
};
|
|
41
|
+
var __decorateElement = (array, flags, name, decorators, target, extra) => {
|
|
42
|
+
var fn, it, done, ctx, access, k = flags & 7, s = !!(flags & 8), p = !!(flags & 16);
|
|
43
|
+
var j = k > 3 ? array.length + 1 : k ? s ? 1 : 2 : 0, key = __decoratorStrings[k + 5];
|
|
44
|
+
var initializers = k > 3 && (array[j - 1] = []), extraInitializers = array[j] || (array[j] = []);
|
|
45
|
+
var desc = k && (!p && !s && (target = target.prototype), k < 5 && (k > 3 || !p) && __getOwnPropDesc(k < 4 ? target : { get [name]() {
|
|
46
|
+
return __privateGet(this, extra);
|
|
47
|
+
}, set [name](x) {
|
|
48
|
+
return __privateSet(this, extra, x);
|
|
49
|
+
} }, name));
|
|
50
|
+
k ? p && k < 4 && __name(extra, (k > 2 ? "set " : k > 1 ? "get " : "") + name) : __name(target, name);
|
|
51
|
+
for (var i = decorators.length - 1; i >= 0; i--) {
|
|
52
|
+
ctx = __decoratorContext(k, name, done = {}, array[3], extraInitializers);
|
|
53
|
+
if (k) {
|
|
54
|
+
ctx.static = s, ctx.private = p, access = ctx.access = { has: p ? (x) => __privateIn(target, x) : (x) => name in x };
|
|
55
|
+
if (k ^ 3) access.get = p ? (x) => (k ^ 1 ? __privateGet : __privateMethod)(x, target, k ^ 4 ? extra : desc.get) : (x) => x[name];
|
|
56
|
+
if (k > 2) access.set = p ? (x, y) => __privateSet(x, target, y, k ^ 4 ? extra : desc.set) : (x, y) => x[name] = y;
|
|
57
|
+
}
|
|
58
|
+
it = (0, decorators[i])(k ? k < 4 ? p ? extra : desc[key] : k > 4 ? void 0 : { get: desc.get, set: desc.set } : target, ctx), done._ = 1;
|
|
59
|
+
if (k ^ 4 || it === void 0) __expectFn(it) && (k > 4 ? initializers.unshift(it) : k ? p ? extra = it : desc[key] = it : target = it);
|
|
60
|
+
else if (typeof it !== "object" || it === null) __typeError("Object expected");
|
|
61
|
+
else __expectFn(fn = it.get) && (desc.get = fn), __expectFn(fn = it.set) && (desc.set = fn), __expectFn(fn = it.init) && initializers.unshift(fn);
|
|
62
|
+
}
|
|
63
|
+
return k || __decoratorMetadata(array, target), desc && __defProp(target, name, desc), p ? k ^ 4 ? extra : desc : target;
|
|
64
|
+
};
|
|
65
|
+
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
66
|
+
var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot " + msg);
|
|
67
|
+
var __privateIn = (member, obj) => Object(obj) !== obj ? __typeError('Cannot use the "in" operator on this value') : member.has(obj);
|
|
68
|
+
var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj));
|
|
69
|
+
var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), setter ? setter.call(obj, value) : member.set(obj, value), value);
|
|
70
|
+
var __privateMethod = (obj, member, method) => (__accessCheck(obj, member, "access private method"), method);
|
|
71
|
+
var _render_dec, _init_dec, _draggingNodeId_dec, _guidelines_dec, _transform_dec, _addNodePipe_dec, _getNode_dec, _container_dec, _nodes_dec, _a, _init;
|
|
72
|
+
class Guideline extends (_a = BasePlugin, _nodes_dec = [inject.nodes()], _container_dec = [inject.container()], _getNode_dec = [inject.getNode()], _addNodePipe_dec = [inject.addNodePipe()], _transform_dec = [inject.canvas.transform()], _guidelines_dec = [observable()], _draggingNodeId_dec = [observable()], _init_dec = [OnInit], _render_dec = [layer(Layer.Foreground)], _a) {
|
|
73
|
+
constructor() {
|
|
74
|
+
super(...arguments);
|
|
75
|
+
__runInitializers(_init, 5, this);
|
|
76
|
+
__publicField(this, "name", "guideline");
|
|
77
|
+
__publicField(this, "config", {
|
|
78
|
+
threshold: 4,
|
|
79
|
+
color: "#007ACC",
|
|
80
|
+
lineWidth: 1,
|
|
81
|
+
enabled: true,
|
|
82
|
+
enableSnap: true
|
|
83
|
+
});
|
|
84
|
+
__publicField(this, "nodes", __runInitializers(_init, 8, this, [])), __runInitializers(_init, 11, this);
|
|
85
|
+
__publicField(this, "container", __runInitializers(_init, 12, this)), __runInitializers(_init, 15, this);
|
|
86
|
+
__publicField(this, "getNode", __runInitializers(_init, 16, this)), __runInitializers(_init, 19, this);
|
|
87
|
+
__publicField(this, "addNodePipe", __runInitializers(_init, 20, this)), __runInitializers(_init, 23, this);
|
|
88
|
+
__publicField(this, "transform", __runInitializers(_init, 24, this)), __runInitializers(_init, 27, this);
|
|
89
|
+
__publicField(this, "guidelines", __runInitializers(_init, 28, this, [])), __runInitializers(_init, 31, this);
|
|
90
|
+
__publicField(this, "draggingNodeId", __runInitializers(_init, 32, this, null)), __runInitializers(_init, 35, this);
|
|
91
|
+
}
|
|
92
|
+
init(config = {}) {
|
|
93
|
+
this.config = __spreadValues(__spreadValues({}, this.config), config);
|
|
94
|
+
this.subscriptions.push(this.addNodePipe({
|
|
95
|
+
transform: () => (operation$) => {
|
|
96
|
+
return operation$.pipe(
|
|
97
|
+
tap((operation) => {
|
|
98
|
+
if (isDraftOperation(operation) && operation.type === "draftOperation") {
|
|
99
|
+
this.handleDraftOperation(operation);
|
|
100
|
+
} else if (operation.type === "commitDraft" || operation.type === "discardDraft") {
|
|
101
|
+
this.clearGuidelines();
|
|
102
|
+
}
|
|
103
|
+
})
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
}));
|
|
107
|
+
}
|
|
108
|
+
handleDraftOperation(operation) {
|
|
109
|
+
if (!this.config.enabled) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
if (operation.operation.type === "update" && operation.operation.data.position) {
|
|
113
|
+
const nodeId = operation.operation.id;
|
|
114
|
+
const newPosition = operation.operation.data.position;
|
|
115
|
+
this.draggingNodeId = nodeId;
|
|
116
|
+
const snapResult = this.calculateGuidelines(nodeId, newPosition);
|
|
117
|
+
if (this.config.enableSnap && snapResult.snapPosition) {
|
|
118
|
+
if (snapResult.snapPosition.x !== void 0) {
|
|
119
|
+
operation.operation.data.position.x = snapResult.snapPosition.x;
|
|
120
|
+
}
|
|
121
|
+
if (snapResult.snapPosition.y !== void 0) {
|
|
122
|
+
operation.operation.data.position.y = snapResult.snapPosition.y;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
calculateGuidelines(draggingNodeId, newPosition) {
|
|
128
|
+
var _a2, _b, _c;
|
|
129
|
+
const guidelines = [];
|
|
130
|
+
const threshold = (_a2 = this.config.threshold) != null ? _a2 : 5;
|
|
131
|
+
const snapPosition = {};
|
|
132
|
+
const draggingNode = this.getNode({ id: draggingNodeId });
|
|
133
|
+
if (!draggingNode)
|
|
134
|
+
return { guidelines: [] };
|
|
135
|
+
const draggingWidth = ((_b = draggingNode.measured) == null ? void 0 : _b.width) || 0;
|
|
136
|
+
const draggingHeight = ((_c = draggingNode.measured) == null ? void 0 : _c.height) || 0;
|
|
137
|
+
const draggingLeft = newPosition.x;
|
|
138
|
+
const draggingRight = newPosition.x + draggingWidth;
|
|
139
|
+
const draggingCenterX = newPosition.x + draggingWidth / 2;
|
|
140
|
+
const draggingTop = newPosition.y;
|
|
141
|
+
const draggingBottom = newPosition.y + draggingHeight;
|
|
142
|
+
const draggingCenterY = newPosition.y + draggingHeight / 2;
|
|
143
|
+
let nearestXSnap;
|
|
144
|
+
let nearestYSnap;
|
|
145
|
+
this.nodes.forEach((node) => {
|
|
146
|
+
var _a3, _b2, _c2, _d;
|
|
147
|
+
if (node.id === draggingNodeId)
|
|
148
|
+
return;
|
|
149
|
+
const nodeLeft = node.position.x;
|
|
150
|
+
const nodeRight = node.position.x + (((_a3 = node.measured) == null ? void 0 : _a3.width) || 0);
|
|
151
|
+
const nodeCenterX = node.position.x + (((_b2 = node.measured) == null ? void 0 : _b2.width) || 0) / 2;
|
|
152
|
+
const nodeTop = node.position.y;
|
|
153
|
+
const nodeBottom = node.position.y + (((_c2 = node.measured) == null ? void 0 : _c2.height) || 0);
|
|
154
|
+
const nodeCenterY = node.position.y + (((_d = node.measured) == null ? void 0 : _d.height) || 0) / 2;
|
|
155
|
+
const checkVerticalSnap = (targetX, label, draggedX) => {
|
|
156
|
+
const distance = Math.abs(draggedX - targetX);
|
|
157
|
+
if (distance <= threshold) {
|
|
158
|
+
guidelines.push({
|
|
159
|
+
type: "vertical",
|
|
160
|
+
position: targetX,
|
|
161
|
+
label
|
|
162
|
+
});
|
|
163
|
+
const newX = targetX - (draggedX - newPosition.x);
|
|
164
|
+
if (!nearestXSnap || distance < nearestXSnap.distance) {
|
|
165
|
+
nearestXSnap = { distance, position: targetX, newX };
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
const checkHorizontalSnap = (targetY, label, draggedY) => {
|
|
170
|
+
const distance = Math.abs(draggedY - targetY);
|
|
171
|
+
if (distance <= threshold) {
|
|
172
|
+
guidelines.push({
|
|
173
|
+
type: "horizontal",
|
|
174
|
+
position: targetY,
|
|
175
|
+
label
|
|
176
|
+
});
|
|
177
|
+
const newY = targetY - (draggedY - newPosition.y);
|
|
178
|
+
if (!nearestYSnap || distance < nearestYSnap.distance) {
|
|
179
|
+
nearestYSnap = { distance, position: targetY, newY };
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
checkVerticalSnap(nodeLeft, "left", draggingLeft);
|
|
184
|
+
checkVerticalSnap(nodeRight, "right", draggingRight);
|
|
185
|
+
checkVerticalSnap(nodeCenterX, "center", draggingCenterX);
|
|
186
|
+
checkVerticalSnap(nodeRight, "edge", draggingLeft);
|
|
187
|
+
checkVerticalSnap(nodeLeft, "edge", draggingRight);
|
|
188
|
+
checkHorizontalSnap(nodeTop, "top", draggingTop);
|
|
189
|
+
checkHorizontalSnap(nodeBottom, "bottom", draggingBottom);
|
|
190
|
+
checkHorizontalSnap(nodeCenterY, "center", draggingCenterY);
|
|
191
|
+
checkHorizontalSnap(nodeBottom, "edge", draggingTop);
|
|
192
|
+
checkHorizontalSnap(nodeTop, "edge", draggingBottom);
|
|
193
|
+
});
|
|
194
|
+
const uniqueGuidelines = guidelines.filter((guideline, index) => {
|
|
195
|
+
return guidelines.findIndex(
|
|
196
|
+
(g) => g.type === guideline.type && Math.abs(g.position - guideline.position) < 1
|
|
197
|
+
) === index;
|
|
198
|
+
});
|
|
199
|
+
if (nearestXSnap) {
|
|
200
|
+
snapPosition.x = nearestXSnap.newX;
|
|
201
|
+
}
|
|
202
|
+
if (nearestYSnap) {
|
|
203
|
+
snapPosition.y = nearestYSnap.newY;
|
|
204
|
+
}
|
|
205
|
+
this.guidelines = uniqueGuidelines;
|
|
206
|
+
return {
|
|
207
|
+
guidelines: uniqueGuidelines,
|
|
208
|
+
snapPosition: Object.keys(snapPosition).length > 0 ? snapPosition : void 0
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
clearGuidelines() {
|
|
212
|
+
this.guidelines = [];
|
|
213
|
+
this.draggingNodeId = null;
|
|
214
|
+
}
|
|
215
|
+
render() {
|
|
216
|
+
const { guidelines, transform, config } = use(() => ({
|
|
217
|
+
guidelines: this.guidelines,
|
|
218
|
+
transform: this.transform,
|
|
219
|
+
config: this.config
|
|
220
|
+
}));
|
|
221
|
+
if (!config.enabled || guidelines.length === 0) {
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
return /* @__PURE__ */ jsx("div", { className: bem("guideline", "container"), children: guidelines.map((guideline, index) => /* @__PURE__ */ jsx(
|
|
225
|
+
"div",
|
|
226
|
+
{
|
|
227
|
+
className: bem("guideline", "line", guideline.type),
|
|
228
|
+
style: __spreadValues({
|
|
229
|
+
position: "absolute",
|
|
230
|
+
backgroundColor: config.color,
|
|
231
|
+
pointerEvents: "none"
|
|
232
|
+
}, guideline.type === "vertical" ? {
|
|
233
|
+
left: guideline.position * transform.scale + transform.positionX,
|
|
234
|
+
top: 0,
|
|
235
|
+
width: config.lineWidth,
|
|
236
|
+
height: this.container.height
|
|
237
|
+
} : {
|
|
238
|
+
left: 0,
|
|
239
|
+
top: guideline.position * transform.scale + transform.positionY,
|
|
240
|
+
width: this.container.width,
|
|
241
|
+
height: config.lineWidth
|
|
242
|
+
})
|
|
243
|
+
},
|
|
244
|
+
`${guideline.type}-${guideline.position}-${index}`
|
|
245
|
+
)) });
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
_init = __decoratorStart(_a);
|
|
249
|
+
__decorateElement(_init, 1, "init", _init_dec, Guideline);
|
|
250
|
+
__decorateElement(_init, 1, "render", _render_dec, Guideline);
|
|
251
|
+
__decorateElement(_init, 5, "nodes", _nodes_dec, Guideline);
|
|
252
|
+
__decorateElement(_init, 5, "container", _container_dec, Guideline);
|
|
253
|
+
__decorateElement(_init, 5, "getNode", _getNode_dec, Guideline);
|
|
254
|
+
__decorateElement(_init, 5, "addNodePipe", _addNodePipe_dec, Guideline);
|
|
255
|
+
__decorateElement(_init, 5, "transform", _transform_dec, Guideline);
|
|
256
|
+
__decorateElement(_init, 5, "guidelines", _guidelines_dec, Guideline);
|
|
257
|
+
__decorateElement(_init, 5, "draggingNodeId", _draggingNodeId_dec, Guideline);
|
|
258
|
+
__decoratorMetadata(_init, Guideline);
|
|
259
|
+
|
|
260
|
+
export { Guideline };
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@knotx/plugins-guideline",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Guideline Plugin for Knotx",
|
|
5
|
+
"author": "boenfu",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"homepage": "https://github.com/boenfu/knotx#readme",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/boenfu/knotx.git",
|
|
11
|
+
"directory": "packages/plugins-guideline"
|
|
12
|
+
},
|
|
13
|
+
"publishConfig": {
|
|
14
|
+
"access": "public"
|
|
15
|
+
},
|
|
16
|
+
"sideEffects": false,
|
|
17
|
+
"exports": {
|
|
18
|
+
".": {
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
|
+
"import": "./dist/index.js",
|
|
21
|
+
"require": "./dist/index.cjs"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"main": "./dist/index.cjs",
|
|
25
|
+
"module": "./dist/index.js",
|
|
26
|
+
"types": "./dist/index.d.ts",
|
|
27
|
+
"files": [
|
|
28
|
+
"dist"
|
|
29
|
+
],
|
|
30
|
+
"peerDependencies": {
|
|
31
|
+
"@knotx/jsx": "0.4.15",
|
|
32
|
+
"@knotx/plugins-canvas": "0.4.15"
|
|
33
|
+
},
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"rxjs": "^7.8.1",
|
|
36
|
+
"@knotx/core": "0.4.15",
|
|
37
|
+
"@knotx/decorators": "0.4.15"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@knotx/build-config": "0.4.15",
|
|
41
|
+
"@knotx/eslint-config": "0.4.15",
|
|
42
|
+
"@knotx/plugins-canvas": "0.4.15",
|
|
43
|
+
"@knotx/typescript-config": "0.4.15",
|
|
44
|
+
"@knotx/jsx": "0.4.15"
|
|
45
|
+
},
|
|
46
|
+
"scripts": {
|
|
47
|
+
"build": "unbuild",
|
|
48
|
+
"dev": "unbuild --stub",
|
|
49
|
+
"lint": "eslint .",
|
|
50
|
+
"typecheck": "tsc --noEmit"
|
|
51
|
+
}
|
|
52
|
+
}
|