Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

try state nesting #49

Merged
merged 18 commits into from
Nov 14, 2023
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,557 changes: 838 additions & 719 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
"electron-settings": "^4.0.2",
"find-free-port": "^2.0.0",
"isomorphic-ws": "^5.0.0",
"lodash.throttle": "^4.1.1",
"lodash.debounce": "^4.0.8",
"monaco-editor": "^0.39.0",
"monaco-editor-webpack-plugin": "^7.1.0",
Expand All @@ -62,6 +63,7 @@
"devDependencies": {
"@electron-toolkit/tsconfig": "^1.0.1",
"@electron/notarize": "^1.2.3",
"@types/lodash.throttle": "^4.1.8",
"@types/lodash.debounce": "^4.0.8",
"@types/node": "^18.16.16",
"@types/react": "^18.2.8",
Expand Down
3 changes: 1 addition & 2 deletions src/renderer/src/components/DiagramEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,7 @@ export const DiagramEditor: React.FC<DiagramEditorProps> = ({ manager, editor, s
setNewTransition(undefined);
};

//Перетаскиваем компонент в редактор
editor.container.on('stateDrop', (position) => {
editor.container.on('dblclick', (position) => {
editor?.container.machineController.createState({
name: 'Состояние',
position,
Expand Down
57 changes: 33 additions & 24 deletions src/renderer/src/lib/basic/Container.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { getColor } from '@renderer/theme';
import { getCapturedNodeArgs } from '@renderer/types/drawable';
import { Point } from '@renderer/types/graphics';
import { MyMouseEvent } from '@renderer/types/mouse';

Expand All @@ -21,7 +22,7 @@ export const MIN_SCALE = 0.2;
* управление камерой, обработка событий и сериализация.
*/
interface ContainerEvents {
stateDrop: Point;
dblclick: Point;
contextMenu: Point;
}

Expand Down Expand Up @@ -60,6 +61,8 @@ export class Container extends EventEmitter<ContainerEvents> {
draw(ctx: CanvasRenderingContext2D, canvas: HTMLCanvasElement) {
this.drawGrid(ctx, canvas);

console.log(this.children);

const drawChildren = (node: Container | Node) => {
node.children.forEach((child) => {
child.draw(ctx, canvas);
Expand Down Expand Up @@ -107,8 +110,9 @@ export class Container extends EventEmitter<ContainerEvents> {
}

private initEvents() {
this.app.canvas.element.addEventListener('dragover', (e) => e.preventDefault());
this.app.canvas.element.addEventListener('drop', this.handleDrop);
// ! Это на будущее
// this.app.canvas.element.addEventListener('dragover', (e) => e.preventDefault());
// this.app.canvas.element.addEventListener('drop', this.handleDrop);

this.app.keyboard.on('spacedown', this.handleSpaceDown);
this.app.keyboard.on('spaceup', this.handleSpaceUp);
Expand All @@ -128,11 +132,15 @@ export class Container extends EventEmitter<ContainerEvents> {
this.app.mouse.on('rightclick', this.handleRightMouseClick);
}

private getCapturedNode(position: Point) {
const end = this.children.size - 1;
getCapturedNode(args: getCapturedNodeArgs) {
const { type } = args;

const end = type === 'states' ? this.children.statesSize : this.children.size;

for (let i = end; i >= 0; i--) {
const node = this.children.getByIndex(i)?.getIntersection(position);
for (let i = end - 1; i >= 0; i--) {
const node = (
type === 'states' ? this.children.getStateByIndex(i) : this.children.getByIndex(i)
)?.getIntersection(args);

if (node) return node;
}
Expand All @@ -148,24 +156,25 @@ export class Container extends EventEmitter<ContainerEvents> {
this.isDirty = true;
}

handleDrop = (e: DragEvent) => {
e.preventDefault();
// ! Это на будущее
// handleDrop = (e: DragEvent) => {
// e.preventDefault();

const rect = this.app.canvas.element.getBoundingClientRect();
const scale = this.app.manager.data.scale;
const offset = this.app.manager.data.offset;
const position = {
x: (e.clientX - rect.left) * scale - offset.x,
y: (e.clientY - rect.top) * scale - offset.y,
};
// const rect = this.app.canvas.element.getBoundingClientRect();
// const scale = this.app.manager.data.scale;
// const offset = this.app.manager.data.offset;
// const position = {
// x: (e.clientX - rect.left) * scale - offset.x,
// y: (e.clientY - rect.top) * scale - offset.y,
// };

this.emit('stateDrop', position);
};
// this.emit('stateDrop', position);
// };

handleMouseDown = (e: MyMouseEvent) => {
if (!e.left || this.isPan) return;

const node = this.getCapturedNode(e);
const node = this.getCapturedNode({ position: e });

if (node) {
node.handleMouseDown(e);
Expand All @@ -189,7 +198,7 @@ export class Container extends EventEmitter<ContainerEvents> {
return;
}

const node = this.getCapturedNode(e);
const node = this.getCapturedNode({ position: e });

if (node) {
node.handleMouseUp(e);
Expand All @@ -200,7 +209,7 @@ export class Container extends EventEmitter<ContainerEvents> {
};

handleRightMouseClick = (e: MyMouseEvent) => {
const node = this.getCapturedNode(e);
const node = this.getCapturedNode({ position: e });

if (node) {
node.handleMouseContextMenu(e);
Expand All @@ -213,7 +222,7 @@ export class Container extends EventEmitter<ContainerEvents> {
if (e.left) this.handleLeftMouseMove(e);
if (e.right) this.handleRightMouseMove(e);

this.isDirty = true;
if (e.left || e.right) this.isDirty = true;
};

private handleLeftMouseMove(e: MyMouseEvent) {
Expand All @@ -238,12 +247,12 @@ export class Container extends EventEmitter<ContainerEvents> {
}

handleMouseDoubleClick = (e: MyMouseEvent) => {
const node = this.getCapturedNode(e);
const node = this.getCapturedNode({ position: e });

if (node) {
node.handleMouseDoubleClick(e);
} else {
this.emit('stateDrop', this.relativeMousePos({ x: e.x, y: e.y }));
this.emit('dblclick', this.relativeMousePos({ x: e.x, y: e.y }));
}
};

Expand Down
1 change: 1 addition & 0 deletions src/renderer/src/lib/data/MachineController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,7 @@ export class MachineController {
}
}

this.container.children.remove('state', child.id);
child.parent = parent;
parent.children.add('state', child.id);
// TODO Сделать удобный проход по переходам состояния
Expand Down
54 changes: 43 additions & 11 deletions src/renderer/src/lib/data/StatesController.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import throttle from 'lodash.throttle';

import { Point } from '@renderer/types/graphics';
import { MyMouseEvent } from '@renderer/types/mouse';

Expand All @@ -7,6 +9,13 @@ import { EventSelection } from '../drawable/Events';
import { InitialStateMark } from '../drawable/InitialStateMark';
import { State } from '../drawable/State';

type DragHandler = (state: State, e: { event: MyMouseEvent }) => void;

type DragInfo = {
parentId: string;
childId: string;
} | null;

/**
* Контроллер {@link State|состояний}.
* Предоставляет подписку на события, связанные с состояниями.
Expand All @@ -22,6 +31,7 @@ interface StatesControllerEvents {
}

export class StatesController extends EventEmitter<StatesControllerEvents> {
dragInfo: DragInfo = null;
initialStateMark: InitialStateMark | null = null;

constructor(public container: Container) {
Expand Down Expand Up @@ -82,19 +92,39 @@ export class StatesController extends EventEmitter<StatesControllerEvents> {
}
};

handleLongPress = (state: State, e: { event: MyMouseEvent }) => {
// если состояние вложено – отсоединяем
if (typeof state.parent !== 'undefined') {
this.container.machineController.unlinkState({ id: state.id });
return;
}
// TODO: визуальная обратная связь
// если состояние вложено – отсоединяем
handleLongPress = (state: State) => {
if (typeof state.parent === 'undefined') return;

// если под курсором есть состояние – присоединить к нему
this.container.machineController.linkStateByPoint(state, e.event);
// TODO: визуальная обратная связь
this.container.machineController.unlinkState({ id: state.id });
};

handleDrag: DragHandler = throttle<DragHandler>((state, e) => {
const possibleParent = (state.parent ?? this.container).getCapturedNode({
position: e.event,
exclude: [state.id],
includeChildrenHeight: false,
type: 'states',
});

this.dragInfo = null;

if (possibleParent) {
this.dragInfo = {
parentId: possibleParent.id,
childId: state.id,
};
}
}, 100);

handleDragEnd = (state: State, e: { dragStartPosition: Point; dragEndPosition: Point }) => {
if (this.dragInfo) {
this.container.machineController.linkState(this.dragInfo.parentId, this.dragInfo.childId);
this.dragInfo = null;
return;
}

this.container.machineController.changeStatePosition(
state.id,
e.dragStartPosition,
Expand All @@ -107,8 +137,9 @@ export class StatesController extends EventEmitter<StatesControllerEvents> {
state.on('mouseup', this.handleMouseUpOnState.bind(this, state));
state.on('dblclick', this.handleStateDoubleClick.bind(this, state));
state.on('contextmenu', this.handleContextMenu.bind(this, state));
state.on('longpress', this.handleLongPress.bind(this, state));
state.on('drag', this.handleDrag.bind(this, state));
state.on('dragend', this.handleDragEnd.bind(this, state));
state.on('longpress', this.handleLongPress.bind(this, state));

state.edgeHandlers.onStartNewTransition = this.handleStartNewTransition;
}
Expand All @@ -118,8 +149,9 @@ export class StatesController extends EventEmitter<StatesControllerEvents> {
state.off('mouseup', this.handleMouseUpOnState.bind(this, state));
state.off('dblclick', this.handleStateDoubleClick.bind(this, state));
state.off('contextmenu', this.handleContextMenu.bind(this, state));
state.off('longpress', this.handleLongPress.bind(this, state));
state.off('drag', this.handleDrag.bind(this, state));
state.off('dragend', this.handleDragEnd.bind(this, state));
state.off('longpress', this.handleLongPress.bind(this, state));

state.edgeHandlers.unbindEvents();
}
Expand Down
16 changes: 16 additions & 0 deletions src/renderer/src/lib/drawable/Children.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ export class Children {
});
}

forEachState(cb: (item: State) => void) {
this.statesList.forEach((id) => {
cb(this.stateMachine.states.get(id) as State);
});
}

getTransitionIds() {
return [...this.transitionsList];
}
Expand Down Expand Up @@ -85,6 +91,12 @@ export class Children {
}
}

getStateByIndex(index: number) {
const id = this.statesList[index];

return this.stateMachine.states.get(id);
}

getByIndex(index: number) {
if (index < this.statesList.length) {
const id = this.statesList[index];
Expand Down Expand Up @@ -112,4 +124,8 @@ export class Children {
get isEmpty() {
return this.size === 0;
}

get statesSize() {
return this.statesList.length;
}
}
40 changes: 32 additions & 8 deletions src/renderer/src/lib/drawable/Node.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { getCapturedNodeArgs } from '@renderer/types/drawable';
import { Point, Rectangle } from '@renderer/types/graphics';
import { MyMouseEvent } from '@renderer/types/mouse';

Expand Down Expand Up @@ -30,6 +31,7 @@ interface NodeEvents {
dblclick: { event: MyMouseEvent };
contextmenu: { event: MyMouseEvent };
longpress: { event: MyMouseEvent };
drag: { event: MyMouseEvent };
dragend: { dragStartPosition: Point; dragEndPosition: Point };
}

Expand Down Expand Up @@ -211,6 +213,8 @@ export abstract class Node extends EventEmitter<NodeEvents> {
y: Math.max(0, this.bounds.y),
};
}

this.emit('drag', { event: e });
};

handleMouseUp = (e: MyMouseEvent) => {
Expand All @@ -233,25 +237,45 @@ export abstract class Node extends EventEmitter<NodeEvents> {
this.emit('contextmenu', { event: e });
};

isUnderMouse<T extends Point>({ x, y }: T, withChildren?: boolean) {
isUnderMouse({ x, y }: Point, includeChildrenHeight?: boolean) {
const drawBounds = this.drawBounds;
const bounds = !withChildren
const bounds = !includeChildrenHeight
? drawBounds
: { ...drawBounds, height: drawBounds.height + drawBounds.childrenHeight };
return isPointInRectangle(bounds, { x, y });
}

getIntersection(position: Point): Node | null {
const drawBounds = this.drawBounds;
getCapturedNode(args: getCapturedNodeArgs) {
const { type } = args;

const end = type === 'states' ? this.children.statesSize : this.children.size;

for (let i = end - 1; i >= 0; i--) {
const node = (
type === 'states' ? this.children.getStateByIndex(i) : this.children.getByIndex(i)
)?.getIntersection(args);

if (node) return node;
}

return null;
}

getIntersection(args: getCapturedNodeArgs): Node | null {
const { position, type, exclude, includeChildrenHeight } = args;

if (exclude?.includes(this.id)) return null;

if (isPointInRectangle(drawBounds, position)) {
if (this.isUnderMouse(position, includeChildrenHeight)) {
return this;
}

const end = this.children.size - 1;
const end = type === 'states' ? this.children.statesSize : this.children.size;

for (let i = end; i >= 0; i--) {
const node = this.children.getByIndex(i)?.getIntersection(position);
for (let i = end - 1; i >= 0; i--) {
const node = (
type === 'states' ? this.children.getStateByIndex(i) : this.children.getByIndex(i)
)?.getIntersection(args);

if (node) return node;
}
Expand Down
Loading