@flowgram-vue/core 0.2.0

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.
Files changed (133) hide show
  1. package/LICENSE +22 -0
  2. package/dist/index.cjs +0 -0
  3. package/dist/index.cjs.map +1 -0
  4. package/dist/index.d.ts +1 -0
  5. package/dist/index.js +0 -0
  6. package/dist/index.js.map +1 -0
  7. package/package.json +63 -0
  8. package/src/common/config-entity.ts +80 -0
  9. package/src/common/entity-data.ts +171 -0
  10. package/src/common/entity-manager-contribution.ts +12 -0
  11. package/src/common/entity-manager.ts +475 -0
  12. package/src/common/entity.ts +482 -0
  13. package/src/common/index.ts +22 -0
  14. package/src/common/playground-context.ts +35 -0
  15. package/src/common/playground-decorator-helper.ts +113 -0
  16. package/src/common/playground-decorators.ts +85 -0
  17. package/src/common/playground-schedule.ts +30 -0
  18. package/src/common/protect-wheel-area.ts +11 -0
  19. package/src/common/schema/index.ts +14 -0
  20. package/src/common/schema/node.ts +15 -0
  21. package/src/common/schema/opacity-schema.ts +19 -0
  22. package/src/common/schema/origin-schema.ts +35 -0
  23. package/src/common/schema/position-schema.ts +35 -0
  24. package/src/common/schema/rotation-schema.ts +19 -0
  25. package/src/common/schema/scale-schema.ts +35 -0
  26. package/src/common/schema/size-schema.ts +42 -0
  27. package/src/common/schema/skew-schema.ts +35 -0
  28. package/src/common/schema/transform-schema.ts +613 -0
  29. package/src/common/utils/bounds.spec.ts +229 -0
  30. package/src/common/utils/bounds.ts +176 -0
  31. package/src/common/utils/index.ts +6 -0
  32. package/src/composables/index.ts +16 -0
  33. package/src/composables/use-config-entity.ts +31 -0
  34. package/src/composables/use-entities.ts +26 -0
  35. package/src/composables/use-entity-data-from-context.ts +34 -0
  36. package/src/composables/use-entity-from-context.ts +31 -0
  37. package/src/composables/use-listen-events.ts +22 -0
  38. package/src/composables/use-playground-container.ts +16 -0
  39. package/src/composables/use-playground-context.ts +15 -0
  40. package/src/composables/use-playground-drag.ts +31 -0
  41. package/src/composables/use-playground.ts +16 -0
  42. package/src/composables/use-refresh.ts +6 -0
  43. package/src/composables/use-service.ts +17 -0
  44. package/src/core/index.ts +8 -0
  45. package/src/core/layer/config/editor-state-config-entity.ts +180 -0
  46. package/src/core/layer/config/index.ts +7 -0
  47. package/src/core/layer/config/playground-config-entity.ts +604 -0
  48. package/src/core/layer/index.ts +8 -0
  49. package/src/core/layer/layer.ts +248 -0
  50. package/src/core/layer/playground-layer.ts +547 -0
  51. package/src/core/pipeline/index.ts +10 -0
  52. package/src/core/pipeline/pipeline-entities-selector.ts +200 -0
  53. package/src/core/pipeline/pipeline-entities.ts +230 -0
  54. package/src/core/pipeline/pipeline-registry.ts +344 -0
  55. package/src/core/pipeline/pipeline-renderer.ts +248 -0
  56. package/src/core/pipeline/pipeline-vue-utils.ts +96 -0
  57. package/src/core/pipeline/pipeline.ts +28 -0
  58. package/src/core/utils/index.ts +12 -0
  59. package/src/core/utils/inject-provider-decorators.ts +27 -0
  60. package/src/core/utils/lazy-inject-decorators.ts +38 -0
  61. package/src/core/utils/mouse-touch-event.ts +120 -0
  62. package/src/core/utils/playground-drag.ts +511 -0
  63. package/src/core/utils/playground-gesture.spec.ts +135 -0
  64. package/src/core/utils/playground-gesture.ts +118 -0
  65. package/src/core/utils/tween.ts +156 -0
  66. package/src/core/utils/use-gesture/core/Controller.ts +212 -0
  67. package/src/core/utils/use-gesture/core/EventStore.ts +46 -0
  68. package/src/core/utils/use-gesture/core/TimeoutStore.ts +28 -0
  69. package/src/core/utils/use-gesture/core/actions.ts +63 -0
  70. package/src/core/utils/use-gesture/core/config/commonConfigResolver.ts +86 -0
  71. package/src/core/utils/use-gesture/core/config/coordinatesConfigResolver.ts +48 -0
  72. package/src/core/utils/use-gesture/core/config/dragConfigResolver.ts +140 -0
  73. package/src/core/utils/use-gesture/core/config/hoverConfigResolver.ts +11 -0
  74. package/src/core/utils/use-gesture/core/config/moveConfigResolver.ts +11 -0
  75. package/src/core/utils/use-gesture/core/config/pinchConfigResolver.ts +59 -0
  76. package/src/core/utils/use-gesture/core/config/resolver.ts +70 -0
  77. package/src/core/utils/use-gesture/core/config/scrollConfigResolver.ts +8 -0
  78. package/src/core/utils/use-gesture/core/config/sharedConfigResolver.ts +28 -0
  79. package/src/core/utils/use-gesture/core/config/support.ts +52 -0
  80. package/src/core/utils/use-gesture/core/config/wheelConfigResolver.ts +8 -0
  81. package/src/core/utils/use-gesture/core/engines/CoordinatesEngine.ts +78 -0
  82. package/src/core/utils/use-gesture/core/engines/DragEngine.ts +403 -0
  83. package/src/core/utils/use-gesture/core/engines/Engine.ts +406 -0
  84. package/src/core/utils/use-gesture/core/engines/HoverEngine.ts +43 -0
  85. package/src/core/utils/use-gesture/core/engines/MoveEngine.ts +52 -0
  86. package/src/core/utils/use-gesture/core/engines/PinchEngine.ts +345 -0
  87. package/src/core/utils/use-gesture/core/engines/ScrollEngine.ts +42 -0
  88. package/src/core/utils/use-gesture/core/engines/WheelEngine.ts +48 -0
  89. package/src/core/utils/use-gesture/core/index.ts +7 -0
  90. package/src/core/utils/use-gesture/core/parser.ts +92 -0
  91. package/src/core/utils/use-gesture/core/types/action.ts +19 -0
  92. package/src/core/utils/use-gesture/core/types/config.ts +255 -0
  93. package/src/core/utils/use-gesture/core/types/handlers.ts +73 -0
  94. package/src/core/utils/use-gesture/core/types/index.ts +11 -0
  95. package/src/core/utils/use-gesture/core/types/internalConfig.ts +80 -0
  96. package/src/core/utils/use-gesture/core/types/state.ts +275 -0
  97. package/src/core/utils/use-gesture/core/types/utils.ts +204 -0
  98. package/src/core/utils/use-gesture/core/types.ts +8 -0
  99. package/src/core/utils/use-gesture/core/utils/events.ts +158 -0
  100. package/src/core/utils/use-gesture/core/utils/fn.ts +32 -0
  101. package/src/core/utils/use-gesture/core/utils/maths.ts +63 -0
  102. package/src/core/utils/use-gesture/core/utils/state.ts +24 -0
  103. package/src/core/utils/use-gesture/core/utils.ts +8 -0
  104. package/src/core/utils/use-gesture/index.ts +6 -0
  105. package/src/core/utils/use-gesture/vanilla/DragGesture.ts +27 -0
  106. package/src/core/utils/use-gesture/vanilla/Gesture.ts +45 -0
  107. package/src/core/utils/use-gesture/vanilla/HoverGesture.ts +27 -0
  108. package/src/core/utils/use-gesture/vanilla/MoveGesture.ts +27 -0
  109. package/src/core/utils/use-gesture/vanilla/PinchGesture.ts +27 -0
  110. package/src/core/utils/use-gesture/vanilla/Recognizer.ts +43 -0
  111. package/src/core/utils/use-gesture/vanilla/ScrollGesture.ts +27 -0
  112. package/src/core/utils/use-gesture/vanilla/WheelGesture.ts +27 -0
  113. package/src/core/utils/use-gesture/vanilla/createGesture.ts +18 -0
  114. package/src/core/utils/use-gesture/vanilla/index.ts +18 -0
  115. package/src/env.d.ts +10 -0
  116. package/src/index.ts +17 -0
  117. package/src/playground-config.ts +70 -0
  118. package/src/playground-container.ts +129 -0
  119. package/src/playground-contribution.ts +81 -0
  120. package/src/playground-mock-tools.ts +143 -0
  121. package/src/playground.ts +401 -0
  122. package/src/plugin/index.ts +6 -0
  123. package/src/plugin/plugin.ts +226 -0
  124. package/src/services/clipboard-service.ts +40 -0
  125. package/src/services/context-menu-service.ts +25 -0
  126. package/src/services/index.ts +10 -0
  127. package/src/services/logger-service.ts +49 -0
  128. package/src/services/selection-service.ts +54 -0
  129. package/src/services/storage-service.ts +65 -0
  130. package/src/vue/index.ts +8 -0
  131. package/src/vue/playground-vue-context.ts +22 -0
  132. package/src/vue/playground-vue-provider.ts +115 -0
  133. package/src/vue/playground-vue-renderer.vue +44 -0
