Skip to content

Commit

Permalink
Merge pull request #49 from bryzZz/state-nesting-ux
Browse files Browse the repository at this point in the history
try state nesting
  • Loading branch information
chekoopa authored Nov 14, 2023
2 parents fab2a69 + c8119cb commit 8ca0566
Show file tree
Hide file tree
Showing 10 changed files with 990 additions and 765 deletions.
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
55 changes: 31 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 @@ -107,8 +108,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 +130,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;

for (let i = end; i >= 0; i--) {
const node = this.children.getByIndex(i)?.getIntersection(position);
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;
}
Expand All @@ -148,24 +154,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 +196,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 +207,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 +220,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 +245,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

0 comments on commit 8ca0566

Please sign in to comment.