From e455a18a2f53588b7a03a933854dfb246d90adb5 Mon Sep 17 00:00:00 2001 From: Zixuan Chen Date: Fri, 3 Jun 2022 23:12:11 +0800 Subject: [PATCH] feat: draw mind map --- package-lock.json | 31 +++++++- package.json | 3 +- rust/Cargo.lock | 1 + rust/crates/tidy-tree/src/lib.rs | 14 +++- rust/crates/wasm/Cargo.toml | 1 + rust/crates/wasm/src/lib.rs | 44 ++++++----- rust/crates/wasm/src/linked_list.rs | 69 ------------------ src/App.tsx | 5 +- src/TidyComponent.tsx | 32 ++++++++ src/dispose.ts | 44 +++++++++++ src/renderer.ts | 68 +++++++++++++++++ src/stories/Tidy.stories.tsx | 54 ++++++++++++++ src/tidy.ts | 109 ++++++++++++++++++++++++++++ src/utils.ts | 9 +++ src/wasmEntry.ts | 6 -- 15 files changed, 388 insertions(+), 102 deletions(-) delete mode 100644 rust/crates/wasm/src/linked_list.rs create mode 100644 src/TidyComponent.tsx create mode 100644 src/dispose.ts create mode 100644 src/renderer.ts create mode 100644 src/stories/Tidy.stories.tsx create mode 100644 src/tidy.ts create mode 100644 src/utils.ts delete mode 100644 src/wasmEntry.ts diff --git a/package-lock.json b/package-lock.json index 669ca5b..de33dbd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,8 @@ "license": "ISC", "dependencies": { "react": "^17.0.2", - "react-dom": "^17.0.2" + "react-dom": "^17.0.2", + "zrender": "^5.3.1" }, "devDependencies": { "@babel/core": "^7.18.2", @@ -20684,6 +20685,19 @@ "node": ">=10" } }, + "node_modules/zrender": { + "version": "5.3.1", + "resolved": "https://registry.npmmirror.com/zrender/-/zrender-5.3.1.tgz", + "integrity": "sha512-7olqIjy0gWfznKr6vgfnGBk7y4UtdMvdwFmK92vVQsQeDPyzkHW1OlrLEKg6GHz1W5ePf0FeN1q2vkl/HFqhXw==", + "dependencies": { + "tslib": "2.3.0" + } + }, + "node_modules/zrender/node_modules/tslib": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.3.0.tgz", + "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==" + }, "node_modules/zwitch": { "version": "1.0.5", "resolved": "https://registry.npmmirror.com/zwitch/-/zwitch-1.0.5.tgz", @@ -37223,6 +37237,21 @@ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "dev": true }, + "zrender": { + "version": "5.3.1", + "resolved": "https://registry.npmmirror.com/zrender/-/zrender-5.3.1.tgz", + "integrity": "sha512-7olqIjy0gWfznKr6vgfnGBk7y4UtdMvdwFmK92vVQsQeDPyzkHW1OlrLEKg6GHz1W5ePf0FeN1q2vkl/HFqhXw==", + "requires": { + "tslib": "2.3.0" + }, + "dependencies": { + "tslib": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.3.0.tgz", + "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==" + } + } + }, "zwitch": { "version": "1.0.5", "resolved": "https://registry.npmmirror.com/zwitch/-/zwitch-1.0.5.tgz", diff --git a/package.json b/package.json index 1a7dfa8..93a9e4b 100644 --- a/package.json +++ b/package.json @@ -59,6 +59,7 @@ }, "dependencies": { "react": "^17.0.2", - "react-dom": "^17.0.2" + "react-dom": "^17.0.2", + "zrender": "^5.3.1" } } diff --git a/rust/Cargo.lock b/rust/Cargo.lock index c6de382..79d3545 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -217,6 +217,7 @@ checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" name = "wasm" version = "0.1.0" dependencies = [ + "tidy-tree", "wasm-bindgen", ] diff --git a/rust/crates/tidy-tree/src/lib.rs b/rust/crates/tidy-tree/src/lib.rs index 336e0d8..b059fba 100644 --- a/rust/crates/tidy-tree/src/lib.rs +++ b/rust/crates/tidy-tree/src/lib.rs @@ -63,10 +63,16 @@ impl TidyTree { self.layout.layout(&mut self.root); } - pub fn get_pos(&self, id: usize) -> Option<(Coord, Coord)> { - let node = self.map.get(&id)?; - let node = unsafe { node.as_ref() }; - Some((node.x, node.y)) + pub fn get_pos(&self) -> Vec { + let mut ans = vec![]; + for (id, node) in self.map.iter() { + let node = unsafe { node.as_ref() }; + ans.push((*id) as Coord); + ans.push(node.x); + ans.push(node.y); + } + + ans } } diff --git a/rust/crates/wasm/Cargo.toml b/rust/crates/wasm/Cargo.toml index b9f4789..a1c1e87 100644 --- a/rust/crates/wasm/Cargo.toml +++ b/rust/crates/wasm/Cargo.toml @@ -9,3 +9,4 @@ crate-type = ["cdylib"] [dependencies] wasm-bindgen = "0.2.74" +tidy-tree = { path = "../tidy-tree" } diff --git a/rust/crates/wasm/src/lib.rs b/rust/crates/wasm/src/lib.rs index a8413c0..c87f953 100644 --- a/rust/crates/wasm/src/lib.rs +++ b/rust/crates/wasm/src/lib.rs @@ -1,29 +1,37 @@ -mod linked_list; extern crate wasm_bindgen; +use tidy_tree::{geometry::Coord, TidyTree, NULL_ID}; use wasm_bindgen::prelude::*; #[wasm_bindgen] -extern "C" { - pub fn alert(s: &str); -} +pub struct Tidy(TidyTree); #[wasm_bindgen] -pub fn greet(name: &str) { - unsafe { - alert(&format!("Hello, {}!", name)); +impl Tidy { + pub fn null_id() -> usize { + NULL_ID } -} -#[wasm_bindgen] -pub fn sum_of_squares(input: &[i32]) -> i32 { - input.iter().map(|x| x * x).sum() -} + pub fn with_basic_layout() -> Self { + Tidy(TidyTree::with_basic_layout()) + } + + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + pub fn add_node(&mut self, id: usize, width: Coord, height: Coord, parent_id: usize) { + self.0.add_node(id, width, height, parent_id); + } + + pub fn data(&mut self, id: &[usize], width: &[Coord], height: &[Coord], parent_id: &[usize]) { + self.0.data(id, width, height, parent_id); + } + + pub fn layout(&mut self) { + self.0.layout(); + } -#[cfg(test)] -mod tests { - #[test] - fn it_works() { - let result = 2 + 2; - assert_eq!(result, 4); + pub fn get_pos(&self) -> Vec { + self.0.get_pos() } } diff --git a/rust/crates/wasm/src/linked_list.rs b/rust/crates/wasm/src/linked_list.rs deleted file mode 100644 index 7659b66..0000000 --- a/rust/crates/wasm/src/linked_list.rs +++ /dev/null @@ -1,69 +0,0 @@ -use wasm_bindgen::prelude::*; - -#[wasm_bindgen] -struct Node { - value: i32, - next: Option>, -} - -#[wasm_bindgen] -pub struct LinkedList { - head: Option>, - len: usize, -} - -#[wasm_bindgen] -impl LinkedList { - #[wasm_bindgen(constructor)] - pub fn new() -> Self { - LinkedList { head: None, len: 0 } - } - - pub fn push(&mut self, value: i32) { - let new_node = Box::new(Node { value, next: None }); - match self.head { - None => { - self.head = Some(new_node); - } - Some(ref mut _node) => { - let mut node: *mut Box = _node; - unsafe { - while let Some(next) = &mut (*node).next { - node = next; - } - - (*node).next = Some(new_node); - } - } - } - - self.len += 1; - } - - pub fn pop(&mut self) -> Option { - self.head.take().map(|node| { - self.head = node.next; - self.len -= 1; - node.value - }) - } -} - -#[cfg(test)] -mod test { - use crate::linked_list::LinkedList; - - #[test] - fn test_link() { - let mut link: LinkedList = LinkedList::new(); - link.push(1); - link.push(2); - link.push(3); - link.push(4); - assert!(link.pop() == Some(1)); - assert!(link.pop() == Some(2)); - assert!(link.pop() == Some(3)); - assert!(link.pop() == Some(4)); - assert!(link.pop() == None); - } -} diff --git a/src/App.tsx b/src/App.tsx index 56b29b3..255e24d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,12 +1,11 @@ import { useEffect, useState } from 'react'; -import { sumOfSquares, init } from './wasmEntry'; +import { TidyLayout } from './tidy'; function App() { const [count, setCount] = useState(0); useEffect(() => { (async () => { - await init(); - console.log(sumOfSquares(1, 2)); + console.log(); })(); }, []); diff --git a/src/TidyComponent.tsx b/src/TidyComponent.tsx new file mode 100644 index 0000000..d4a1566 --- /dev/null +++ b/src/TidyComponent.tsx @@ -0,0 +1,32 @@ +import { useEffect, useRef, useState } from 'react'; +import { Renderer } from './renderer'; +import { InnerNode, LayoutType, Node, TidyLayout } from './tidy'; + +interface Props { + root: Node; + layoutType?: LayoutType; +} + +export const TidyComponent = ({ root, layoutType }: Props) => { + const renderRef = useRef(); + const containerRef = useRef(null); + const layoutRef = useRef(); + useEffect(() => { + const func = async () => { + renderRef.current = new Renderer(containerRef.current!); + layoutRef.current = await TidyLayout.create(layoutType); + const innerRoot = layoutRef.current.set_root(root); + layoutRef.current.layout(); + renderRef.current.init(innerRoot); + // TODO: Draw + }; + + func(); + return () => { + renderRef.current?.dispose(); + }; + }, []); + useEffect(() => {}, [root]); + + return
; +}; diff --git a/src/dispose.ts b/src/dispose.ts new file mode 100644 index 0000000..9f9ff14 --- /dev/null +++ b/src/dispose.ts @@ -0,0 +1,44 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +export interface IDisposable { + dispose(): void; +} + +export function disposeAll(disposables: IDisposable[]) { + while (disposables.length) { + const item = disposables.pop(); + if (item) { + item.dispose(); + } + } +} + +export abstract class Disposable { + private _isDisposed = false; + + protected _disposables: IDisposable[] = []; + + public dispose(): any { + if (this._isDisposed) { + return; + } + this._isDisposed = true; + disposeAll(this._disposables); + } + + protected _register(value: T): T { + if (this._isDisposed) { + value.dispose(); + } else { + this._disposables.push(value); + } + return value; + } + + protected get isDisposed() { + return this._isDisposed; + } +} diff --git a/src/renderer.ts b/src/renderer.ts new file mode 100644 index 0000000..6c50b01 --- /dev/null +++ b/src/renderer.ts @@ -0,0 +1,68 @@ +import { Rect, ZRenderType, dispose, init, BezierCurve, Group } from 'zrender'; +import { Disposable } from './dispose'; +import { InnerNode } from './tidy'; +import { visit } from './utils'; + +export class Renderer extends Disposable { + private render: ZRenderType; + private root: InnerNode | undefined; + private rectMap: Map = new Map(); + private lineFromMap: Map = new Map(); + private lineToMap: Map = new Map(); + constructor(container: HTMLElement) { + super(); + this.render = init(container); + this._register({ + dispose: () => { + dispose(this.render); + }, + }); + } + + init(root: InnerNode) { + this.root = root; + const g = new Group(); + this.render.add(g); + g.setPosition([this.render.getWidth() / 2, 12]); + g.setScale([1.2, 1.2]); + visit(root, (node) => { + const rect = new Rect({ + shape: { + x: node.x - node.width / 2, + y: node.y, + width: node.width, + height: node.height, + r: 4, + }, + style: { + stroke: '#2b5de9', + fill: '#a8bbf0', + }, + }); + this.rectMap.set(node.id, rect); + g.add(rect); + + for (const child of node.children) { + const line = new BezierCurve({ + shape: { + x1: node.x, + y1: node.y + node.height, + x2: child.x, + y2: child.y, + cpx1: node.x, + cpy1: (child.y + node.y + node.height) / 2, + cpx2: child.x, + cpy2: (child.y + node.y + node.height) / 2, + }, + style: { + stroke: '#2b5de9', + }, + }); + + g.add(line); + this.lineFromMap.set(node.id, line); + this.lineToMap.set(child.id, line); + } + }); + } +} diff --git a/src/stories/Tidy.stories.tsx b/src/stories/Tidy.stories.tsx new file mode 100644 index 0000000..3afcd47 --- /dev/null +++ b/src/stories/Tidy.stories.tsx @@ -0,0 +1,54 @@ +import React from 'react'; +import { LayoutType, Node } from '../tidy'; +import { TidyComponent } from '../TidyComponent'; +import { ComponentStory, ComponentMeta } from '@storybook/react'; + +export default { + title: 'Tidy', + component: TidyComponent, +} as ComponentMeta; + +interface Props { + layoutType?: LayoutType; + root: Node; +} + +/** + * Primary UI component for user interaction + */ +export const TidyLayout = ({ root, layoutType, ...props }: Props) => { + return ; +}; + +TidyLayout.args = { + root: createTree(100) as Node, +}; + +function createNode(): Node { + return { + id: (Math.random() * 1e9) | 0, + height: 20 * Math.random() + 10, + width: 20 * Math.random() + 10, + x: 0, + y: 0, + children: [], + }; +} + +function createTree(num: number): Node { + const root = createNode(); + const arr = [root]; + for (let i = 0; i < num; i++) { + const child = createNode(); + const parentIndex = (arr.length * Math.random()) | 0; + const parent = arr[parentIndex]; + parent.children.push(child); + child.parentId = parent.id; + arr.push(child); + if (parent.children.length > 5) { + arr.splice(parentIndex, 1); + } + } + + return root; +} diff --git a/src/tidy.ts b/src/tidy.ts new file mode 100644 index 0000000..d565ac1 --- /dev/null +++ b/src/tidy.ts @@ -0,0 +1,109 @@ +import initWasm, { Tidy, Tidy as TidyWasm } from '../wasm_dist/wasm'; +import { Disposable } from './dispose'; + +export enum LayoutType { + Basic = 'basic', +} + +export { initWasm }; + +export interface Node { + id?: number; + width: number; + height: number; + parentId?: number; + x: number; + y: number; + children: Node[]; +} + +export interface InnerNode { + id: number; + width: number; + height: number; + parentId?: number; + x: number; + y: number; + children: InnerNode[]; +} + +let nullId = -1; +const NULL_ID = () => { + if (nullId === -1) { + nullId = Tidy.null_id(); + } + return nullId; +}; +export class TidyLayout extends Disposable { + private tidy: TidyWasm; + private nextId = 1; + private root: InnerNode | undefined; + private idToNode: Map = new Map(); + static async create(type: LayoutType = LayoutType.Basic) { + await initWasm(); + return new TidyLayout(type); + } + + private constructor(type: LayoutType = LayoutType.Basic) { + super(); + if (type === LayoutType.Basic) { + this.tidy = TidyWasm.with_basic_layout(); + } else { + throw new Error('not implemented'); + } + this._register({ + dispose: () => { + this.tidy.free(); + }, + }); + } + + layout() { + this.tidy.layout(); + const positions = this.tidy.get_pos(); + for (let i = 0; i < positions.length; i += 3) { + const id = positions[i] | 0; + const node = this.idToNode.get(id)!; + node.x = positions[i + 1]; + node.y = positions[i + 2]; + } + } + + set_root(root: Node): InnerNode { + //TODO: Free old nodes + const stack = [root]; + const ids: number[] = []; + const width: number[] = []; + const height: number[] = []; + const parents: number[] = []; + while (stack.length) { + const node = stack.pop()!; + if (node.id == null) { + node.id = this.nextId++; + } + + ids.push(node.id!); + width.push(node.width); + height.push(node.height); + parents.push(node.parentId ?? NULL_ID()); + this.idToNode.set(node.id!, node as InnerNode); + for (const child of node.children) { + if (child.parentId == null) { + child.parentId = node.id; + } + + stack.push(child); + } + } + + this.root = root as InnerNode; + this.tidy.data( + new Uint32Array(ids), + new Float64Array(width), + new Float64Array(height), + new Uint32Array(parents), + ); + + return this.root; + } +} diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..8b88b6c --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,9 @@ +export function visit( + node: T, + func: (node: T) => void, +) { + func(node); + for (const child of node.children) { + visit(child, func); + } +} diff --git a/src/wasmEntry.ts b/src/wasmEntry.ts deleted file mode 100644 index 764b3c0..0000000 --- a/src/wasmEntry.ts +++ /dev/null @@ -1,6 +0,0 @@ -import init, { sum_of_squares, LinkedList } from '../wasm_dist/wasm'; - -export { init, LinkedList }; -export function sumOfSquares(...arr: number[]) { - return sum_of_squares(new Int32Array(arr)); -}