Skip to content

Commit b14b551

Browse files
committed
#507 Added autoarrange collection
1 parent 1bf5e60 commit b14b551

10 files changed

+262
-5
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export interface Box {
2+
x: number;
3+
y: number;
4+
width: number;
5+
height: number;
6+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { Box } from './autoarrange-table.model';
2+
3+
export const getRandomInt = (min: number, max: number): number => {
4+
return Math.floor(Math.random() * (max - min + 1)) + min;
5+
};
6+
7+
export const isOverlapping = (box1: Box, box2: Box): boolean => {
8+
return (
9+
box1.x < box2.x + box2.width &&
10+
box1.x + box1.width > box2.x &&
11+
box1.y < box2.y + box2.height &&
12+
box1.y + box1.height > box2.y
13+
);
14+
};
15+
16+
export const calculateCollisionArea = (box1: Box, box2: Box): number => {
17+
const xOverlap = Math.max(
18+
0,
19+
Math.min(box1.x + box1.width, box2.x + box2.width) -
20+
Math.max(box1.x, box2.x)
21+
);
22+
const yOverlap = Math.max(
23+
0,
24+
Math.min(box1.y + box1.height, box2.y + box2.height) -
25+
Math.max(box1.y, box2.y)
26+
);
27+
return xOverlap * yOverlap;
28+
};

src/common/autoarrange-table/index.ts

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { Size } from '@/core/model';
2+
import { Box } from './autoarrange-table.model';
3+
import {
4+
calculateCollisionArea,
5+
isOverlapping,
6+
} from './autoarrange-table.utils';
7+
8+
function* spiralPositions(
9+
centerX: number,
10+
centerY: number,
11+
canvasSize: Size
12+
): Generator<[number, number]> {
13+
let x = 0,
14+
y = 0,
15+
dx = 0,
16+
dy = -1;
17+
18+
for (let i = 0; i < Math.max(canvasSize.width, canvasSize.height) ** 2; i++) {
19+
if (
20+
-canvasSize.width / 2 < x &&
21+
x < canvasSize.width / 2 &&
22+
-canvasSize.height / 2 < y &&
23+
y < canvasSize.height / 2
24+
) {
25+
yield [centerX + x, centerY + y];
26+
}
27+
if (x === y || (x < 0 && x === -y) || (x > 0 && x === 1 - y)) {
28+
[dx, dy] = [-dy, dx];
29+
}
30+
x += dx;
31+
y += dy;
32+
}
33+
}
34+
35+
export function findFreePositionOrMinCollision(
36+
boxes: Box[],
37+
newBoxSize: Size,
38+
canvasSize: Size
39+
): Box | null {
40+
const centerX = Math.floor(canvasSize.width / 2);
41+
const centerY = Math.floor(canvasSize.height / 2);
42+
let minCollisionBox: Box | null = null;
43+
let minCollisionArea = Infinity;
44+
45+
for (const [x, y] of spiralPositions(centerX, centerY, canvasSize)) {
46+
const newBox = {
47+
x,
48+
y,
49+
width: newBoxSize.width,
50+
height: newBoxSize.height,
51+
};
52+
if (
53+
x >= 0 &&
54+
y >= 0 &&
55+
x + newBoxSize.width <= canvasSize.width &&
56+
y + newBoxSize.height <= canvasSize.height
57+
) {
58+
let collisionArea = 0;
59+
let isFree = true;
60+
61+
for (const existingBox of boxes) {
62+
if (isOverlapping(newBox, existingBox)) {
63+
isFree = false;
64+
collisionArea += calculateCollisionArea(newBox, existingBox);
65+
}
66+
}
67+
68+
if (isFree) {
69+
return newBox;
70+
}
71+
72+
if (collisionArea < minCollisionArea) {
73+
minCollisionArea = collisionArea;
74+
minCollisionBox = newBox;
75+
}
76+
}
77+
}
78+
79+
if (minCollisionBox !== null) {
80+
return minCollisionBox;
81+
}
82+
// TODO: if no free position is found, return a random one
83+
return null;
84+
}

src/common/helpers/set-off-set-zoom-to-coords.helper.spec.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ describe('setOffSetZoomToCoords', () => {
1010
const initialContextState: CanvasViewSettingsContextModel = {
1111
canvasViewSettings: {
1212
canvasSize: { width: 5000, height: 5000 },
13+
canvasViewSize: { width: 5000, height: 5000 },
1314
viewBoxSize: { width: 20000, height: 20000 },
1415
zoomFactor: 2,
1516
scrollPosition: { x: 0, y: 0 },
@@ -25,6 +26,7 @@ describe('setOffSetZoomToCoords', () => {
2526
setLoadSample: () => {},
2627
setViewBoxSize: () => {},
2728
setAutoSave: () => {},
29+
setCanvasViewSize: () => {},
2830
};
2931

3032
// Act
@@ -51,6 +53,7 @@ describe('setOffSetZoomToCoords', () => {
5153
const initialContextState: CanvasViewSettingsContextModel = {
5254
canvasViewSettings: {
5355
canvasSize: { width: 10000, height: 10000 },
56+
canvasViewSize: { width: 5000, height: 5000 },
5457
viewBoxSize: { width: 25000, height: 15000 },
5558
zoomFactor: 2,
5659
scrollPosition: { x: 0, y: 0 },
@@ -66,6 +69,7 @@ describe('setOffSetZoomToCoords', () => {
6669
setLoadSample: () => {},
6770
setViewBoxSize: () => {},
6871
setAutoSave: () => {},
72+
setCanvasViewSize: () => {},
6973
};
7074

7175
// Act
@@ -92,6 +96,7 @@ describe('setOffSetZoomToCoords', () => {
9296
const initialContextState: CanvasViewSettingsContextModel = {
9397
canvasViewSettings: {
9498
canvasSize: { width: 300, height: 100 },
99+
canvasViewSize: { width: 5000, height: 5000 },
95100
viewBoxSize: { width: 2000, height: 5000 },
96101
zoomFactor: 5,
97102
scrollPosition: { x: 0, y: 0 },
@@ -107,6 +112,7 @@ describe('setOffSetZoomToCoords', () => {
107112
setLoadSample: () => {},
108113
setViewBoxSize: () => {},
109114
setAutoSave: () => {},
115+
setCanvasViewSize: () => {},
110116
};
111117

112118
// Act
@@ -135,6 +141,10 @@ describe('setOffSetZoomToCoords', () => {
135141
width: Number.MAX_SAFE_INTEGER,
136142
height: Number.MAX_SAFE_INTEGER,
137143
},
144+
canvasViewSize: {
145+
width: Number.MAX_SAFE_INTEGER,
146+
height: Number.MAX_SAFE_INTEGER,
147+
},
138148
viewBoxSize: {
139149
width: Number.MAX_SAFE_INTEGER,
140150
height: Number.MAX_SAFE_INTEGER,
@@ -153,6 +163,7 @@ describe('setOffSetZoomToCoords', () => {
153163
setLoadSample: () => {},
154164
setViewBoxSize: () => {},
155165
setAutoSave: () => {},
166+
setCanvasViewSize: () => {},
156167
};
157168

158169
// Act
@@ -178,6 +189,7 @@ describe('setOffSetZoomToCoords', () => {
178189
const initialContextState: CanvasViewSettingsContextModel = {
179190
canvasViewSettings: {
180191
canvasSize: { width: 5000, height: 5000 },
192+
canvasViewSize: { width: 5000, height: 5000 },
181193
viewBoxSize: { width: 20000, height: 20000 },
182194
zoomFactor: 2,
183195
scrollPosition: { x: 0, y: 0 },
@@ -193,6 +205,7 @@ describe('setOffSetZoomToCoords', () => {
193205
setLoadSample: () => {},
194206
setViewBoxSize: () => {},
195207
setAutoSave: () => {},
208+
setCanvasViewSize: () => {},
196209
};
197210

198211
// Act

src/core/providers/canvas-view-settings/canvas-view-settings.model.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Coords, Size } from '@/core/model';
33

44
export interface CanvasViewSettingsModel {
55
canvasSize: Size;
6+
canvasViewSize: Size;
67
viewBoxSize: Size;
78
zoomFactor: number;
89
scrollPosition: Coords;
@@ -26,6 +27,7 @@ const initialAutoSaveValue =
2627

2728
export const createInitialSettings = (DEFAULT_ZOOM_FACTOR: number) => ({
2829
canvasSize: CANVAS_SIZE,
30+
canvasViewSize: CANVAS_SIZE,
2931
viewBoxSize: { width: 0, height: 0 },
3032
zoomFactor: DEFAULT_ZOOM_FACTOR,
3133
scrollPosition: { x: 0, y: 0 },
@@ -38,6 +40,7 @@ export interface CanvasViewSettingsContextModel {
3840
canvasViewSettings: CanvasViewSettingsModel;
3941
setScrollPosition: (scrollPosition: Coords) => void;
4042
setCanvasSize: (canvasSize: Size) => void;
43+
setCanvasViewSize: (canvasViewSize: Size) => void;
4144
setFilename: (filename: string) => void;
4245
zoomIn: () => void;
4346
zoomOut: () => void;

src/core/providers/canvas-view-settings/canvas-view-settings.provider.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,13 @@ export const CanvasViewSettingsProvider: React.FC<Props> = props => {
5353
}));
5454
};
5555

56+
const setCanvasViewSize = (canvasSize: Size) => {
57+
setCanvasViewSettings(canvasViewSettings => ({
58+
...canvasViewSettings,
59+
canvasViewSize: canvasSize,
60+
}));
61+
};
62+
5663
const setFilename = (filename: string) => {
5764
setCanvasViewSettings(canvasViewSettings => ({
5865
...canvasViewSettings,
@@ -104,6 +111,7 @@ export const CanvasViewSettingsProvider: React.FC<Props> = props => {
104111
canvasViewSettings,
105112
setScrollPosition,
106113
setCanvasSize,
114+
setCanvasViewSize,
107115
setFilename,
108116
zoomIn,
109117
zoomOut,

src/pods/canvas/canvas.pod.tsx

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ export const CanvasPod: React.FC = () => {
4545
} = useCanvasSchemaContext();
4646
const {
4747
canvasViewSettings,
48+
setCanvasViewSize,
4849
setScrollPosition,
4950
setLoadSample,
5051
setViewBoxSize,
@@ -121,6 +122,34 @@ export const CanvasPod: React.FC = () => {
121122

122123
const containerRef = React.useRef<HTMLDivElement>(null);
123124

125+
React.useEffect(() => {
126+
if (containerRef.current) {
127+
const container = containerRef.current;
128+
129+
// TODO: Rename setOffSetZoomToCoords so that it does not only apply to coords
130+
// maybe setOffsetZoom?
131+
132+
const CANVAS_VIEW_SIZE_WITH_APPLIED_ZOOM_OFFSET = setOffSetZoomToCoords(
133+
container.clientWidth + containerRef.current.scrollLeft * 2,
134+
container.clientHeight + containerRef.current.scrollTop * 2,
135+
viewBoxSize,
136+
canvasSize,
137+
zoomFactor
138+
);
139+
140+
setCanvasViewSize({
141+
width: CANVAS_VIEW_SIZE_WITH_APPLIED_ZOOM_OFFSET.x,
142+
height: CANVAS_VIEW_SIZE_WITH_APPLIED_ZOOM_OFFSET.y,
143+
});
144+
}
145+
}, [
146+
containerRef.current?.clientWidth,
147+
containerRef.current?.scrollLeft,
148+
containerRef.current?.clientHeight,
149+
containerRef.current?.scrollTop,
150+
viewBoxSize,
151+
]);
152+
124153
const handleScroll = () => {
125154
if (containerRef.current) {
126155
setScrollPosition(

src/pods/toolbar/components/add-collection/add-collection.component.tsx

Lines changed: 56 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,68 @@ import {
1010
} from '@/core/providers/canvas-schema';
1111
import { ADD_COLLECTION_TITLE } from '@/common/components/modal-dialog';
1212
import { SHORTCUTS } from '../../shortcut/shortcut.const';
13-
14-
const BORDER_MARGIN = 40;
13+
import { findFreePositionOrMinCollision } from '@/common/autoarrange-table';
14+
import { setOffSetZoomToCoords } from '@/common/helpers/set-off-set-zoom-to-coords.helper';
15+
import { getTableSize } from './add-collection.helper';
16+
import { mapTableVMtoBoxVMMapper } from './add-collection.mapper';
1517

1618
export const AddCollection = () => {
1719
const { openModal, closeModal } = useModalDialogContext();
1820
const { canvasSchema, addTable } = useCanvasSchemaContext();
1921
const { canvasViewSettings, setLoadSample } = useCanvasViewSettingsContext();
22+
const { canvasSize, zoomFactor, viewBoxSize } = canvasViewSettings;
2023

2124
const handleAddTable = (newTable: TableVm) => {
22-
const updatedTable = {
25+
if (!newTable) {
26+
return;
27+
}
28+
29+
const tableSize = setOffSetZoomToCoords(
30+
getTableSize(newTable.fields).width,
31+
getTableSize(newTable.fields).height,
32+
viewBoxSize,
33+
canvasSize,
34+
zoomFactor
35+
);
36+
37+
const getTableSizeOffSetDependingAtZoom = () => {
38+
return {
39+
width: tableSize.x / getTableSize(newTable.fields).width,
40+
height: tableSize.y / getTableSize(newTable.fields).height,
41+
};
42+
};
43+
44+
const position = findFreePositionOrMinCollision(
45+
mapTableVMtoBoxVMMapper(canvasSchema.tables),
46+
{
47+
width:
48+
getTableSize(newTable.fields).width /
49+
getTableSizeOffSetDependingAtZoom().width,
50+
height:
51+
getTableSize(newTable.fields).height /
52+
getTableSizeOffSetDependingAtZoom().height,
53+
},
54+
{
55+
width: canvasViewSettings.canvasViewSize.width,
56+
height: canvasViewSettings.canvasViewSize.height,
57+
}
58+
);
59+
60+
console.log(
61+
getTableSize(newTable.fields).width /
62+
getTableSizeOffSetDependingAtZoom().width,
63+
getTableSize(newTable.fields).height /
64+
getTableSizeOffSetDependingAtZoom().height
65+
);
66+
67+
if (!position) {
68+
return;
69+
}
70+
71+
const updatedTable: TableVm = {
2372
...newTable,
24-
x: canvasViewSettings.scrollPosition.x + BORDER_MARGIN,
25-
y: canvasViewSettings.scrollPosition.y + BORDER_MARGIN,
73+
x: position.x - getTableSize(newTable.fields).width / 2,
74+
y: position.y - getTableSize(newTable.fields).height / 2,
2675
};
2776

2877
addTable(updatedTable);
@@ -40,9 +89,11 @@ export const AddCollection = () => {
4089
ADD_COLLECTION_TITLE
4190
);
4291
};
92+
4393
const handleCloseModal = () => {
4494
closeModal();
4595
};
96+
4697
return (
4798
<ToolbarButton
4899
icon={<TableIcon />}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { Size } from '@/core/model';
2+
import { FieldVm, TABLE_CONST } from '@/core/providers';
3+
4+
export const getTableSize = (fields: FieldVm[]): Size => {
5+
const rowHeight = TABLE_CONST.ROW_HEIGHT;
6+
const headerHeight = TABLE_CONST.HEADER_HEIGHT;
7+
const headerTitleGap = TABLE_CONST.HEADER_TITLE_GAP;
8+
const fieldCount = fields.length;
9+
10+
const totalHeight =
11+
headerHeight + headerTitleGap + fieldCount * rowHeight + 10;
12+
const totalWidth = TABLE_CONST.DEFAULT_TABLE_WIDTH;
13+
14+
return {
15+
width: totalWidth,
16+
height: totalHeight,
17+
};
18+
};

0 commit comments

Comments
 (0)