@@ -0,0 +1,401 @@
1
+ /**
2
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ import { nanoid } from 'nanoid';
7
+ import type { Component } from 'vue';
8
+ import { inject, injectable, optional, named } from 'inversify';
9
+ import { Disposable, DisposableCollection, domUtils, type Event } from '@flowgram-vue/utils';
10
+ import { CommandService } from '@flowgram-vue/command';
11
+
12
+ import { SelectionService } from './services';
13
+ import { PlaygroundContribution, PlaygroundRegistry } from './playground-contribution';
14
+ import { PlaygroundConfig } from './playground-config';
15
+ import {
16
+ type PipelineDimension,
17
+ // PipelineDispatcher,
18
+ PipelineRegistry,
19
+ PipelineRenderer,
20
+ } from './core/pipeline';
21
+ import {
22
+ EditorStateConfigEntity,
23
+ PlaygroundConfigEntity,
24
+ type PlaygroundConfigRevealOpts,
25
+ } from './core/layer/config';
26
+ import { Layer, LayerRegistry } from './core';
27
+ import {
28
+ // type AbleDispatchEvent,
29
+ // AbleManager,
30
+ type ConfigEntity,
31
+ EntityManager,
32
+ type EntityRegistry,
33
+ PlaygroundContext,
34
+ ContributionProvider,
35
+ PlaygroundContextProvider,
36
+ } from './common';
37
+ // import { PlaygroundCommandRegistry, PlaygroundId, toContextMenuPath } from './playground-registries';
38
+
39
+ @injectable()
40
+ export class Playground<CONTEXT = PlaygroundContext> implements Disposable {
41
+ readonly toDispose = new DisposableCollection();
42
+
43
+ readonly node: HTMLElement;
44
+
45
+ private _focused = false;
46
+
47
+ readonly onBlur: Event<void>;
48
+
49
+ readonly onFocus: Event<void>;
50
+
51
+ readonly onZoom: Event<number>;
52
+
53
+ readonly onScroll: Event<{ scrollX: number; scrollY: number }>;
54
+
55
+ get onResize() {
56
+ return this.pipelineRegistry.onResizeEmitter.event;
57
+ }
58
+
59
+ // 唯一 className,适配画布多实例场景
60
+ private playgroundClassName = nanoid();
61
+
62
+ constructor(
63
+ // @inject(PlaygroundId) readonly id: PlaygroundId,
64
+ @inject(EntityManager) readonly entityManager: EntityManager,
65
+ @inject(PlaygroundRegistry) readonly registry: PlaygroundRegistry,
66
+ @inject(PlaygroundContextProvider)
67
+ @optional()
68
+ readonly contextProvider: PlaygroundContextProvider,
69
+ @inject(PipelineRenderer)
70
+ readonly pipelineRenderer: PipelineRenderer,
71
+ // @inject(PlaygroundCommandRegistry) protected readonly commands: PlaygroundCommandRegistry,
72
+ @inject(PipelineRegistry)
73
+ readonly pipelineRegistry: PipelineRegistry,
74
+ // @inject(AbleManager) readonly ableManager: AbleManager,
75
+ // @inject(PipelineDispatcher) protected readonly dispatcher: PipelineDispatcher,
76
+ @inject(PlaygroundConfig)
77
+ protected readonly playgroundConfig: PlaygroundConfig,
78
+ @inject(ContributionProvider)
79
+ @named(PlaygroundContribution)
80
+ @optional()
81
+ protected readonly contributionProvider: ContributionProvider<PlaygroundContribution>,
82
+ /**
83
+ * 用于管理画布命令
84
+ */
85
+ @inject(CommandService) readonly commandService: CommandService,
86
+ /**
87
+ * 用于管理画布选择
88
+ */
89
+ @inject(SelectionService) readonly selectionService: SelectionService
90
+ ) {
91
+ this.toDispose.pushAll([
92
+ this.pipelineRenderer,
93
+ this.pipelineRegistry,
94
+ this.entityManager,
95
+ // this.ableManager,
96
+ this.commandService,
97
+ this.selectionService,
98
+ Disposable.create(() => {
99
+ this.node.remove();
100
+ }),
101
+ pipelineRenderer.onAllLayersRendered(() => {
102
+ this.contributions.forEach((contrib) => contrib.onAllLayersRendered?.(this));
103
+ }),
104
+ ]);
105
+ // Deafult entities added
106
+ const editStates =
107
+ this.entityManager.createEntity<EditorStateConfigEntity>(EditorStateConfigEntity);
108
+ this.entityManager.createEntity(PlaygroundConfigEntity);
109
+ this.node = playgroundConfig.node || document.createElement('div');
110
+ this.config.playgroundDomNode = this.node;
111
+ this.toDispose.pushAll([
112
+ // 浏览器原生的 scrollIntoView 会导致页面的滚动
113
+ // 需要禁用这种操作,否则会引发画布 viewport 计算问题
114
+ domUtils.addStandardDisposableListener(this.node, 'scroll', (event: UIEvent) => {
115
+ this.node.scrollTop = 0;
116
+ this.node.scrollLeft = 0;
117
+ event.preventDefault();
118
+ event.stopPropagation();
119
+ }),
120
+ ]);
121
+ this.node.classList.add('gedit-playground');
122
+ if (process.env.NODE_ENV !== 'test') {
123
+ this.node.classList.add(this.playgroundClassName);
124
+ }
125
+ this.node.dataset.testid = 'sdk.workflow.canvas';
126
+ if (playgroundConfig.layers)
127
+ playgroundConfig.layers.forEach((layer) => this.registry.registerLayer(layer));
128
+ // if (playgroundConfig.ables)
129
+ // playgroundConfig.ables.forEach(able => this.ableManager.registerAble(able));
130
+ // if (playgroundConfig.entities)
131
+ // playgroundConfig.entities.forEach(entity => this.entityManager.registerEntity(entity));
132
+ if (playgroundConfig.editorStates)
133
+ playgroundConfig.editorStates.forEach((state) => editStates.registerState(state));
134
+ if (playgroundConfig.zoomEnable !== undefined) this.zoomEnable = playgroundConfig.zoomEnable;
135
+ if (playgroundConfig.entityConfigs) {
136
+ for (const [k, v] of playgroundConfig.entityConfigs) {
137
+ const entity = this.entityManager.getEntity<ConfigEntity>(k, true);
138
+ entity?.updateConfig(v);
139
+ }
140
+ }
141
+ this.node.addEventListener('blur', () => {
142
+ this.blur();
143
+ });
144
+ this.node.addEventListener('focus', () => {
145
+ this.focus();
146
+ });
147
+ this.node.tabIndex = 0;
148
+ this.node.appendChild(this.pipelineRenderer.node);
149
+ this.onBlur = this.pipelineRegistry.onBlurEmitter.event;
150
+ this.onFocus = this.pipelineRegistry.onFocusEmitter.event;
151
+ this.onZoom = this.pipelineRegistry.onZoomEmitter.event;
152
+ this.onScroll = this.pipelineRegistry.onScrollEmitter.event;
153
+ }
154
+
155
+ get context(): CONTEXT {
156
+ return this.contextProvider?.();
157
+ }
158
+
159
+ protected get contributions(): PlaygroundContribution[] {
160
+ return this.contributionProvider.getContributions();
161
+ }
162
+
163
+ init(): void {
164
+ const { contributions } = this;
165
+ for (const contrib of contributions) {
166
+ if (contrib.registerPlayground) contrib.registerPlayground(this.registry);
167
+ }
168
+ for (const contrib of contributions) {
169
+ if (contrib.onInit) contrib.onInit(this);
170
+ }
171
+ }
172
+
173
+ get pipelineNode(): HTMLDivElement {
174
+ return this.pipelineRenderer.node;
175
+ }
176
+
177
+ setParent(parent: HTMLElement): void {
178
+ parent.appendChild(this.node);
179
+ this.resize();
180
+ }
181
+
182
+ // get onDispatch(): Event<AbleDispatchEvent> {
183
+ // return this.ableManager.onAbleDispatch;
184
+ // }
185
+
186
+ /**
187
+ * 对应的右键菜单路径
188
+ */
189
+ // get contextMenuPath(): string[] {
190
+ // return this.playgroundConfig.contextMenuPath ? this.playgroundConfig.contextMenuPath : toContextMenuPath(this.id);
191
+ // }
192
+ get zoomEnable(): boolean {
193
+ return this.config.zoomEnable;
194
+ }
195
+
196
+ set zoomEnable(zoomEnable) {
197
+ this.config.zoomEnable = zoomEnable;
198
+ }
199
+
200
+ /**
201
+ * 转换为内部的命令 id
202
+ * @param commandId
203
+ */
204
+ // toPlaygroundCommandId(commandId: string): string {
205
+ // return this.registry.commands.toCommandId(commandId);
206
+ // }
207
+ /**
208
+ * 通知所有关联 able 的 entity
209
+ */
210
+ // dispatch<P>(payloadKey: string | symbol, payload: P): string[] {
211
+ // return this.ableManager.dispatch(payloadKey, payload);
212
+ // }
213
+
214
+ /**
215
+ * 刷新所有 layer
216
+ */
217
+ flush(): void {
218
+ this.pipelineRenderer.flush();
219
+ }
220
+
221
+ /**
222
+ * 执行命令
223
+ * @param commandId
224
+ * @param args
225
+ */
226
+ // execCommand<T>(commandId: string, ...args: any[]): Promise<T | undefined> {
227
+ // return this.commands.executeCommand<T>(commandId, ...args);
228
+ // }
229
+ private isReady = false;
230
+
231
+ ready(): void {
232
+ if (this.isReady) return;
233
+ this.isReady = true;
234
+ if (this.playgroundConfig.autoResize) {
235
+ const resize = () => {
236
+ if (this.disposed) return;
237
+ this.resize();
238
+ };
239
+ if (typeof ResizeObserver !== 'undefined') {
240
+ const resizeObserver = new ResizeObserver(resize);
241
+ resizeObserver.observe(this.node);
242
+ this.toDispose.push(
243
+ Disposable.create(() => {
244
+ resizeObserver.disconnect();
245
+ })
246
+ );
247
+ } else {
248
+ this.toDispose.push(
249
+ domUtils.addStandardDisposableListener(window.document.body, 'resize', resize, {
250
+ passive: true,
251
+ })
252
+ );
253
+ }
254
+ this.resize();
255
+ }
256
+ this.pipelineRegistry.ready();
257
+ this.pipelineRenderer.ready();
258
+ const { contributions } = this;
259
+ for (const contrib of contributions) {
260
+ if (contrib.onReady) contrib.onReady(this);
261
+ }
262
+ }
263
+
264
+ /**
265
+ * 按下边顺序执行
266
+ * 1. 指定的 entity 位置或 pos 位置
267
+ * 2. selection 位置
268
+ * 3. 初始化位置
269
+ */
270
+ scrollToView(opts?: PlaygroundConfigRevealOpts): Promise<void> {
271
+ const playgroundEntity =
272
+ this.entityManager.getEntity<PlaygroundConfigEntity>(PlaygroundConfigEntity)!;
273
+ return playgroundEntity.scrollToView(opts);
274
+ }
275
+
276
+ /**
277
+ * 这里会由 widget 透传进来
278
+ * @param msg
279
+ */
280
+ resize(msg?: PipelineDimension, scrollToCenter = true): boolean {
281
+ if (!msg) {
282
+ const boundingRect = this.node.getBoundingClientRect();
283
+ msg = {
284
+ width: boundingRect.width,
285
+ height: boundingRect.height,
286
+ };
287
+ }
288
+ // 页面宽度变更 触发滚动偏移
289
+ const { width, height } = this.config.config;
290
+ if (msg.width === 0 || msg.height === 0) {
291
+ return false;
292
+ }
293
+ const oldConfig = this.config.config;
294
+ let { scrollX, scrollY } = this.config.config;
295
+ // 这个在处理滚动
296
+ if (scrollToCenter && width && Math.round(msg.width) !== width) {
297
+ scrollX += (width - msg.width) / 2;
298
+ }
299
+ if (scrollToCenter && height && Math.round(msg.height) !== height) {
300
+ scrollY += (height - msg.height) / 2;
301
+ }
302
+ if (
303
+ Math.round(msg.width) !== width ||
304
+ Math.round(msg.height) !== height ||
305
+ oldConfig.scrollX !== scrollX ||
306
+ oldConfig.scrollY !== scrollY
307
+ ) {
308
+ this.config.updateConfig({ ...msg, scrollX, scrollY });
309
+ this.pipelineRegistry.onResizeEmitter.fire(msg);
310
+ }
311
+ return true;
312
+ }
313
+
314
+ /**
315
+ * 触发 focus
316
+ */
317
+ protected focus(): void {
318
+ if (this._focused) return;
319
+ this._focused = true;
320
+ this.pipelineRegistry.onFocusEmitter.fire();
321
+ }
322
+
323
+ /**
324
+ * 触发 blur
325
+ */
326
+ protected blur(): void {
327
+ if (!this._focused) return;
328
+ this._focused = false;
329
+ this.pipelineRegistry.onBlurEmitter.fire();
330
+ }
331
+
332
+ get focused(): boolean {
333
+ return this._focused;
334
+ }
335
+
336
+ /**
337
+ * 画布配置数据
338
+ */
339
+ get config(): PlaygroundConfigEntity {
340
+ return this.entityManager.getEntity<PlaygroundConfigEntity>(PlaygroundConfigEntity)!;
341
+ }
342
+
343
+ /**
344
+ * 画布编辑状态管理
345
+ */
346
+ get editorState(): EditorStateConfigEntity {
347
+ return this.entityManager.getEntity<EditorStateConfigEntity>(EditorStateConfigEntity)!;
348
+ }
349
+
350
+ getConfigEntity<T extends ConfigEntity>(r: EntityRegistry<T>): T {
351
+ return this.entityManager.getEntity<T>(r, true) as T;
352
+ }
353
+
354
+ dispose(): void {
355
+ if (this.disposed) return;
356
+ const { contributions } = this;
357
+ for (const contrib of contributions) {
358
+ if (contrib.onDispose) contrib.onDispose(this);
359
+ }
360
+ this.toDispose.dispose();
361
+ }
362
+
363
+ get disposed(): boolean {
364
+ return this.toDispose.disposed;
365
+ }
366
+
367
+ /**
368
+ * 转换成 Vue 组件
369
+ */
370
+ toVueComponent(): Component {
371
+ return this.pipelineRenderer.toVueComponent();
372
+ }
373
+
374
+ /**
375
+ * 注册 layer
376
+ */
377
+ registerLayer<P extends Layer = Layer>(
378
+ layerRegistry: LayerRegistry<P>,
379
+ layerOptions?: P['options']
380
+ ): void {
381
+ this.pipelineRegistry.registerLayer<P>(layerRegistry, layerOptions);
382
+ }
383
+
384
+ /**
385
+ * 注册 多个 layer
386
+ */
387
+ registerLayers(...layerRegistries: LayerRegistry[]): void {
388
+ layerRegistries.forEach((layer) => this.pipelineRegistry.registerLayer(layer));
389
+ }
390
+
391
+ /**
392
+ * 获取 layer
393
+ */
394
+ getLayer<T extends Layer>(layerRegistry: LayerRegistry<T>): T | undefined {
395
+ return this.pipelineRegistry.getLayer<T>(layerRegistry);
396
+ }
397
+
398
+ get onAllLayersRendered(): Event<void> {
399
+ return this.pipelineRenderer.onAllLayersRendered;
400
+ }
401
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ export * from './plugin';
@@ -0,0 +1,226 @@
1
+ /**
2
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ import { ContainerModule, interfaces } from 'inversify';
7
+
8
+ import { PlaygroundContribution } from '../playground-contribution';
9
+ import { type Playground } from '../playground';
10
+
11
+ export interface PluginContext {
12
+ /**
13
+ * 画布实例
14
+ */
15
+ playground: Playground;
16
+ /**
17
+ * IOC 容器
18
+ */
19
+ container: interfaces.Container;
20
+ /**
21
+ * 获取 IOC 容器的 单例模块
22
+ * @param identifier
23
+ */
24
+ get<T>(identifier: interfaces.ServiceIdentifier<T>): T;
25
+
26
+ /**
27
+ * 获取 IOC 容器的 多例模块
28
+ */
29
+ getAll<T>(identifier: interfaces.ServiceIdentifier<T>): T[];
30
+ }
31
+
32
+ export const PluginContext = Symbol('PluginContext');
33
+ export interface PluginBindConfig {
34
+ bind: interfaces.Bind;
35
+ unbind: interfaces.Unbind;
36
+ isBound: interfaces.IsBound;
37
+ rebind: interfaces.Rebind;
38
+ }
39
+
40
+ export interface PluginConfig<Opts, CTX extends PluginContext = PluginContext> {
41
+ /**
42
+ * 插件 IOC 注册, 等价于 containerModule
43
+ * @param ctx
44
+ */
45
+ onBind?: (bindConfig: PluginBindConfig, opts: Opts) => void;
46
+ /**
47
+ * 画布注册阶段
48
+ */
49
+ onInit?: (ctx: CTX, opts: Opts) => void;
50
+ /**
51
+ * 画布准备阶段,一般用于 dom 事件注册等
52
+ */
53
+ onReady?: (ctx: CTX, opts: Opts) => void;
54
+ /**
55
+ * 画布销毁阶段
56
+ */
57
+ onDispose?: (ctx: CTX, opts: Opts) => void;
58
+ /**
59
+ * 画布所有 layer 渲染结束
60
+ */
61
+ onAllLayersRendered?: (ctx: CTX, opts: Opts) => void;
62
+ /**
63
+ * IOC 模块,用于更底层的插件扩展
64
+ */
65
+ containerModules?: interfaces.ContainerModule[];
66
+ }
67
+
68
+ export const Plugin = Symbol('Plugin');
69
+
70
+ export type Plugin<Options = any> = {
71
+ options: Options;
72
+ pluginId: string;
73
+ singleton: boolean;
74
+ initPlugin: () => void;
75
+ contributionKeys?: interfaces.ServiceIdentifier[];
76
+ containerModules?: interfaces.ContainerModule[];
77
+ };
78
+
79
+ export interface PluginsProvider<CTX extends PluginContext = PluginContext> {
80
+ (ctx: CTX): Plugin[];
81
+ }
82
+
83
+ export type PluginCreator<Options> = (opts: Options) => Plugin<Options>;
84
+
85
+ export function loadPlugins(plugins: Plugin[], container: interfaces.Container): void {
86
+ const pluginInitSet = new Set<string>();
87
+ const singletonPluginIds = new Set<string>();
88
+ const modules: interfaces.ContainerModule[] = plugins
89
+ .reduceRight((result: Plugin[], plugin: Plugin) => {
90
+ const shouldSkip = plugin.singleton && singletonPluginIds.has(plugin.pluginId);
91
+ if (plugin.singleton) {
92
+ singletonPluginIds.add(plugin.pluginId);
93
+ }
94
+ return shouldSkip ? result : [plugin, ...result];
95
+ }, [])
96
+ .reduce((res, plugin) => {
97
+ if (!pluginInitSet.has(plugin.pluginId)) {
98
+ plugin.initPlugin();
99
+ pluginInitSet.add(plugin.pluginId);
100
+ }
101
+ if (plugin.containerModules && plugin.containerModules.length > 0) {
102
+ for (let module of plugin.containerModules) {
103
+ // 去重
104
+ if (!res.includes(module)) {
105
+ res.push(module);
106
+ }
107
+ }
108
+ return res;
109
+ }
110
+ return res;
111
+ }, [] as interfaces.ContainerModule[]);
112
+ modules.forEach((module) => container.load(module));
113
+ plugins.forEach((plugin) => {
114
+ if (plugin.contributionKeys) {
115
+ for (const contribution of plugin.contributionKeys) {
116
+ container.bind(contribution).toConstantValue(plugin.options);
117
+ }
118
+ }
119
+ });
120
+ }
121
+
122
+ function toPlaygroundContainerModule<Options, CTX extends PluginContext = PluginContext>(
123
+ config: {
124
+ onInit?: (ctx: CTX, opts: Options) => void;
125
+ onDispose?: (ctx: CTX, opts: Options) => void;
126
+ onReady?: (ctx: CTX, opts: Options) => void;
127
+ onAllLayersRendered?: (ctx: CTX, opts: Options) => void;
128
+ },
129
+ opts: Options
130
+ ): interfaces.ContainerModule {
131
+ return new ContainerModule((bind) => {
132
+ bind(PlaygroundContribution).toDynamicValue((ctx) => {
133
+ const pluginContext = ctx.container.get<CTX>(PluginContext);
134
+ return {
135
+ onInit: () => {
136
+ config.onInit?.(pluginContext, opts);
137
+ },
138
+ onReady: () => {
139
+ config.onReady?.(pluginContext, opts);
140
+ },
141
+ onDispose: () => {
142
+ config.onDispose?.(pluginContext, opts);
143
+ },
144
+ onAllLayersRendered: () => {
145
+ config.onAllLayersRendered?.(pluginContext, opts);
146
+ },
147
+ };
148
+ });
149
+ });
150
+ }
151
+
152
+ let pluginIndex = 0;
153
+ export function definePluginCreator<Options, CTX extends PluginContext = PluginContext>(
154
+ config: {
155
+ containerModules?: interfaces.ContainerModule[];
156
+ contributionKeys?: interfaces.ServiceIdentifier[];
157
+ singleton?: boolean;
158
+ } & PluginConfig<Options, CTX>
159
+ ): PluginCreator<Options> {
160
+ const { contributionKeys, singleton = false } = config;
161
+ pluginIndex += 1;
162
+ const pluginId = `Playground_${pluginIndex}`;
163
+ return (opts: Options) => {
164
+ const containerModules: interfaces.ContainerModule[] = [];
165
+ let isInit = false;
166
+
167
+ return {
168
+ pluginId,
169
+ singleton,
170
+ initPlugin: () => {
171
+ // 防止 plugin 被上层业务多次 init
172
+ if (isInit) {
173
+ return;
174
+ }
175
+ isInit = true;
176
+
177
+ if (config.containerModules) {
178
+ containerModules.push(...config.containerModules);
179
+ }
180
+ if (config.onBind) {
181
+ containerModules.push(
182
+ new ContainerModule((bind, unbind, isBound, rebind) => {
183
+ config.onBind!(
184
+ {
185
+ bind,
186
+ unbind,
187
+ isBound,
188
+ rebind,
189
+ },
190
+ opts
191
+ );
192
+ })
193
+ );
194
+ }
195
+ if (config.onInit || config.onDispose || config.onReady || config.onAllLayersRendered) {
196
+ containerModules.push(toPlaygroundContainerModule<Options, CTX>(config, opts));
197
+ }
198
+ },
199
+ options: opts,
200
+ contributionKeys,
201
+ containerModules,
202
+ };
203
+ };
204
+ }
205
+
206
+ /**
207
+ * @example
208
+ * createPlaygroundPlugin({
209
+ * // IOC 注册
210
+ * onBind(bind) {
211
+ * bind('xxx').toSelf().inSingletonScope()
212
+ * },
213
+ * // 画布初始化
214
+ * onInit(ctx) {
215
+ * ctx.playground.registerLayer(MyLayer)
216
+ * },
217
+ * // 画布销毁
218
+ * onDispose(ctx) {
219
+ * },
220
+ * // IOC 模块
221
+ * containerModules: [new ContainerModule(() => {})]
222
+ * })
223
+ */
224
+ export const createPlaygroundPlugin = <CTX extends PluginContext = PluginContext>(
225
+ options: PluginConfig<undefined, CTX>
226
+ ) => definePluginCreator<undefined, CTX>(options)(undefined);
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ import { injectable } from 'inversify';
7
+ import { Emitter, type Event, type MaybePromise } from '@flowgram-vue/utils';
8
+
9
+ export const ClipboardService = Symbol('ClipboardService');
10
+
11
+ export interface ClipboardService {
12
+ onClipboardChanged: Event<string>;
13
+ readText(): MaybePromise<string>;
14
+
15
+ writeText(value: string): MaybePromise<void>;
16
+ }
17
+
18
+ /**
19
+ * 剪贴板服务,一般用于管理临时的复制黏贴数据
20
+ * TODO: 后续可以支持调用浏览器
21
+ */
22
+ @injectable()
23
+ export class DefaultClipboardService implements ClipboardService {
24
+ private _currentData: string;
25
+
26
+ protected readonly onClipboardChangedEmitter = new Emitter<string>();
27
+
28
+ readonly onClipboardChanged: Event<string> = this.onClipboardChangedEmitter.event;
29
+
30
+ readText(): string {
31
+ return this._currentData;
32
+ }
33
+
34
+ writeText(value: string): void {
35
+ if (this._currentData !== value) {
36
+ this._currentData = value;
37
+ this.onClipboardChangedEmitter.fire(value);
38
+ }
39
+ }
40
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ import { injectable } from 'inversify';
7
+
8
+ /**
9
+ * 圈选右键菜单相关 service
10
+ */
11
+ @injectable()
12
+ export class ContextMenuService {
13
+ /**
14
+ * 右键面板是否展示,展示的时候为 true
15
+ */
16
+ private isRightPanelVisible: boolean;
17
+
18
+ get rightPanelVisible(): boolean {
19
+ return this.isRightPanelVisible;
20
+ }
21
+
22
+ set rightPanelVisible(visible: boolean) {
23
+ this.isRightPanelVisible = visible;
24
+ }
25
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ export * from './selection-service';
7
+ export * from './storage-service';
8
+ export * from './clipboard-service';
9
+ export * from './context-menu-service';
10
+ export * from './logger-service';