Skip to content

Commit

Permalink
feat: resize multiple & keep aspect ratio
Browse files Browse the repository at this point in the history
  • Loading branch information
juanfran committed Feb 4, 2025
1 parent 1784868 commit e641353
Show file tree
Hide file tree
Showing 10 changed files with 251 additions and 187 deletions.
28 changes: 2 additions & 26 deletions apps/web/src/app/modules/board/board/board.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,6 @@ import { ContextMenuStore } from '@tapiz/ui/context-menu/context-menu.store';
import { BoardContextMenuComponent } from '../components/board-context-menu/board-contextmenu.component';
import { HistoryService } from '../services/history.service';
import { MoveService } from '@tapiz/cdk/services/move.service';
import { ResizeService } from '@tapiz/ui/resize/resize.service';
import { RotateService } from '@tapiz/ui/rotate/rotate.service';
import { NodeToolbarComponent } from '../components/node-toolbar/node-toolbar.component';
import { WsService } from '../../ws/services/ws.service';
Expand Down Expand Up @@ -84,6 +83,7 @@ import { NotesVisibilityComponent } from '../components/notes-visibility/notes-v
import { NoteHeightCalculatorComponent } from '../components/note/components/note-height-calculator/note-height-calculator.component';
import { BoardDragDirective } from './directives/board-drag.directive';
import { BoardHeaderOptionsComponent } from '../components/board-header-options/board-header-options.component';
import { BoardResizeDirective } from './directives/board-resize.directive';

@Component({
selector: 'tapiz-board',
Expand Down Expand Up @@ -125,6 +125,7 @@ import { BoardHeaderOptionsComponent } from '../components/board-header-options/
CopyPasteDirective,
BoardShourtcutsDirective,
BoardDragDirective,
BoardResizeDirective,
],
host: {
'[class.node-selection-disabled]': '!nodeSelectionEnabled()',
Expand All @@ -151,7 +152,6 @@ export class BoardComponent implements AfterViewInit, OnDestroy {
private boardFacade = inject(BoardFacade);
private contextMenuStore = inject(ContextMenuStore);
private moveService = inject(MoveService);
private resizeService = inject(ResizeService);
private rotateService = inject(RotateService);
private subscriptionService = inject(SubscriptionService);
private drawingStore = inject(DrawingStore);
Expand Down Expand Up @@ -325,30 +325,6 @@ export class BoardComponent implements AfterViewInit, OnDestroy {
);
});

this.resizeService.onStart$.pipe(takeUntilDestroyed()).subscribe((node) => {
this.boardFacade.patchHistory((history) => {
const nodeAction: StateActions = {
data: node,
op: 'patch',
};
history.past.unshift([nodeAction]);
history.future = [];

return history;
});
});

this.resizeService.onResize$
.pipe(takeUntilDestroyed())
.subscribe((node) => {
this.store.dispatch(
BoardActions.batchNodeActions({
history: false,
actions: [this.nodesActions.patch(node)],
}),
);
});

this.moveService.setUp({
zoom: this.store.select(boardPageFeature.selectZoom),
relativePosition: this.store.select(boardPageFeature.selectPosition),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
import { Directive, inject } from '@angular/core';
import { BoardFacade } from '../../../../services/board-facade.service';
import { Store } from '@ngrx/store';
import { isResizable, StateActions } from '@tapiz/board-commons';
import { NodesActions } from '../../services/nodes-actions';
import { BoardActions } from '@tapiz/board-commons/actions/board.actions';
import { ResizeService } from '@tapiz/ui/resize';
import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';

import { Resizable, TuNode } from '@tapiz/board-commons';
import { filter, map, share } from 'rxjs';
import {
translate,
rotate,
compose,
decomposeTSR,
} from 'transformation-matrix';
@Directive({
selector: '[tapizBoarResizeDirective]',
})
export class BoardResizeDirective {
#resizeService = inject(ResizeService);
#boardFacade = inject(BoardFacade);
#store = inject(Store);
#nodesActions = inject(NodesActions);
#selectFocusNodes = toSignal(this.#boardFacade.selectFocusNodes$);
#initialNodes: TuNode<Resizable>[] = [];

constructor() {
const onResize$ = this.#resizeService.onResize$.pipe(
takeUntilDestroyed(),
share(),
);

onResize$.pipe(filter((event) => event.type === 'start')).subscribe(() => {
this.#boardFacade.patchHistory((history) => {
const focusedNodes = this.#selectFocusNodes();

if (!focusedNodes) {
return history;
}

this.#initialNodes = focusedNodes.filter(isResizable);

const actions: StateActions[] = this.#initialNodes.map((node) => {
return {
data: {
id: node.id,
type: node.type,
content: {
position: { ...node.content.position },
width: node.content.width,
height: node.content.height,
},
} as TuNode<Resizable>,
op: 'patch',
};
});

history.past.unshift(actions);
history.future = [];

return history;
});
});

