Source: src/layouts/FrameLayout.ts

import { DUMMY_POSITION } from "../consts";
import { getStyleNames, assignOptions, fill, cloneItems } from "../utils";
import { ILayout, IRectlProperties, ISize, IInfiniteGridGroup, IInfiniteGridItem } from "../types";

export type FrameType = number[][];
export interface IFrameShape {
	left?: number;
	top?: number;
	type: any;
	width: number;
	height: number;
	index?: number;
}
export interface IFrameLayoutInterface {
	horizontal: boolean;
	margin: number;
	frame: FrameType;
	frameFill: boolean;
	itemSize: number | ISize;
	[key: string]: any;
}
/*
Frame
[
[1, 1, 1, 1, 1],
[0, 0, 2, 2, 2],
[0, 0, 2, 2, 2],
[3, 4, 5, 5, 5],
]
*/
function disableFrame(
	frame: FrameType,
	type: number,
	top: number,
	left: number,
	width: number,
	height: number,
) {
	for (let i = top; i < top + height; ++i) {
		for (let j = left; j < left + width; ++j) {
			if (type !== frame[i][j]) {
				continue;
			}
			frame[i][j] = 0;
		}
	}
}
function searchShapeInFrame(
	frame: FrameType,
	type: number,
	top: number,
	left: number,
	width: number,
	height: number,
) {
	const size: IFrameShape = {
		left,
		top,
		type,
		width: 1,
		height: 1,
	};

	for (let i = left; i < width; ++i) {
		if (frame[top][i] === type) {
			size.width = i - left + 1;
			continue;
		}
		break;
	}
	for (let i = top; i < height; ++i) {
		if (frame[i][left] === type) {
			size.height = i - top + 1;
			continue;
		}
		break;
	}
	// After finding the shape, it will not find again.
	disableFrame(frame, type, top, left, size.width, size.height);
	return size;
}
function getShapes(frame: FrameType) {
	const height = frame.length;
	const width = height ? frame[0].length : 0;
	const shapes: IFrameShape[] = [];

	for (let i = 0; i < height; ++i) {
		for (let j = 0; j < width; ++j) {
			const type = frame[i][j];

			if (!type) {
				continue;
			}
			// Separate shapes with other numbers.
			shapes.push(searchShapeInFrame(frame, type, i, j, width, height));
		}
	}
	shapes.sort((a, b) => (a.type < b.type ? -1 : 1));
	return {
		shapes,
		width,
		height,
	};
}
/**
 * @classdesc FrameLayout is a layout that allows you to place cards in a given frame. It is a layout that corresponds to a level intermediate between the placement of the image directly by the designer and the layout using the algorithm.
 * @ko FrameLayout은 주어진 프레임에 맞춰 카드를 배치하는 레이아웃입니다. 디자이너가 직접 이미지를 배치하는 것과 알고리즘을 사용한 배치의 중간 정도 수준에 해당하는 레이아웃이다.
 * @class eg.InfiniteGrid.FrameLayout
 * @param {Object} [options] The option object of eg.InfiniteGrid.FrameLayout module <ko>eg.InfiniteGrid.FrameLayout 모듈의 옵션 객체</ko>
 * @param {String} [options.margin=0] Margin used to create space around items <ko>아이템들 사이의 공간</ko>
 * @param {Boolean} [options.horizontal=false] Direction of the scroll movement (false: vertical, true: horizontal) <ko>스크롤 이동 방향 (false: 세로방향, true: 가로방향)</ko>
 * @param {Boolean} [options.itemSize=0] The size of the items. If it is 0, it is calculated as the size of the first item in items. <ko> 아이템의 사이즈. 만약 아이템 사이즈가 0이면, 아이템들의 첫번째 아이템의 사이즈로 계산이 된다. </ko>
 * @param {Boolean} [options.frame=[]] The size of the items. If it is 0, it is calculated as the size of the first item in items. <ko> 아이템의 사이즈. 만약 아이템 사이즈가 0이면, 아이템들의 첫번째 아이템의 사이즈로 계산이 된다. </ko>
 * @param {Boolean} [options.frameFill=true] Make sure that the frame can be attached after the previous frame. <ko> 다음 프레임이 전 프레임에 이어 붙일 수 있는지 있는지 확인한다. </ko>
 * @example
```
<script>
var ig = new eg.InfiniteGrid("#grid". {
  horizontal: true,
});

ig.setLayout(eg.InfiniteGrid.FrameLayout, {
  margin: 10,
  itemSize: 200,
  frame: [
    [1, 1, 1, 1, 1],
    [0, 0, 2, 2, 2],
    [0, 0, 2, 2, 2],
    [3, 4, 5, 5, 5],
  ],
});

// or

var layout = new eg.InfiniteGrid.FrameLayout({
  margin: 10,
  itemSize: 200,
  horizontal: true,
  frame: [
    [1, 1, 1, 1, 1],
    [0, 0, 2, 2, 2],
    [0, 0, 2, 2, 2],
    [3, 4, 5, 5, 5],
  ],
});

</script>
```
 **/
