fix: en doc images (#68)
* fix: en doc images * chore: add ci trigger in merge queue
2
.github/workflows/ci.yml
vendored
@ -4,6 +4,8 @@ on:
|
||||
branches: [ "main" ]
|
||||
pull_request:
|
||||
branches: [ "main" ]
|
||||
merge_group:
|
||||
branches: [ "main" ]
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@ -22,7 +22,7 @@ const { playground } = useClientContext()
|
||||
- The Layer implements observer-style reactive dynamic dependency collection similar to MobX. Data updates will trigger autorun or render.
|
||||
:::
|
||||
|
||||

|
||||

|
||||
|
||||
- Layer Lifecycle
|
||||
|
||||
|
||||
@ -12,7 +12,7 @@
|
||||
|
||||
Before answering this question, let's first understand aspect - oriented programming. The purpose of aspect - oriented programming is to split the granularity of domain logic into smaller parts. The cross - cutting part can be "consumed on demand" by the vertical - cutting part. The connection between the cross - cutting and vertical - cutting parts is also called weaving. And IOC plays the role of weaving and is injected into the vertical - cutting part.
|
||||
|
||||

|
||||

|
||||
|
||||
Ideal Aspect - Oriented Programming
|
||||
|
||||
|
||||
@ -12,7 +12,7 @@ The variable engine is designed to follow the DIP (Dependency Inversion Principl
|
||||
|
||||
:::
|
||||
|
||||

|
||||

|
||||
|
||||
### Glossary
|
||||
|
||||
@ -67,7 +67,7 @@ An AST node used to declare a new variable, which points to a value that changes
|
||||
:::
|
||||
<table>
|
||||
<tr>
|
||||
<td><img loading="lazy" src="/variable-type1.png"/></td>
|
||||
<td><img loading="lazy" src="/en-variable-type1.png"/></td>
|
||||
<td><img loading="lazy" src="/variable-type2.png"/></td>
|
||||
</tr>
|
||||
</table>
|
||||
@ -81,4 +81,4 @@ An AST node used to declare a new variable, which points to a value that changes
|
||||
|
||||
:::
|
||||
|
||||

|
||||

|
||||
|
||||
@ -1,3 +0,0 @@
|
||||
[
|
||||
"introduction"
|
||||
]
|
||||
|
Before Width: | Height: | Size: 45 KiB |
|
Before Width: | Height: | Size: 93 KiB |
|
Before Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 322 KiB |
|
Before Width: | Height: | Size: 179 KiB |
|
Before Width: | Height: | Size: 63 KiB |
@ -1,3 +0,0 @@
|
||||
[
|
||||
"index"
|
||||
]
|
||||
@ -1,175 +0,0 @@
|
||||
# Core Modeling
|
||||
|
||||
Playground provides a coordinate system at its foundation
|
||||
|
||||
```ts
|
||||
interface Playground {
|
||||
node: HTMLDivElement // DOM node where canvas is mounted
|
||||
scrollToView({
|
||||
entities?: Entity[] // Specified nodes
|
||||
easing?: boolean // Enable easing
|
||||
bounds?: Reactangle // Scroll to specific bbox position
|
||||
}) // Make canvas scroll smoothly to a specific node and center it
|
||||
toReactComponent() // Render as React node
|
||||
readonly: boolean // Read-only mode
|
||||
config: PlaygroundConfigEntity // Contains canvas data like zoom, scroll etc.
|
||||
}
|
||||
// Quick access hook
|
||||
const playground = usePlayground()
|
||||
```
|
||||
|
||||
## Layer
|
||||
|
||||
:::warning P.S.
|
||||
- The rendering layer establishes its own coordinate system at the bottom layer, implementing simulated scrolling, zooming and other logic based on this coordinate system. When calculating viewport, nodes also need to be transformed to this coordinate system
|
||||
- Rendering is split into multiple layers (Layer) on the canvas. The layered design is based on ECS data splitting concept. Different Layers only listen to the data they want and render independently without interference. Layer can be understood as the System in ECS, which is where Entity data is ultimately consumed
|
||||
- Layer implements observer reactive dynamic dependency collection similar to mobx, data updates will trigger autorun or render
|
||||
:::
|
||||
|
||||

|
||||
|
||||
- Layer lifecycle
|
||||
|
||||
```ts
|
||||
interface Layer {
|
||||
/**
|
||||
* Triggered during initialization
|
||||
*/
|
||||
onReady?(): void;
|
||||
|
||||
/**
|
||||
* Triggered when playground size changes
|
||||
*/
|
||||
onResize?(size: PipelineDimension): void;
|
||||
|
||||
/**
|
||||
* Triggered when playground gains focus
|
||||
*/
|
||||
onFocus?(): void;
|
||||
|
||||
/**
|
||||
* Triggered when playground loses focus
|
||||
*/
|
||||
onBlur?(): void;
|
||||
|
||||
/**
|
||||
* Listen to zoom changes
|
||||
*/
|
||||
onZoom?(scale: number): void;
|
||||
|
||||
/**
|
||||
* Listen to scroll changes
|
||||
*/
|
||||
onScroll?(scroll: { scrollX: number; scrollY: number }): void;
|
||||
|
||||
/**
|
||||
* Triggered when viewport updates
|
||||
*/
|
||||
onViewportChange?(): void;
|
||||
|
||||
/**
|
||||
* Triggered when readonly or disable state changes
|
||||
* @param state
|
||||
*/
|
||||
onReadonlyOrDisabledChange?(state: { disabled: boolean; readonly: boolean }): void;
|
||||
|
||||
/**
|
||||
* Data updates automatically trigger React render, if not provided React rendering won't be called
|
||||
*/
|
||||
render?(): JSX.Element
|
||||
}
|
||||
```
|
||||
|
||||
Layer's positioning is actually similar to Unity game engine's [MonoBehaviour](https://docs.unity3d.com/ScriptReference/MonoBehaviour.html). Unity game engine's script extensions are all based on this, which can be considered the core design, and the underlying implementation is also based on C#'s Reflection dependency injection capability
|
||||
|
||||
```C#
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
public class MyMonoBehavior : MonoBehaviour
|
||||
{
|
||||
void Awake()
|
||||
{
|
||||
Debug.Log("Awake method is always called before application starts.");
|
||||
}
|
||||
void Start()
|
||||
{
|
||||
Debug.Log("Start method is always called after Awake().");
|
||||
}
|
||||
void Update()
|
||||
{
|
||||
Debug.Log("Update method is called in every frame.");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- Layer's reactive updates
|
||||
|
||||
```ts
|
||||
export class DemoLayer extends Layer {
|
||||
// Any inversify module injection
|
||||
@inject(FlowDocument) document: FlowDocument
|
||||
// Observe single Entity
|
||||
@observeEntity(SomeEntity) entity: SomeEntity
|
||||
// Observe multiple Entities
|
||||
@observeEntities(SomeEntity) entities: SomeEntity[]
|
||||
// Observe Entity data block (ECS - Component) changes
|
||||
@observeEntityDatas(SomeEntity, SomeEntityData) transforms: SomeEntityData[]
|
||||
autorun() {}
|
||||
render() {
|
||||
return <div></div>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## FlowNodeEntity
|
||||
- Nodes form a tree. In free canvas mode, nodes are flat without child nodes
|
||||
```ts
|
||||
inteface FlowNodeEntity {
|
||||
id: string
|
||||
children?: FlowNodeEntity[]
|
||||
pre?: FlowNodeEntity
|
||||
next?: FlowNodeEntity
|
||||
parent?: FlowNodeEntity
|
||||
collapsed: boolean // Whether expanded
|
||||
getData(dataRegistry): NodeEntityData
|
||||
addData(dataRegistry)
|
||||
}
|
||||
```
|
||||
|
||||
## 2.4.4 FlowNodeTransformData Node's bbox
|
||||
|
||||
```ts
|
||||
class FlowNodeTransformData {
|
||||
localTransform: Matrix, // Relative offset, only relative to the previous Sibling node offset in the same Block
|
||||
worldTransform: Matrix, // Absolute offset, relative to the accumulated offset of Parent and Sibling nodes
|
||||
delta:Point // Center/left alignment offset, independent from Matrix, controlled by each node itself
|
||||
getSize(): Size, // Calculated from own (independent node) or child branch node width/height spacing
|
||||
getBounds(): Rectangle // Calculated from worldMatrix and size, used for final rendering, this range can also be used to determine highlight selection area
|
||||
inputPoint(): Point // Input point position, generally the top center position of the first node in Block (centered layout)
|
||||
outputPoint(): Point // Output point position, default is node's bottom center position, but for conditional branches, determined by built-in end node and specific logic
|
||||
}
|
||||
```
|
||||
|
||||
## FlowNodeRenderData Node content rendering data
|
||||
|
||||
```ts
|
||||
class FlowNodeRenderData {
|
||||
node: HTMLDivElement // Current node's DOM element
|
||||
expanded:boolean // Whether expanded
|
||||
activated: boolean // Whether activated
|
||||
hidden: boolean // Whether hidden
|
||||
}
|
||||
```
|
||||
|
||||
## FlowDocument
|
||||
|
||||
```ts
|
||||
interface FLowDocument {
|
||||
root: FlowNodeEntity // Canvas root node
|
||||
fromJSON(data): void // Import data
|
||||
toJSON(): FlowDocumentJSON // Export data
|
||||
addNode(type: string, meta: any): FlowNodeEntity // Add node
|
||||
traveseDFS(fn: (node: flowNodeEntity) => void, startNode = this.root) // Traverse
|
||||
}
|
||||
```
|
||||
@ -1,186 +0,0 @@
|
||||
# Frequently Asked Questions
|
||||
## Why Not Use ReactFlow
|
||||
- ReactFlow doesn't do data modeling or provide layout algorithms, it only handles rendering. Development complexity and labor costs remain high
|
||||
See: https://reactflow.dev/examples/nodes/custom-node
|
||||
- ReactFlow's interaction customization cost is high. As shown below, it's difficult to select points when the canvas is zoomed out, and it doesn't support drag-and-drop line reconnection
|
||||
<table>
|
||||
<tr>
|
||||
<td><img loading="lazy" src="/reactflow/reactflow-render.gif"/></td>
|
||||
<td><img loading="lazy" src="/reactflow/reactflow-interaction.gif"/></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
## ReactFlow Paid Features
|
||||
|
||||
| Paid Features | Supported by FlowGramAI | Future Plan |
|
||||
|----------------------------------|------------------------|--------------|
|
||||
| Grouping | Yes | |
|
||||
| Redo/Undo | Yes | |
|
||||
| Copy/Paste | Yes | |
|
||||
| HelpLines | Yes | |
|
||||
| Custom Nodes and Shapes | Yes | |
|
||||
| Custom Lines | Yes | |
|
||||
| AutoLayout | Yes | |
|
||||
| ForceLayout | No | No |
|
||||
| Expand/Collapse | Yes | |
|
||||
| Collaborative | No | Yes |
|
||||
| WorkflowBuilder (Complete Auto-layout Case) | Yes | |
|
||||
|
||||
## Why Need IOC
|
||||
|
||||
:::tip Key Concepts:
|
||||
- Inversion of Control: A design principle in object-oriented programming that reduces coupling between code modules. The most common form is Dependency Injection (DI)
|
||||
- Domain Logic: Also called Business Logic, these are logic related to specific product features
|
||||
- Aspect-Oriented Programming (AOP): The core design principle is to split software systems into common logic (cross-cutting, pervasive) and domain logic (vertical-cutting) aspects, where cross-cutting parts can be "consumed as needed" by all vertical-cutting parts
|
||||
|
||||
:::
|
||||
|
||||
Before answering this question, let's understand aspect-oriented programming. AOP aims to break down domain logic into finer granularity, where cross-cutting parts can be "consumed as needed" by vertical-cutting parts. The connection between cross-cutting and vertical-cutting is called Weaving, and IOC plays the role of Weaving injection into vertical-cutting parts.
|
||||
|
||||

|
||||
|
||||
Ideal aspect-oriented programming
|
||||
|
||||
```ts
|
||||
- myAppliation provides business logic
|
||||
- service specific business logic services
|
||||
- customDomainLogicService
|
||||
- contributionImplement hook registration instantiation
|
||||
- MyApplicationContributionImpl
|
||||
- component business components
|
||||
|
||||
- core provides common logic
|
||||
- model common models
|
||||
- contribution hook interfaces
|
||||
- LifecycleContribution application lifecycle
|
||||
- CommandContribution
|
||||
- service common services
|
||||
- CommandService
|
||||
- ClipboardService
|
||||
- component common components
|
||||
```
|
||||
|
||||
```ts
|
||||
// IOC injection
|
||||
@injectable()
|
||||
export class CustomDomainLogicService {
|
||||
@inject(FlowContextService) protected flowContextService: FlowContextService;
|
||||
@inject(CommandService) protected commandService: CommandService;
|
||||
@inject(SelectionService) protected selectionService: SelectionService;
|
||||
}
|
||||
// IOC interface declaration
|
||||
interface LifecycleContribution {
|
||||
onInit(): void
|
||||
onStart(): void
|
||||
onDispose(): void
|
||||
}
|
||||
// IOC interface implementation
|
||||
@injectable()
|
||||
export class MyApplicationContributionImpl implement LifecycleContribution {
|
||||
onStart(): void {
|
||||
// Specific business logic code
|
||||
}
|
||||
}
|
||||
|
||||
// Manually mount to lifecycle hook
|
||||
bind(LifecycleContribution).toService(MyApplicationContributionImpl)
|
||||
```
|
||||
|
||||
:::warning IOC is a means of aspect-oriented programming. After introduction, underlying modules can expose interfaces for external registration, bringing benefits:
|
||||
- Implements microkernel + plugin design, enabling plugin pluggability and on-demand consumption
|
||||
- Allows for cleaner package separation, implementing feature-based package splitting
|
||||
:::
|
||||
|
||||
## Why Need ECS
|
||||
|
||||
:::warning ECS (Entity-Component-System)
|
||||
Suitable for decoupling large data objects, often used in games, where each character (Entity) in the game has very large data, which needs to be split into multiple components such as physical engine related data, skin related, role attributes, etc. (multiple Components), consumed by different subsystems (Systems). The data structure of the process is complex, which is very suitable for ECS for decomposition
|
||||
|
||||
:::
|
||||
|
||||
<img loading="lazy" style={{filter: 'invert(0.9)'}} src="./assets/ecs.png"/>
|
||||
|
||||
ReduxStore pseudo code
|
||||
```jsx pure
|
||||
const store = () => ({
|
||||
nodes: [{
|
||||
position: any
|
||||
form: any
|
||||
data3: any
|
||||
|
||||
}],
|
||||
edges: []
|
||||
})
|
||||
|
||||
function Playground() {
|
||||
const { nodes } = useStore(store)
|
||||
|
||||
return nodes.map(node => <Node data={node} />)
|
||||
}
|
||||
```
|
||||
Advantages:
|
||||
- Centralized data management is simple to use
|
||||
|
||||
Disadvantages:
|
||||
- Centralized data management cannot be precisely updated, leading to performance bottlenecks
|
||||
- Poor scalability, adding a new node data will couple to a large JSON
|
||||
|
||||
ECS Solution
|
||||
Notes:
|
||||
- NodeData corresponds to ECS - Component
|
||||
- Layer corresponds to ECS - System
|
||||
```jsx pure
|
||||
class FlowDocument {
|
||||
dataDefines: [
|
||||
NodePositionData,
|
||||
NodeFormData,
|
||||
NodeLineData
|
||||
]
|
||||
nodeEntities: Entity[] = []
|
||||
}
|
||||
|
||||
|
||||
class Entity {
|
||||
id: string // Only id, no data
|
||||
getData: (dataId: string) => EntityData
|
||||
}
|
||||
|
||||
// Render lines
|
||||
class LinesLayer {
|
||||
@observeEntityData(NodeLineData) lines
|
||||
render() {
|
||||
return lines.map(line => <Line data={line} />)
|
||||
}
|
||||
}
|
||||
|
||||
// Render node positions
|
||||
class NodePositionsLayer {
|
||||
@observeEntityData(NodePositionData) positions
|
||||
return() {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// Render node forms
|
||||
class NodeFormsLayer {
|
||||
@observeEntityData(NodeFormData) contents
|
||||
return() {}
|
||||
}
|
||||
|
||||
class Playground {
|
||||
layers: [
|
||||
LinesLayer, // Line rendering
|
||||
NodePositionsLayer, // Position rendering
|
||||
NodeFormsLayer // Content rendering
|
||||
],
|
||||
render() {
|
||||
return this.layers.map(layer => layer.render())
|
||||
}
|
||||
}
|
||||
```
|
||||
Advantages:
|
||||
- Node data is split separately for control rendering, performance can be done precisely updated
|
||||
- Strong scalability, adding a new node data, then adding a XXXData + XXXLayer
|
||||
|
||||
Disadvantages:
|
||||
- There is a learning cost
|
||||
@ -1,30 +0,0 @@
|
||||
# Node Engine
|
||||
|
||||
The Node Engine is a framework for writing flow node logic, allowing businesses to focus on their own rendering and data logic without concerning themselves with underlying canvas and node interaction APIs. Meanwhile, the node engine has established the best node writing paradigms, helping businesses solve various issues that may arise in flow business, such as coupling between data logic and rendering.
|
||||
The node engine is optional. If you don't have these complex node logic requirements, you can choose not to enable the node engine and maintain node data and rendering yourself. Complex node logic includes: 1) Nodes can validate or trigger data side effects without rendering; 2) Rich node interactions; 3) Redo/undo; etc.
|
||||
|
||||
# Basic Concepts
|
||||
|
||||
FlowNodeEntity
|
||||
Flow node model.
|
||||
|
||||
FlowNodeRegistry
|
||||
Static configuration of flow nodes.
|
||||
|
||||
FormMeta
|
||||
Static configuration of the node engine. Configured in the formMeta field of FlowNodeRegistry.
|
||||
|
||||
Form
|
||||
Forms in the node engine. It maintains node data and provides rendering, validation, side effects, and other capabilities. Its FormModel provides access to and modification of node data and triggers validation capabilities.
|
||||
|
||||
Field
|
||||
A rendering field in the node form. Note that Form already provides data layer logic, Field is more of a rendering layer model that only exists after form field rendering.
|
||||
|
||||
validate
|
||||
Form validation. Usually includes validation for individual fields and overall form validation.
|
||||
|
||||
effect
|
||||
Form data side effects. Usually refers to specific logic triggered when form data experiences certain events. For example, when a field's data changes and needs to synchronize information to a store, this can be called an effect.
|
||||
|
||||
FormPlugin
|
||||
Form plugins. Can be configured in formMeta, plugins can perform a series of deep operations on forms. Such as variable plugins.
|
||||
@ -1,78 +0,0 @@
|
||||
# Variable Engine
|
||||
|
||||
## Overall Design
|
||||
|
||||
### Architecture Layers
|
||||
|
||||
:::warning Architecture Layers
|
||||
The variable engine design follows the DIP (Dependency Inversion Principle) and is divided into three layers based on code stability, abstraction level, and proximity to business:
|
||||
- Variable Abstract Layer: The highest level of abstraction and most stable code in the variable architecture
|
||||
- Variable Implementation Layer: The more volatile part of the variable architecture, typically requiring adjustments between different business needs
|
||||
- Variable Business Layer: The Facade provided to business in the variable architecture, interacting with canvas engine and node engine
|
||||
:::
|
||||
|
||||

|
||||
|
||||
### Terminology
|
||||
|
||||
#### 🌟 Scope
|
||||
:::warning ⭐️⭐️⭐️ Definition:
|
||||
A conventional space where variable declarations and consumption are described through AST
|
||||
- Conventional space: The space is entirely defined by business
|
||||
- In low-code design state, it can be a node, a component, a right panel...
|
||||
- In code, it can be a Statement, a code block, a function, a file...
|
||||
:::
|
||||
|
||||
What is the scope space? It can be defined by different businesses.
|
||||
|
||||
#### 🌟 Abstract Syntax Tree (AST)
|
||||
|
||||
:::warning Definition:
|
||||
⭐️⭐️⭐️ A protocol that combines AST nodes in tree form to achieve explicit/implicit CRUD of variable information
|
||||
- AST nodes: Reactive protocol nodes in AST
|
||||
- Explicit CRUD, e.g.: Business explicitly sets a variable's type
|
||||
- Implicit CRUD, e.g.: Business declares a variable, and its type is automatically inferred from initialization parameters
|
||||
:::
|
||||
|
||||
:::warning Variables, types, expressions, structures, and other variable information in the scope are essentially combinations of AST nodes
|
||||
- Variable -> VariableDeclaration node
|
||||
- Expression -> Expression node
|
||||
- Type -> TypeNode node
|
||||
- Structure -> StructDeclaration node
|
||||
:::
|
||||
|
||||
Reference link: https://ts-ast-viewer.com/
|
||||
|
||||
#### Variable
|
||||
|
||||
:::warning Definition:
|
||||
An AST node used to declare new variables, using a unique identifier to point to a value that changes within a specific set range
|
||||
- Value changing within a specific set range: The variable's value must be within the range described by the variable type
|
||||
- Unique identifier: The variable must have a unique Key value
|
||||
:::
|
||||
|
||||
[Variables in JavaScript, unique Key + pointing to a changing value](./assets/variable-code.png)
|
||||
|
||||
#### Variable Type
|
||||
|
||||
:::warning Definition:
|
||||
⭐️⭐️⭐️ An AST node used to constrain a variable, where the constrained variable value can only change within a predetermined set range
|
||||
- A variable can be bound to a variable type
|
||||
:::
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td><img loading="lazy" src="./assets/variable-type1.png"/></td>
|
||||
<td><img loading="lazy" src="./assets/variable-type2.png"/></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
### Visual Understanding of the Variable Engine
|
||||
|
||||
:::warning Imagine a variable engine world like this:
|
||||
- Define countries through individual scopes
|
||||
- Each country contains three main citizens: declarations, types, and expressions
|
||||
- Countries communicate with each other through scope chains
|
||||
:::
|
||||
|
||||

|
||||
BIN
apps/docs/src/public/en-layer-uml.jpg
Normal file
|
After Width: | Height: | Size: 93 KiB |
BIN
apps/docs/src/public/en-varaible-zone.png
Normal file
|
After Width: | Height: | Size: 82 KiB |
BIN
apps/docs/src/public/en-variable-engine.png
Normal file
|
After Width: | Height: | Size: 556 KiB |
BIN
apps/docs/src/public/en-variable-type1.png
Normal file
|
After Width: | Height: | Size: 296 KiB |
BIN
apps/docs/src/public/en-weaving.png
Normal file
|
After Width: | Height: | Size: 196 KiB |
|
Before Width: | Height: | Size: 179 KiB After Width: | Height: | Size: 289 KiB |