onResize$
.pipe(
filter((event) => event.type === 'move'),
map((mouseMove) => {
const shiftKey = mouseMove.event.shiftKey;

return this.#initialNodes.map((node) => {
const nodeInitial = this.#initialNodes.find(
(n) => n.id === node.id,
);

if (!nodeInitial) {
return;
}

const diffX = mouseMove.event.x - mouseMove.initialPosition.x;
const diffY = mouseMove.event.y - mouseMove.initialPosition.y;

const rotation = mouseMove.nodeRotation ?? 0;
const angle = rotation * (Math.PI / 180);
let deltaX = Math.round(
diffX * Math.cos(angle) + diffY * Math.sin(angle),
);
let deltaY = Math.round(
diffY * Math.cos(angle) - diffX * Math.sin(angle),
);

if (shiftKey) {
const originalWidth = nodeInitial.content.width;
const originalHeight = nodeInitial.content.height;
let m;

switch (mouseMove.position) {
case 'top-left':
m = originalHeight / originalWidth;
break;
case 'top-right':
m = -originalHeight / originalWidth;
break;
case 'bottom-left':
m = -originalHeight / originalWidth;
break;
case 'bottom-right':
m = originalHeight / originalWidth;
break;
default:
m = 0;
}

const denominator = 1 + m * m;
const adjustedDeltaX = (deltaX + m * deltaY) / denominator;
const adjustedDeltaY = m * adjustedDeltaX;

deltaX = Math.round(adjustedDeltaX);
deltaY = Math.round(adjustedDeltaY);
}

let newWidth = nodeInitial.content.width;
let newHeight = nodeInitial.content.height;

let currentMatrix = compose(
translate(
nodeInitial.content.position.x,
nodeInitial.content.position.y,
),
rotate(angle),
);

if (mouseMove.position === 'top-left') {
newWidth -= deltaX;
newHeight -= deltaY;
currentMatrix = compose(currentMatrix, translate(deltaX, deltaY));
} else if (mouseMove.position === 'top-right') {
newWidth += deltaX;
newHeight -= deltaY;
currentMatrix = compose(currentMatrix, translate(0, deltaY));
} else if (mouseMove.position === 'bottom-left') {
newWidth -= deltaX;
newHeight += deltaY;
currentMatrix = compose(currentMatrix, translate(deltaX, 0));
} else if (mouseMove.position === 'bottom-right') {
newWidth += deltaX;
newHeight += deltaY;
}

const decomposed = decomposeTSR(currentMatrix);

if (newWidth < 5 || newHeight < 5) {
return;
}

return {
id: node.id,
type: node.type,
content: {
position: {
x: decomposed.translate.tx,
y: decomposed.translate.ty,
},
width: newWidth,
height: newHeight,
},
} as TuNode<Resizable>;
});
}),
)
.subscribe((nodes) => {
const actions = nodes
.filter((node) => !!node)
.map((node) => {
return this.#nodesActions.patch(node);
});

this.#store.dispatch(
BoardActions.batchNodeActions({
history: false,
actions,
}),
);
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,6 @@ export class BoardToolbarComponent {

note() {
const createNote = () => {
console.log('create note');
this.toolbarSubscription = this.#zoneService
.select()
.subscribe(({ userId, position }) => {
Expand Down
9 changes: 9 additions & 0 deletions libs/board-commons/src/lib/models/resizable.model.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Point } from '../models/point.model.js';
import { TuNode } from './node.model.js';

export interface Resizable {
width: number;
Expand All @@ -12,3 +13,11 @@ export type ResizePosition =
| 'top-right'
| 'bottom-left'
| 'bottom-right';

export const isResizable = (node: TuNode): node is TuNode<Resizable> => {
return (
'width' in node.content &&
'height' in node.content &&
'position' in node.content
);
};
Loading

0 comments on commit e641353

Please sign in to comment.