class FrameLayout implements ILayout {
	public options: IFrameLayoutInterface;
	protected _itemSize: number | ISize;
	protected _shapes: {
		shapes: IFrameShape[],
		width?: number,
		height?: number,
	};
	protected _size: number;
	protected _style: IRectlProperties;

	constructor(options: Partial<IFrameLayoutInterface> = {}) {
		this.options = assignOptions({
			margin: 0,
			horizontal: false,
			itemSize: 0,
			frame: [],
			frameFill: true,
		}, options);
		const frame = this.options.frame.map(row => row.slice());

		this._itemSize = this.options.itemSize || 0;
		// divide frame into shapes.
		this._shapes = getShapes(frame);
		this._size = 0;
		this._style = getStyleNames(this.options.horizontal);
	}
	/**
	 * Adds items of groups at the bottom of a outline.
	 * @ko 그룹들의 아이템들을 아웃라인 아래에 추가한다.
	 * @method eg.InfiniteGrid.FrameLayout#layout
	 * @param {Array} groups Array of groups to be layouted <ko>레이아웃할 그룹들의 배열</ko>
	 * @param {Array} outline Array of outline points to be reference points <ko>기준점이 되는 아웃라인 점들의 배열</ko>
	 * @return {eg.InfiniteGrid.FrameLayout} An instance of a module itself<ko>모듈 자신의 인스턴스</ko>
	 * @example
	 * layout.layout(groups, [100, 200, 300, 400]);
	 */
	public layout(groups: IInfiniteGridGroup[] = [], outline: number[] = []) {
		const length = groups.length;
		let point = outline;

		for (let i = 0; i < length; ++i) {
			const group = groups[i];
			const outlines = this._layout(group.items, point, true);

			group.outlines = outlines;
			point = outlines.end;
		}
		return this;
	}
	/**
	 * Set the viewport size of the layout.
	 * @ko 레이아웃의 가시 사이즈를 설정한다.
	 * @method eg.InfiniteGrid.FrameLayout#setSize
	 * @param {Number} size The viewport size of container area where items are added to a layout <ko>레이아웃에 아이템을 추가하는 컨테이너 영역의 가시 사이즈</ko>
	 * @return {eg.InfiniteGrid.FrameLayout} An instance of a module itself<ko>모듈 자신의 인스턴스</ko>
	 * @example
	 * layout.setSize(800);
	 */
	public setSize(size: number) {
		this._size = size;
		return this;
	}
	/**
	 * Adds items at the bottom of a outline.
	 * @ko 아이템들을 아웃라인 아래에 추가한다.
	 * @method eg.InfiniteGrid.FrameLayout#append
	 * @param {Array} items Array of items to be layouted <ko>레이아웃할 아이템들의 배열</ko>
	 * @param {Array} [outline=[]] Array of outline points to be reference points <ko>기준점이 되는 아웃라인 점들의 배열</ko>
	 * @return {Object} Layouted items and outline of start and end <ko> 레이아웃이 된 아이템과 시작과 끝의 아웃라인이 담긴 정보</ko>
	 * @example
	 * layout.prepend(items, [100]);
	 */
	public append(items: IInfiniteGridItem[], outline?: number[], cache?: boolean) {
		return this._insert(items, outline, true, cache);
	}
	/**
	 * Adds items at the top of a outline.
	 * @ko 아이템을 아웃라인 위에 추가한다.
	 * @method eg.InfiniteGrid.FrameLayout#prepend
	 * @param {Array} items Array of items to be layouted <ko>레이아웃할 아이템들의 배열</ko>
	 * @param {Array} [outline=[]] Array of outline points to be reference points <ko>기준점이 되는 아웃라인 점들의 배열</ko>
	 * @return {Object} Layouted items and outline of start and end <ko> 레이아웃이 된 아이템과 시작과 끝의 아웃라인이 담긴 정보</ko>
	 * @example
	 * layout.prepend(items, [100]);
	 */
	public prepend(items: IInfiniteGridItem[], outline?: number[], cache?: boolean) {
		return this._insert(items, outline, false, cache);
	}
	protected _getItemSize() {
		this._checkItemSize();

		return this._itemSize;
	}
	protected _checkItemSize() {
		if (this.options.itemSize) {
			this._itemSize = this.options.itemSize;
			return;
		}
		const style = this._style;
		const size = style.size2;
		const margin = this.options.margin;

		// if itemSize is not in options, caculate itemSize from size.
		this._itemSize = (this._size + margin) / this._shapes[size]! - margin;
	}
	protected _layout(items: IInfiniteGridItem[], outline: number[] = [], isAppend?: boolean) {
		const length = items.length;
		const style = this._style;
		const { margin, frameFill } = this.options;
		const size1Name = style.size1;
		const size2Name = style.size2;
		const pos1Name = style.startPos1;
		const pos2Name = style.startPos2;
		const itemSize = this._getItemSize();
		const isItemObject = typeof itemSize === "object";
		const itemSize2 = isItemObject ? (itemSize as ISize)[size2Name] : itemSize as number;
		const itemSize1 = isItemObject ? (itemSize as ISize)[size1Name] : itemSize as number;
		const shapesSize = this._shapes[size2Name]!;
		const shapes = this._shapes.shapes;
		const shapesLength = shapes.length;
		const startOutline = fill(new Array(shapesSize), DUMMY_POSITION);
		const endOutline = fill(new Array(shapesSize), DUMMY_POSITION);
		let dist = 0;
		let end = 0;

		if (!shapesLength) {
			return { start: outline, end: outline };
		}
		for (let i = 0; i < length; i += shapesLength) {
			for (let j = 0; j < shapesLength && i + j < length; ++j) {
				const item = items[i + j];
				const shape = shapes[j];
				const shapePos1 = shape[pos1Name]!;
				const shapePos2 = shape[pos2Name]!;
				const shapeSize1 = shape[size1Name]!;
				const shapeSize2 = shape[size2Name]!;
				const pos1 = end - dist + shapePos1 * (itemSize1 + margin);
				const pos2 = shapePos2 * (itemSize2 + margin);
				const size1 = shapeSize1 * (itemSize1 + margin) - margin;
				const size2 = shapeSize2 * (itemSize2 + margin) - margin;

				for (let k = shapePos2; k < shapePos2 + shapeSize2 && k < shapesSize; ++k) {
					if (startOutline[k] === DUMMY_POSITION) {
						startOutline[k] = pos1;
					}
					startOutline[k] = Math.min(startOutline[k], pos1);
					endOutline[k] = Math.max(endOutline[k], pos1 + size1 + margin);
				}
				item.rect = {
					[pos1Name]: pos1,
					[pos2Name]: pos2,
					[size1Name]: size1,
					[size2Name]: size2,
				} as any;
			}
			end = Math.max(...endOutline);
			// check dist once
			if (i !== 0) {
				continue;
			}
			// find & fill empty block
			if (!frameFill) {
				dist = 0;
				continue;
			}
			dist = end;

			for (let j = 0; j < shapesSize; ++j) {
				if (startOutline[j] === DUMMY_POSITION) {
					continue;
				}
				// the dist between frame's end outline and next frame's start outline
				// expect that next frame's start outline is startOutline[j] + end
				dist = Math.min(startOutline[j] + end - endOutline[j], dist);
			}
		}
		for (let i = 0; i < shapesSize; ++i) {
			if (startOutline[i] !== DUMMY_POSITION) {
				continue;
			}
			startOutline[i] = Math.max(...startOutline);
			endOutline[i] = startOutline[i];
		}
		// The target outline is start outline when type is appending
		const targetOutline = isAppend ? startOutline : endOutline;
		const prevOutlineEnd = outline.length === 0 ? 0 : Math[isAppend ? "max" : "min"](...outline);
		let prevOutlineDist = isAppend ? 0 : end;

		if (frameFill && outline.length === shapesSize) {
			prevOutlineDist = -DUMMY_POSITION;
			for (let i = 0; i < shapesSize; ++i) {
				if (startOutline[i] === endOutline[i]) {
					continue;
				}
				// if appending type is prepend(false), subtract dist from appending group's height.

				prevOutlineDist = Math.min(targetOutline[i] + prevOutlineEnd - outline[i], prevOutlineDist);
			}
		}
		for (let i = 0; i < shapesSize; ++i) {
			startOutline[i] += prevOutlineEnd - prevOutlineDist;
			endOutline[i] += prevOutlineEnd - prevOutlineDist;
		}
		items.forEach(item => {
			item.rect[pos1Name] += prevOutlineEnd - prevOutlineDist;
		});
		return {
			start: startOutline.map(point => parseInt(point, 10)),
			end: endOutline.map(point => parseInt(point, 10)),
		};
	}
	private _insert(items: IInfiniteGridItem[] = [], outline: number[] = [], isAppend?: boolean, cache?: boolean) {
		// this only needs the size of the item.
		const clone = cache ? items : cloneItems(items);

		return {
			items: clone,
			outlines: this._layout(clone, outline, isAppend),
		};
	}
}

export default FrameLayout;
comments powered by Disqus