bt builder progress

This commit is contained in:
IDONTSUDO 2024-02-05 15:28:09 +03:00
parent 5dec799002
commit acc79df97d
22 changed files with 664 additions and 85 deletions

View file

@ -7,7 +7,8 @@
"test:dev": "NODE_ENV=test_dev tsc-watch --onSuccess 'ts-node ./build/test/test.js'",
"test:unit": "NODE_ENV=unit tsc-watch --onSuccess 'ts-node ./build/test/test.js'",
"test:e2e": "NODE_ENV=e2e tsc-watch --onSuccess 'ts-node ./build/test/test.js'",
"dev": "NODE_ENV=dev tsc-watch --onSuccess 'ts-node ./build/src/main.js'"
"dev": "NODE_ENV=dev tsc-watch --onSuccess 'ts-node ./build/src/main.js'",
"build:stand": " "
},
"author": "IDONTSUDO",
"devDependencies": {
@ -27,7 +28,9 @@
"source-map-support": "latest",
"ts-node": "^10.9.1",
"tslint": "latest",
"typescript": "^5.1.6"
"typescript": "^5.1.6",
"node-watch": "^0.7.4",
"nodemon": "^3.0.1"
},
"dependencies": {
"@grpc/grpc-js": "^1.9.0",
@ -42,14 +45,13 @@
"md5": "^2.3.0",
"mongoose": "^7.6.2",
"mongoose-autopopulate": "^1.1.0",
"node-watch": "^0.7.4",
"nodemon": "^3.0.1",
"reflect-metadata": "^0.1.13",
"socket.io": "^4.7.2",
"socket.io-client": "^4.7.2",
"spark-md5": "^3.0.2",
"ts-md5": "^1.3.1",
"tsc-watch": "^6.0.4",
"uuid": "^9.0.1"
"uuid": "^9.0.1",
"pm2": "^5.3.1"
}
}

View file

@ -21,14 +21,21 @@
"mobx-react-lite": "^4.0.4",
"mobx-store-inheritance": "^1.0.6",
"react": "^18.2.0",
"react-accessible-treeview": "^2.8.3",
"react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^16.0.1",
"react-dom": "^18.2.0",
"react-i18next": "^13.3.1",
"react-infinite-scroll-component": "^6.1.0",
"react-router-dom": "^6.18.0",
"react-scripts": "5.0.1",
"reflect-metadata": "^0.1.13",
"rete-connection-plugin": "^2.0.0",
"rete-react-plugin": "^2.0.4",
"rete-render-utils": "^2.0.1",
"sass": "^1.66.1",
"socket.io-client": "^4.7.2",
"styled-components": "^6.1.8",
"three": "^0.159.0",
"three-stdlib": "^2.28.9",
"three-transform-controls": "^1.0.4",

View file

