import MoveableManager from "./MoveableManager";
import { GroupableProps, GroupRect, MoveableManagerInterface, MoveableTargetGroupsType, RectInfo } from "./types";
import ChildrenDiffer from "@egjs/children-differ";
import { getControlAbleGesto, getTargetAbleGesto } from "./gesto/getAbleGesto";
import Groupable from "./ables/Groupable";
import { MIN_NUM, MAX_NUM, TINY_NUM } from "./consts";
import {
getAbsolutePosesByState, equals, unsetGesto, rotatePosesInfo,
convertTransformOriginArray,
isDeepArrayEquals,
sign,
getRefTarget,
} from "./utils";
import { minus, plus } from "@scena/matrix";
import { getIntersectionPointsByConstants, getMinMaxs } from "overlap-area";
import { find, isArray, throttle } from "@daybrush/utils";
import { getMoveableTargetInfo } from "./utils/getMoveableTargetInfo";
import { solveC, solveConstantsDistance } from "./Snappable/utils";
import { setStoreCache } from "./store/Store";
function getMaxPos(poses: number[][][], index: number) {
return Math.max(...poses.map(([pos1, pos2, pos3, pos4]) => {
return Math.max(pos1[index], pos2[index], pos3[index], pos4[index]);
}));
}
function getMinPos(poses: number[][][], index: number) {
return Math.min(...poses.map(([pos1, pos2, pos3, pos4]) => {
return Math.min(pos1[index], pos2[index], pos3[index], pos4[index]);
}));
}
function getGroupRect(parentPoses: number[][][], rotation: number): GroupRect {
let pos1 = [0, 0];
let pos2 = [0, 0];
let pos3 = [0, 0];
let pos4 = [0, 0];
let width = 0;
let height = 0;
if (!parentPoses.length) {
return {
pos1,
pos2,
pos3,
pos4,
minX: 0,
minY: 0,
maxX: 0,
maxY: 0,
width,
height,
rotation,
};
}
const fixedRotation = throttle(rotation, TINY_NUM);
if (fixedRotation % 90) {
const rad = fixedRotation / 180 * Math.PI;
const a1 = Math.tan(rad);
const a2 = -1 / a1;
// ax = y // -ax + y = 0 // 0 => 1
// -ax = y // ax + y = 0 // 0 => 3
const a1MinMax = [MAX_NUM, MIN_NUM];
const a1MinMaxPos = [[0, 0], [0, 0]];
const a2MinMax = [MAX_NUM, MIN_NUM];
const a2MinMaxPos = [[0, 0], [0, 0]];
parentPoses.forEach(poses => {
poses.forEach(pos => {
// const b1 = pos[1] - a1 * pos[0];
// const b2 = pos[1] - a2 * pos[0];
const a1Dist = solveConstantsDistance([-a1, 1, 0], pos);
const a2Dist = solveConstantsDistance([-a2, 1, 0], pos);
if (a1MinMax[0] > a1Dist) {
a1MinMaxPos[0] = pos;
a1MinMax[0] = a1Dist;
}
if (a1MinMax[1] < a1Dist) {
a1MinMaxPos[1] = pos;
a1MinMax[1] = a1Dist;
}
if (a2MinMax[0] > a2Dist) {
a2MinMaxPos[0] = pos;
a2MinMax[0] = a2Dist;
}
if (a2MinMax[1] < a2Dist) {
a2MinMaxPos[1] = pos;
a2MinMax[1] = a2Dist;
}
});
});
const [a1MinPos, a1MaxPos] = a1MinMaxPos;
const [a2MinPos, a2MaxPos] = a2MinMaxPos;
const minHorizontalLine = [-a1, 1, solveC([-a1, 1], a1MinPos)];
const maxHorizontalLine = [-a1, 1, solveC([-a1, 1], a1MaxPos)];
const minVerticalLine = [-a2, 1, solveC([-a2, 1], a2MinPos)];
const maxVerticalLine = [-a2, 1, solveC([-a2, 1], a2MaxPos)];
[pos1, pos2, pos3, pos4] = [
[minHorizontalLine, minVerticalLine],
[minHorizontalLine, maxVerticalLine],
[maxHorizontalLine, minVerticalLine],
[maxHorizontalLine, maxVerticalLine],
].map(([line1, line2]) => getIntersectionPointsByConstants(line1, line2)[0]);
width = a2MinMax[1] - a2MinMax[0];
height = a1MinMax[1] - a1MinMax[0];
} else {
const minX = getMinPos(parentPoses, 0);
const minY = getMinPos(parentPoses, 1);
const maxX = getMaxPos(parentPoses, 0);
const maxY = getMaxPos(parentPoses, 1);
pos1 = [minX, minY];
pos2 = [maxX, minY];
pos3 = [minX, maxY];
pos4 = [maxX, maxY];
width = maxX - minX;
height = maxY - minY;
if (fixedRotation % 180) {
// 0
// 1 2
// 3 4
// 90
// 3 1
// 4 2
// 180
// 4 3
// 2 1
// 270
// 2 4
// 1 3
// 1, 2, 3,4 = 3 1 4 2
const changedX = [pos3, pos1, pos4, pos2];
[pos1, pos2, pos3, pos4] = changedX;
width = maxY - minY;
height = maxX - minX;
}
}
if (fixedRotation % 360 > 180) {
// 1 2 4 3
// 3 4 2 1
const changedX = [pos4, pos3, pos2, pos1];
[pos1, pos2, pos3, pos4] = changedX;
}
const { minX, minY, maxX, maxY } = getMinMaxs([pos1, pos2, pos3, pos4]);
return {
pos1,
pos2,
pos3,
pos4,
width,
height,
minX,
minY,
maxX,
maxY,
rotation,
};
}
type SelfGroup = Array<MoveableManager | null | SelfGroup>;
type CheckedMoveableManager = { finded: boolean; manager: MoveableManager };
function findMoveableGroups(
moveables: CheckedMoveableManager[],
childTargetGroups: MoveableTargetGroupsType,
): SelfGroup {
const groups = childTargetGroups.map(targetGroup => {
if (isArray(targetGroup)) {
const childMoveableGroups = findMoveableGroups(moveables, targetGroup);
const length = childMoveableGroups.length;
if (length > 1) {
return childMoveableGroups;
} else if (length === 1) {
return childMoveableGroups[0];
} else {
return null;
}
} else {
const checked = find(moveables, ({ manager }) => manager.props.target === targetGroup)!;
if (checked) {
checked.finded = true;
return checked.manager;
}
return null;
}
}).filter(Boolean);
if (groups.length === 1 && isArray(groups[0])) {
return groups[0];
}
return groups;
}
/**
* @namespace Moveable.Group
* @description You can make targets moveable.
*/
class MoveableGroup extends MoveableManager<GroupableProps> {
public static defaultProps = {
...MoveableManager.defaultProps,
transformOrigin: ["50%", "50%"],
groupable: true,
dragArea: true,
keepRatio: true,
targets: [],
defaultGroupRotate: 0,
defaultGroupOrigin: "50% 50%",
};
public differ: ChildrenDiffer<HTMLElement | SVGElement> = new ChildrenDiffer();
public moveables: MoveableManager[] = [];
public transformOrigin = "50% 50%";
public renderGroupRects: GroupRect[] = [];
private _targetGroups: MoveableTargetGroupsType = [];
private _hasFirstTargets = false;
public componentDidMount() {
super.componentDidMount();
}
public checkUpdate() {
this._isPropTargetChanged = false;
this.updateAbles();
}
public getTargets() {
return this.props.targets!;
}
public updateRect(type?: "Start" | "" | "End", isTarget?: boolean, isSetState = true) {
const state = this.state;
if (!this.controlBox || state.isPersisted) {
return;
}
setStoreCache(true);
this.moveables.forEach(moveable => {
moveable.updateRect(type, false, false);
});
const props = this.props;
const moveables = this.moveables;
const target = state.target! || props.target!;
const checkeds = moveables.map(moveable => ({ finded: false, manager: moveable }));
const targetGroups = this.props.targetGroups || [];
const moveableGroups = findMoveableGroups(
checkeds,
targetGroups,
);
const useDefaultGroupRotate = props.useDefaultGroupRotate;
moveableGroups.push(...checkeds.filter(({ finded }) => !finded).map(({ manager }) => manager));
const renderGroupRects: GroupRect[] = [];
const isReset = !isTarget || (type !== "" && props.updateGroup);
let defaultGroupRotate = props.defaultGroupRotate || 0;
if (!this._hasFirstTargets) {
const persistedRoatation = props.persistData?.rotation;
if (persistedRoatation != null) {
defaultGroupRotate = persistedRoatation;
}
}
function getMoveableGroupRect(group: SelfGroup, parentRotation: number, isRoot?: boolean): GroupRect {
const posesRotations = group.map(moveable => {
if (isArray(moveable)) {
const rect = getMoveableGroupRect(moveable, parentRotation);
const poses = [rect.pos1, rect.pos2, rect.pos3, rect.pos4];
renderGroupRects.push(rect);
return { poses, rotation: rect.rotation };
} else {
return {
poses: getAbsolutePosesByState(moveable!.state),
rotation: moveable!.getRotation(),
};
}
});
const rotations = posesRotations.map(({ rotation }) => rotation);
let groupRotation = 0;
const firstRotation = rotations[0];
const isSameRotation = rotations.every(nextRotation => {
return Math.abs(firstRotation - nextRotation) < 0.1;
});
if (isReset) {
groupRotation = !useDefaultGroupRotate && isSameRotation ? firstRotation : defaultGroupRotate;
} else {
groupRotation = !useDefaultGroupRotate && !isRoot && isSameRotation ? firstRotation : parentRotation;
}
const groupPoses = posesRotations.map(({ poses }) => poses);
const groupRect = getGroupRect(
groupPoses,
groupRotation,
);
return groupRect;
}
const rootGroupRect = getMoveableGroupRect(moveableGroups, this.rotation, true);
if (isReset) {
// reset rotataion
this.rotation = rootGroupRect.rotation;
this.transformOrigin = props.defaultGroupOrigin || "50% 50%";
this.scale = [1, 1];
}
this._targetGroups = targetGroups;
this.renderGroupRects = renderGroupRects;
const transformOrigin = this.transformOrigin;
const rotation = this.rotation;
const scale = this.scale;
const { width, height, minX, minY } = rootGroupRect;
const posesInfo = rotatePosesInfo(
[
[0, 0],
[width, 0],
[0, height],
[width, height],
],
convertTransformOriginArray(transformOrigin, width, height),
this.rotation / 180 * Math.PI,
);
const { minX: deltaX, minY: deltaY } = getMinMaxs(posesInfo.result);
const rotateScale = ` rotate(${rotation}deg)`
+ ` scale(${sign(scale[0])}, ${sign(scale[1])})`;
const transform = `translate(${-deltaX}px, ${-deltaY}px)${rotateScale}`;
this.controlBox.style.transform
= `translate3d(${minX}px, ${minY}px, ${this.props.translateZ || 0})`;
target.style.cssText += `left:0px;top:0px;`
+ `transform-origin:${transformOrigin};`
+ `width:${width}px;height:${height}px;`
+ `transform: ${transform}`;
state.width = width;
state.height = height;
const container = this.getContainer();
const info = getMoveableTargetInfo(
this.controlBox,
target,
this.controlBox,
this.getContainer(),
this._rootContainer || container,
[],
);
const pos = [info.left!, info.top!];
const [
pos1,
pos2,
pos3,
pos4,
] = getAbsolutePosesByState(info); // info.left + info.pos(1 ~ 4)
const minPos = getMinMaxs([pos1, pos2, pos3, pos4]);
const delta = [minPos.minX, minPos.minY];
const direction = sign(scale[0] * scale[1]);
info.pos1 = minus(pos1, delta);
info.pos2 = minus(pos2, delta);
info.pos3 = minus(pos3, delta);
info.pos4 = minus(pos4, delta);
// info.left = info.left + delta[0];
// info.top = info.top + delta[1];
info.left = minX - info.left! + delta[0];
info.top = minY - info.top! + delta[1];
info.origin = minus(plus(pos, info.origin!), delta);
info.beforeOrigin = minus(plus(pos, info.beforeOrigin!), delta);
info.originalBeforeOrigin = plus(pos, info.originalBeforeOrigin!);
info.transformOrigin = minus(plus(pos, info.transformOrigin!), delta);
target.style.transform
= `translate(${-deltaX - delta[0]}px, ${-deltaY - delta[1]}px)`
+ rotateScale;
setStoreCache();
this.updateState(
{
...info,
posDelta: delta,
direction,
beforeDirection: direction,
},
isSetState,
);
}
public getRect(): RectInfo {
return {
...super.getRect(),
children: this.moveables.map(child => child.getRect()),
};
}
public triggerEvent(name: string, e: any, isManager?: boolean): any {
if (isManager || name.indexOf("Group") > -1) {
return super.triggerEvent(name as any, e);
} else {
this._emitter.trigger(name, e);
}
}
public getRequestChildStyles() {
const styleNames = this.getEnabledAbles().reduce((names, able) => {
const ableStyleNames = (able.requestChildStyle?.() ?? []) as Array<keyof CSSStyleDeclaration>;
return [...names, ...ableStyleNames];
}, [] as Array<keyof CSSStyleDeclaration>);
return styleNames;
}
public getMoveables(): MoveableManagerInterface[] {
return [...this.moveables];
}
protected updateAbles() {
super.updateAbles([...this.props.ables!, Groupable], "Group");
}
protected _updateTargets() {
super._updateTargets();
this._originalDragTarget = this.props.dragTarget || this.areaElement;
this._dragTarget = getRefTarget(this._originalDragTarget, true);
}
protected _updateEvents() {
const state = this.state;
const props = this.props;
const prevTarget = this._prevDragTarget;
const nextTarget = props.dragTarget || this.areaElement;
const targets = props.targets!;
const { added, changed, removed } = this.differ.update(targets);
const isTargetChanged = added.length || removed.length;
if (isTargetChanged || this._prevOriginalDragTarget !== this._originalDragTarget) {
unsetGesto(this, false);
unsetGesto(this, true);
this.updateState({ gestos: {} });
}
if (prevTarget !== nextTarget) {
state.target = null;
}
if (!state.target) {
state.target = this.areaElement;
this.controlBox.style.display = "block";
}
if (state.target) {
if (!this.targetGesto) {
this.targetGesto = getTargetAbleGesto(this, this._dragTarget!, "Group");
}
if (!this.controlGesto) {
this.controlGesto = getControlAbleGesto(this, "GroupControl");
}
}
const isContainerChanged = !equals(state.container, props.container);
if (isContainerChanged) {
state.container = props.container;
}
if (
isContainerChanged
|| isTargetChanged
|| this.transformOrigin !== (props.defaultGroupOrigin || "50% 50%")
|| changed.length
|| targets.length && !isDeepArrayEquals(this._targetGroups, props.targetGroups || [])
) {
this.updateRect();
this._hasFirstTargets = true;
}
this._isPropTargetChanged = !!isTargetChanged;
}
protected _updateObserver() { }
}
/**
* Sets the initial rotation of the group.
* @name Moveable.Group#defaultGroupRotate
* @default 0
* @example
* import Moveable from "moveable";
*
* const moveable = new Moveable(document.body, {
* target: [].slice.call(document.querySelectorAll(".target")),
* defaultGroupRotate: 0,
* });
*
* moveable.defaultGroupRotate = 40;
*/
/**
* Sets the initial origin of the group.
* @name Moveable.Group#defaultGroupOrigin
* @default 0
* @example
* import Moveable from "moveable";
*
* const moveable = new Moveable(document.body, {
* target: [].slice.call(document.querySelectorAll(".target")),
* defaultGroupOrigin: "50% 50%",
* });
*
* moveable.defaultGroupOrigin = "20% 40%";
*/
/**
* Whether to hide the line in child moveable for group corresponding to the rect of the target.
* @name Moveable.Group#hideChildMoveableDefaultLines
* @default false
* @example
* import Moveable from "moveable";
*
* const moveable = new Moveable(document.body, {
* target: [].slice.call(document.querySelectorAll(".target")),
* hideChildMoveableDefaultLines: false,
* });
*
* moveable.hideChildMoveableDefaultLines = true;
*/
export default MoveableGroup;