@ -4,6 +4,7 @@ import { StringExtensions } from "./string";
export type CallBackVoidFunction = <T>(value: T) => void;
export type CallBackStringVoidFunction = (value: string) => void;
export type CallBackEventTarget = (value: EventTarget) => void;
declare global {
interface Array<T> {

View file

@ -19,6 +19,8 @@ import {
GridHelper,
CameraHelper,
Quaternion,
MeshBasicMaterial,
PlaneGeometry,
} from "three";
import { TypedEvent } from "../helper/typed_event";
import { Result } from "../helper/result";
@ -244,6 +246,14 @@ export class CoreThreeRepository extends TypedEvent<BaseSceneItemModel> {
floor.userData = {};
floor.userData[UserData.cameraInitialization] = true;
this.scene.add(floor);
const planeMesh = new Mesh(
new PlaneGeometry(10, 10, 10, 10),
new MeshBasicMaterial({ color: 0x808080, wireframe: true })
);
planeMesh.userData[UserData.selectedObject] = true;
planeMesh.rotation.x = -Math.PI / 2;
this.scene.add(planeMesh);
}
render() {

View file

@ -1,5 +1,3 @@
// TODO(IDONTSUDO): нужно переписать все запросы под BaseStore
import { NavigateFunction } from "react-router-dom";
import { Result } from "../helper/result";
import { UiBaseError } from "../model/ui_base_error";

View file

@ -0,0 +1,15 @@
import * as React from "react";
export enum CoreTextType {
header,
}
export interface ITextProps {
text: string;
type: CoreTextType;
}
export function CoreText(props: ITextProps) {
if (props.type === CoreTextType.header) return <div style={{ color: "white", fontSize: "20px" }}>{props.text}</div>;
return <div>{props.text}</div>;
}

View file

@ -0,0 +1,106 @@
import * as React from "react";
import { CoreText, CoreTextType } from "../../../core/ui/text/text";
import { useRete } from "rete-react-plugin";
import { createEditor } from "./ui/editor/editor";
import { SkillTree } from "./ui/skill_tree/skill_tree";
import { BehaviorTreeBuilderStore } from "./behavior_tree_builder_store";
export const behaviorTreeBuilderScreenPath = "behavior/tree/screen/path";
const skills = {
name: "",
children: [
{
name: "arm",
children: [{ name: "move to", interface: "Vector3", out: "vo" }],
},
{
name: "image",
children: [
{
name: "object detection",
interface: "image",
out: "BoundBox",
},
{ name: "people detection", interface: "image", out: "BoundBox" },
],
},
],
};
export const behaviorTreeBuilderStore = new BehaviorTreeBuilderStore();
export function BehaviorTreeBuilderScreen() {
const [store] = React.useState(behaviorTreeBuilderStore);
const [ref] = useRete(createEditor);
React.useEffect(() => {
store.init();
store.dragZoneSetOffset(
// @ts-expect-error
ref.current.offsetTop,
// @ts-expect-error
ref.current.offsetLeft,
// @ts-expect-error
Number(String(ref.current.style.width).replaceAll("px", "")),
// @ts-expect-error
Number(String(ref.current.style.height).replaceAll("px", ""))
);
return () => {
store.dispose();
};
}, [store, ref]);
return (
<div>
<div
style={{
width: "100vw",
height: "86px",
background: "#041226",
}}
>
<CoreText text="Robossembler studio" type={CoreTextType.header} />
</div>
<div style={{ display: "flex" }}>
<div
style={{
width: "30vw",
height: "70px",
background: "#1B2E42",
}}
></div>
<div
style={{
width: "1782px",
height: "70px",
background: "#244366",
}}
></div>
</div>
<div style={{ display: "flex" }}>
<div
style={{
width: "30vw",
background: "#1B3041",
boxShadow: "inset 0px 10px 4px #16283D",
}}
>
<div style={{ overflow: "auto", height: "100%", width: "100%", padding: "10px" }}>
<SkillTree dragEnd={store.dragEnd} skills={skills} />
</div>
</div>
<div
ref={ref}
style={{
width: "1782px",
height: String(window.innerHeight - 86 - 70) + "px",
background: "#244366",
}}
></div>
</div>
</div>
);
}

View file

@ -0,0 +1,61 @@
import makeAutoObservable from "mobx-store-inheritance";
import { HttpError } from "../../../core/repository/http_repository";
import { UiErrorState } from "../../../core/store/base_store";
import { BtNodeView } from "./ui/editor/editor_view";
interface I2DArea {
x: number;
y: number;
w: number;
h: number;
}
export class BehaviorTreeBuilderStore extends UiErrorState<HttpError> {
area?: I2DArea;
errorHandingStrategy: (error: HttpError) => void;
btNodeView: BtNodeView = new BtNodeView();
constructor() {
super();
makeAutoObservable(this);
}
canRun = true;
draw() {}
dragEnd = (e: EventTarget) => {
if (this.canRun) {
//@ts-expect-error
this.drawSkillCheck(e.x as number, e.y as number, e.target.innerText as string);
this.canRun = false;
setTimeout(() => {
this.canRun = true;
}, 1000);
}
};
drawSkillCheck(x: number, y: number, name: string) {
const drawPoint = { x: x, y: y, w: 1, h: 1 };
if (
drawPoint.x < this.area!.x + this.area!.w &&
drawPoint.x + drawPoint.w > this.area!.x &&
drawPoint.y < this.area!.y + this.area!.h &&
drawPoint.y + drawPoint.h > this.area!.y
) {
this.drawSkill(x, y - (this.area!.y + this.area!.h / 2), name);
}
}
drawSkill(x: number, y: number, name: string) {
this.btNodeView.emit({
x: x,
y: y,
name: name,
});
}
async init(): Promise<any> {}
dragZoneSetOffset(offsetTop: number, offsetWidth: number, width: number, height: number) {
this.area = {
x: offsetTop,
y: offsetWidth,
w: width,
h: height,
};
}
dispose() {}
}

View file

@ -0,0 +1,19 @@
.fill-area {
display: table;
z-index: -1;
position: absolute;
top: -320000px;
left: -320000px;
width: 640000px;
height: 640000px;
}
.background {
background-color: #ffffff;
opacity: 1;
background-image: linear-gradient(#f1f1f1 3.2px, transparent 3.2px),
linear-gradient(90deg, #f1f1f1 3.2px, transparent 3.2px), linear-gradient(#f1f1f1 1.6px, transparent 1.6px),
linear-gradient(90deg, #f1f1f1 1.6px, #ffffff 1.6px);
background-size: 80px 80px, 80px 80px, 16px 16px, 16px 16px;
background-position: -3.2px -3.2px, -3.2px -3.2px, -1.6px -1.6px, -1.6px -1.6px;
}

View file

@ -0,0 +1,11 @@
import { BaseSchemes } from "rete";
import { AreaPlugin } from "rete-area-plugin";
export function addCustomBackground<S extends BaseSchemes, K>(area: AreaPlugin<S, K>) {
const background = document.createElement("div");
background.classList.add("background");
background.classList.add("fill-area");
area.area.content.add(background);
}

View file

@ -0,0 +1,36 @@
import * as React from "react";
import styled from "styled-components";
import { ClassicScheme, Presets } from "rete-react-plugin";
const { useConnection } = Presets.classic;
const Svg = styled.svg`
overflow: visible !important;
position: absolute;
pointer-events: none;
width: 9999px;
height: 9999px;
`;
const Path = styled.path<{ styles?: (props: any) => any }>`
fill: none;
stroke-width: 5px;
stroke: black;
pointer-events: auto;
${(props) => props.styles && props.styles(props)}
`;
export function CustomConnection(props: {
data: ClassicScheme["Connection"] & { isLoop?: boolean };
styles?: () => any;
}) {
const { path } = useConnection();
if (!path) return null;
return (
<Svg data-testid="connection">
<Path styles={props.styles} d={path} />
</Svg>
);
}

View file

@ -0,0 +1,164 @@
import * as React from "react";
import { ClassicScheme, RenderEmit, Presets } from "rete-react-plugin";
import styled, { css } from "styled-components";
import { $nodewidth, $socketmargin, $socketsize } from "./vars";
const { RefSocket, RefControl } = Presets.classic;
type NodeExtraData = { width?: number; height?: number };
export const NodeStyles = styled.div<NodeExtraData & { selected: boolean; styles?: (props: any) => any }>`
background: black;
border: 2px solid grey;
border-radius: 10px;
cursor: pointer;
box-sizing: border-box;
width: ${(props) => (Number.isFinite(props.width) ? `${props.width}px` : `${$nodewidth}px`)};
height: ${(props) => (Number.isFinite(props.height) ? `${props.height}px` : "auto")};
padding-bottom: 6px;
position: relative;
user-select: none;
&:hover {
background: #333;
}
${(props) =>
props.selected &&
css`
border-color: red;
`}
.title {
color: white;
font-family: sans-serif;
font-size: 18px;
padding: 8px;
}
.output {
text-align: right;
}
.input {
text-align: left;
}
.output-socket {
text-align: right;
margin-right: -1px;
display: inline-block;
}
.input-socket {
text-align: left;
margin-left: -1px;
display: inline-block;
}
.input-title,
.output-title {
vertical-align: middle;
color: white;
display: inline-block;
font-family: sans-serif;
font-size: 14px;
margin: ${$socketmargin}px;
line-height: ${$socketsize}px;
}
.input-control {
z-index: 1;
width: calc(100% - ${$socketsize + 2 * $socketmargin}px);
vertical-align: middle;
display: inline-block;
}
.control {
display: block;
padding: ${$socketmargin}px ${$socketsize / 2 + $socketmargin}px;
}
${(props) => props.styles && props.styles(props)}
`;
function sortByIndex<T extends [string, undefined | { index?: number }][]>(entries: T) {
entries.sort((a, b) => {
const ai = a[1]?.index || 0;
const bi = b[1]?.index || 0;
return ai - bi;
});
}
type Props<S extends ClassicScheme> = {
data: S["Node"] & NodeExtraData;
styles?: () => any;
emit: RenderEmit<S>;
};
export type NodeComponent<Scheme extends ClassicScheme> = (props: Props<Scheme>) => JSX.Element;
export function CustomNode<Scheme extends ClassicScheme>(props: Props<Scheme>) {
const inputs = Object.entries(props.data.inputs);
const outputs = Object.entries(props.data.outputs);
const controls = Object.entries(props.data.controls);
const selected = props.data.selected || false;
const { id, label, width, height } = props.data;
sortByIndex(inputs);
sortByIndex(outputs);
sortByIndex(controls);
return (
<NodeStyles selected={selected} width={width} height={height} styles={props.styles} data-testid="node">
<div
onPointerDown={(e) => {
e.stopPropagation();
console.log(">>>");
}}
className="title"
data-testid="title"
>
{label}
</div>
{outputs.map(
([key, output]) =>
output && (
<div className="output" key={key} data-testid={`output-${key}`}>
<div style={{ color: "white" }}>BODY</div>
<div className="output-title" data-testid="output-title">
{output?.label}
</div>
<RefSocket
name="output-socket"
side="output"
emit={props.emit}
socketKey={key}
nodeId={id}
payload={output.socket}
/>
</div>
)
)}
{controls.map(([key, control]) => {
return control ? <RefControl key={key} name="control" emit={props.emit} payload={control} /> : null;
})}
{inputs.map(
([key, input]) =>
input && (
<div className="input" key={key} data-testid={`input-${key}`}>
<RefSocket
name="input-socket"
emit={props.emit}
side="input"
socketKey={key}
nodeId={id}
payload={input.socket}
/>
{input && (!input.control || !input.showControl) && (
<div className="input-title" data-testid="input-title">
{input?.label}
</div>
)}
{input?.control && input?.showControl && (
<span className="input-control">
<RefControl key={key} name="input-control" emit={props.emit} payload={input.control} />
</span>
)}
</div>
)
)}
</NodeStyles>
);
}

View file

@ -0,0 +1,23 @@
import * as React from "react";
import { ClassicPreset } from "rete";
import styled from "styled-components";
import { $socketsize } from "./vars";
const Styles = styled.div`
display: inline-block;
cursor: pointer;
border: 1px solid grey;
width: ${$socketsize}px;
height: ${$socketsize * 2}px;
vertical-align: middle;
background: #fff;
z-index: 2;
box-sizing: border-box;
&:hover {
background: #ddd;
}
`;
export function CustomSocket<T extends ClassicPreset.Socket>(props: { data: T }) {
return <Styles title={props.data.name} />;
}

View file

@ -0,0 +1,88 @@
import { createRoot } from "react-dom/client";
import { NodeEditor, GetSchemes, ClassicPreset } from "rete";
import { AreaPlugin, AreaExtensions } from "rete-area-plugin";
import { ConnectionPlugin, Presets as ConnectionPresets } from "rete-connection-plugin";
import { ReactPlugin, Presets, ReactArea2D } from "rete-react-plugin";
import { CustomNode } from "./custom_node";
import { CustomSocket } from "./custom_socket";
import { CustomConnection } from "./custom_connection";
import { addCustomBackground } from "./custom_background";
import { StyledNode } from "./style_node";
import { behaviorTreeBuilderStore } from "../../behavior_tree_builder_screen";
type Schemes = GetSchemes<ClassicPreset.Node, ClassicPreset.Connection<ClassicPreset.Node, ClassicPreset.Node>>;
type AreaExtra = ReactArea2D<Schemes>;
export async function createEditor(container: HTMLElement) {
const socket = new ClassicPreset.Socket("socket");
behaviorTreeBuilderStore.btNodeView.on(async (event) => {
setTimeout(async () => {
const node = new ClassicPreset.Node(event.name);
const { x, y } = areaContainer.area.pointer;
await editor.addNode(node);
await areaContainer.translate(node.id, { x, y });
}, 50);
});
const editor = new NodeEditor<Schemes>();
const areaContainer = new AreaPlugin<Schemes, AreaExtra>(container);
const connection = new ConnectionPlugin<Schemes, AreaExtra>();
const render = new ReactPlugin<Schemes, AreaExtra>({ createRoot });
AreaExtensions.selectableNodes(areaContainer, AreaExtensions.selector(), {
accumulating: AreaExtensions.accumulateOnCtrl(),
});
render.addPreset(
Presets.classic.setup({
customize: {
node(context) {
if (context.payload.label === "Fully customized") {
return CustomNode;
}
if (context.payload.label === "Override styles") {
return StyledNode;
}
return Presets.classic.Node;
},
socket(_context) {
return CustomSocket;
},
connection(_context) {
return CustomConnection;
},
},
})
);
connection.addPreset(ConnectionPresets.classic.setup());
addCustomBackground(areaContainer);
editor.use(areaContainer);
areaContainer.use(connection);
areaContainer.use(render);
AreaExtensions.simpleNodesOrder(areaContainer);
const a = new ClassicPreset.Node("Override styles");
a.addOutput("a", new ClassicPreset.Output(socket));
a.addInput("a", new ClassicPreset.Input(socket));
await editor.addNode(a);
const b = new ClassicPreset.Node("Fully customized");
b.addOutput("a", new ClassicPreset.Output(socket));
b.addInput("a", new ClassicPreset.Input(socket));
await editor.addNode(b);
await editor.addConnection(new ClassicPreset.Connection(a, "a", b, "a"));
setTimeout(() => {
AreaExtensions.zoomAt(areaContainer, editor.getNodes());
}, 100);
return {
destroy: () => areaContainer.destroy(),
};
}

View file

@ -0,0 +1,7 @@
import { TypedEvent } from "../../../../../core/helper/typed_event";
export interface BtDrawView {
x: number;
y: number;
name: string;
}
export class BtNodeView extends TypedEvent<BtDrawView> {}

View file

@ -0,0 +1,30 @@
import { Presets } from "rete-react-plugin";
import { css } from "styled-components";
import "./background.css";
const styles = css<{ selected?: boolean }>`
background: #ebebeb;
border-color: #646464;
.title {
color: #646464;
}
&:hover {
background: #f2f2f2;
}
.output-socket {
margin-right: -1px;
}
.input-socket {
margin-left: -1px;
}
${(props) =>
props.selected &&
css`
border-color: red;
`}
`;
export function StyledNode(props: any) {
// eslint-disable-next-line react/jsx-pascal-case
return <Presets.classic.Node styles={() => styles} {...props} />;
}

View file

@ -0,0 +1,3 @@
export const $nodewidth = 200;
export const $socketmargin = 6;
export const $socketsize = 16;

View file

@ -0,0 +1,69 @@
import * as React from "react";
import TreeView, { EventCallback, IBranchProps, INode, LeafProps, flattenTree } from "react-accessible-treeview";
import { IFlatMetadata } from "react-accessible-treeview/dist/TreeView/utils";
import { CallBackEventTarget } from "../../../../../core/extensions/extensions";
export interface ISkillTreeProps {
skills: any;
dragEnd: CallBackEventTarget;
}
interface IRefListerProps {
element: INode<IFlatMetadata>;
getNodeProps: (args?: { onClick?: EventCallback }) => IBranchProps | LeafProps;
handleExpand: EventCallback;
handleSelect: EventCallback;
dragEnd: CallBackEventTarget;
}
export const RefListener = (props: IRefListerProps) => {
const canvasRef = React.useRef<HTMLDataElement>(null);
React.useEffect(() => {
if (canvasRef.current) {
canvasRef.current.addEventListener("dragend", (e) => {
// @ts-expect-error
if (e.target.innerHTML) {
// @ts-expect-error
props.dragEnd(e);
}
});
}
}, [canvasRef, props]);
return (
<div {...props.getNodeProps({ onClick: props.handleExpand })}>
<div
onClick={(e) => {
props.handleSelect(e);
}}
/>
<span ref={canvasRef} style={{ color: "white" }} draggable="true">
{props.element.name}
</span>
</div>
);
};
export function SkillTree(props: ISkillTreeProps) {
return (
<div>
<TreeView
data={flattenTree(props.skills)}
aria-label="onSelect"
onSelect={() => {}}
multiSelect
defaultExpandedIds={[1, 2]}
nodeAction="check"
nodeRenderer={({ element, getNodeProps, handleSelect, handleExpand }) => {
return (
<RefListener
dragEnd={props.dragEnd}
getNodeProps={getNodeProps}
element={element}
handleExpand={handleExpand}
handleSelect={handleSelect}
/>
);
}}
/>
</div>
);
}

View file

@ -1,68 +0,0 @@
export {};
// import React from "react";
// import { CoreError, UiErrorState } from "../core/store/base_store";
// import { SelectProjectStore } from "./select_project/presentation/select_project_store";
// export declare type ClassConstructor<T> = {
// new (...args: any[]): T;
// };
// interface MobxReactComponentProps<T extends UiErrorState<CoreError>, ClassConstructor> {
// store: ClassConstructor;
// children: (element: T) => React.ReactElement;
// }
// class UiStateErrorComponent<T extends UiErrorState<CoreError>, K> extends React.Component<
// MobxReactComponentProps<T, K>,
// { store: T | undefined }
// > {
// async componentDidMount(): Promise<void> {
// const store = this.props.store as ClassConstructor<T>;
// console.log(store);
// const s = new store();
// this.setState({ store: s });
// if (this.state !== null) {
// await this.state.store?.init();
// }
// }
// componentWillUnmount(): void {
// if (this.state.store !== undefined) {
// this.state.store.dispose();
// }
// }
// render() {
// if (this.state !== null) {
// if (this.state.store?.isLoading) {
// return <>Loading</>;
// }
// if (this.state.store !== undefined) {
// return this.props.children(this.state.store);
// }
// }
// return (
// <div>
// <>{this.props.children}</>
// </div>
// );
// }
// }
// export const ExampleScreen: React.FC = () => {
// return (
// <div>
// <UiStateErrorComponent<SelectProjectStore, {}> store={SelectProjectStore}>
// {(store) => {
// console.log(store);
// return (
// <div>
// {store.projects.map((el) => {
// return <>{el}</>;
// })}
// </div>
// );
// }}
// </UiStateErrorComponent>
// </div>
// );
// };

View file

@ -24,4 +24,5 @@ export enum SceneMode {
MOVING = "Moving",
EMPTY = "Empty",
ADD_CAMERA = "Add camera",
MAGNETISM_MARKING = "magnetism_marking",
}

View file

@ -26,8 +26,8 @@ export const SceneManger = observer(() => {
};
}, [id, sceneMangerStore]);
const sceneIcons: SceneManagerView[] = [SceneMode.ROTATE, SceneMode.MOVING, SceneMode.ADD_CAMERA].map((el) => {
return { name: el, clickHandel: () => sceneMangerStore.setSceneMode(el) };
const sceneIcons: SceneManagerView[] = Object.values(SceneMode).map((el) => {
return { name: el, clickHandel: () => sceneMangerStore.setSceneMode(el as SceneMode) };
});
return (
@ -148,14 +148,7 @@ export const SceneManger = observer(() => {
return <StaticAssetModelView onTap={() => sceneMangerStore.deleteSceneItem(el)} model={el} />;
})}
</div>
{/* {sceneMangerStore.sceneMenuIsShow ? (
<>
<SceneMenu x={sceneMangerStore.sceneMenu.x} y={sceneMangerStore.sceneMenu.y} />
</>
) : (
<></>
)} */}
<div>{sceneMangerStore.sceneMode === SceneMode.MAGNETISM_MARKING ? <></> : <></>}</div>
</div>
</div>
);

View file

@ -9,6 +9,7 @@ import { SocketLister } from "./features/socket_lister/socket_lister";
import { RouterProvider } from "react-router-dom";
import { router } from "./core/routers/routers";
import { SceneManger } from "./features/scene_manager/presentation/scene_manager";
import { BehaviorTreeBuilderScreen } from "./features/behavior_tree_builder/presentation/behavior_tree_builder_screen";
extensions();
const root = ReactDOM.createRoot(document.getElementById("root") as HTMLElement);
@ -21,6 +22,8 @@ root.render(
{/* </SocketLister> */}
<>
<SceneManger></SceneManger>
{/* <BehaviorTreeBuilderScreen /> */}
</>
</>
);