123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601 |
- import { useStore } from "../store";
- import {
- components,
- DrawItem,
- ShapeType,
- ComponentSnapInfo,
- } from "../components";
- import { computed, reactive, Ref, ref, toRaw, watch, watchEffect } from "vue";
- import {
- createLine,
- eqNGDire,
- eqPoint,
- lineIntersection,
- lineLen,
- linePointLen,
- linePointProjection,
- numEq,
- Pos,
- vector,
- vector2IncludedAngle,
- verticalVector,
- zeroEq,
- } from "@/utils/math";
- import { globalWatch, installGlobalVar } from "./use-global-vars";
- import { BaseItem } from "../components/util";
- import {
- ScaleVectorType,
- useGetTransformerOperDirection,
- useGetTransformerOperType,
- useTransformer,
- } from "./use-transformer";
- import { Transform } from "konva/lib/Util";
- import { useCacheUnitTransform, useViewerInvertTransform } from "./use-viewer";
- import { MathUtils } from "three";
- import { arrayInsert, mergeFuns, rangMod } from "@/utils/shared";
- import { useTestPoints } from "./use-debugger";
- export type SnapInfo = ComponentSnapInfo & Pick<DrawItem, "id">;
- const useStoreSnapInfos = () => {
- const store = useStore();
- const types = Object.keys(components) as ShapeType[];
- const infos = reactive(new Set<SnapInfo>());
- const cleanups = [] as Array<() => void>;
- for (const type of types) {
- const comp = components[type];
- if (!("getSnapInfos" in comp)) continue;
- cleanups.push(
- globalWatch(
- () => store.getTypeItems(type),
- (items, _, onCleanup) => {
- if (!items) return;
- for (const item of items) {
- const snaps = computed(() => {
- if (item.ref) {
- return comp.getSnapInfos!(item as any) as SnapInfo[];
- } else {
- return [];
- }
- });
- const snapInfoWatchStop = watch(
- snaps,
- (snaps, _, onCleanup) => {
- snaps.forEach((snap) => {
- snap.id = item.id;
- infos.add(snap);
- });
- onCleanup(() => {
- snaps.forEach((snap) => infos.delete(snap));
- });
- },
- { immediate: true }
- );
- const existsWatchStop = watchEffect(() => {
- if (!items.includes(item as any)) {
- snapInfoWatchStop();
- existsWatchStop();
- }
- });
- onCleanup(() => {
- snapInfoWatchStop();
- existsWatchStop();
- });
- }
- },
- { immediate: true }
- )
- );
- }
- return {
- infos: computed(() => Array.from(infos.values())),
- cleanup: mergeFuns(cleanups),
- };
- };
- export const useCustomSnapInfos = installGlobalVar(() => {
- const infos = ref<ComponentSnapInfo[]>([]);
- return {
- infos,
- add: (snap: ComponentSnapInfo) => {
- infos.value.push(snap);
- },
- remove: (snap: ComponentSnapInfo) => {
- const index = infos.value.findIndex((p) => toRaw(p) === toRaw(snap));
- if (index !== -1) {
- infos.value.splice(index, 1);
- }
- },
- };
- }, Symbol('customSnapInfos'));
- export const useGlobalSnapInfos = installGlobalVar(() => {
- const storeInfos = useStoreSnapInfos();
- const customInfos = useCustomSnapInfos();
- return {
- var: computed(
- () =>
- [...customInfos.infos.value, ...storeInfos.infos.value] as SnapInfo[]
- ),
- onDestroy: storeInfos.cleanup,
- };
- }, Symbol("snapInfos"));
- export const useSnapConfig = () => {
- const unitTransform = useCacheUnitTransform();
- return {
- get snapOffset() {
- return unitTransform.getPixel(10);
- },
- };
- };
- export type SnapResultInfo = {
- attractSnaps: AttractSnapInfo[];
- selfSnaps: ComponentSnapInfo[];
- clear: () => void;
- };
- export const useSnapResultInfo = installGlobalVar(() => {
- const snapInfo = reactive({
- attractSnaps: [],
- selfSnaps: [],
- clear() {
- snapInfo.attractSnaps.length = 0;
- snapInfo.selfSnaps.length = 0;
- },
- }) as SnapResultInfo;
- return snapInfo;
- }, Symbol("snapResultInfo"));
- export type AttractSnapInfo = {
- ref: ComponentSnapInfo;
- current: ComponentSnapInfo;
- offset: number;
- angle: number;
- join: Pos;
- refDirection: Pos;
- refLinkNdx: number;
- currentLinkNdx: number;
- };
- // TODO 返回结果按照self.point参照线多少排序, 子数组按照offset排序
- export const filterAttractSnapInfos = (
- refInfos: ComponentSnapInfo[],
- selfInfos: ComponentSnapInfo[],
- filters: (
- self: ComponentSnapInfo,
- ref: ComponentSnapInfo,
- result: AttractSnapInfo[]
- ) =>
- | { items?: AttractSnapInfo[]; stop?: boolean; stopSelf?: boolean }
- | AttractSnapInfo[],
- sortKey?: "offset" | "angle"
- ) => {
- const attractSnapInfosGroups: AttractSnapInfo[][] = [];
- for (const self of selfInfos) {
- if (self.point.view) continue;
- const attractSnapInfos: AttractSnapInfo[] = [];
- for (const ref of refInfos) {
- let infos = filters(self, ref, attractSnapInfos);
- const stop = Array.isArray(infos) ? false : !!infos.stop;
- const stopRef = Array.isArray(infos) ? false : !!infos.stopSelf;
- infos = Array.isArray(infos) ? infos : infos.items!;
- if (infos && sortKey) {
- for (const info of infos) {
- arrayInsert(
- attractSnapInfos,
- info,
- (a, b) => b[sortKey] < a[sortKey]
- );
- }
- }
- if (stop) {
- return attractSnapInfosGroups;
- }
- if (stopRef) {
- break;
- }
- }
- arrayInsert(
- attractSnapInfosGroups,
- attractSnapInfos,
- (a, b) => a.length < b.length
- );
- }
- return attractSnapInfosGroups;
- };
- type FilterAttrib = {
- maxOffset?: number;
- maxAngle?: number;
- type?: "inter" | "projection" | "all" | "none";
- };
- const getAttractSnapInfos = (
- ref: ComponentSnapInfo,
- self: ComponentSnapInfo,
- filter: FilterAttrib
- ) => {
- filter.type = filter.type || "all";
- const limitAngle = "maxAngle" in filter;
- const limitOffset = "maxOffset" in filter;
- const isAll = filter.type === "all";
- const attractSnapInfos: AttractSnapInfo[] = [];
- for (let i = 0; i < self.linkAngle.length; i++) {
- for (let j = 0; j < ref.linkAngle.length; j++) {
- const angle = Math.abs(ref.linkAngle[j] - self.linkAngle[i]);
- if (limitAngle && angle > filter.maxAngle!) {
- continue;
- }
- let join: Pos | null = null;
- let offset: number | null = null;
- const checkOffset = (getJoin: () => Pos | null) => {
- join = getJoin();
- offset = join && lineLen(self.point, join);
- const adopt = join && (!limitOffset || offset! < filter.maxOffset!);
- if (!adopt) {
- join = null;
- offset = null;
- }
- return adopt;
- };
- if (filter.type === "none") {
- const adopt = checkOffset(() => ref.point);
- if (!adopt) continue;
- } else {
- const refLine = [
- ref.point,
- vector(ref.point).add(ref.linkDirections[j]),
- ];
- if (filter.type === "projection" || isAll) {
- const adopt = checkOffset(() =>
- linePointProjection(refLine, self.point)
- );
- if (!adopt && !isAll) continue;
- }
- const curLine = [
- self.point,
- vector(self.point).add(self.linkDirections[i]),
- ];
- if (!join && !checkOffset(() => lineIntersection(refLine, curLine))) {
- continue;
- }
- }
- attractSnapInfos.push({
- ref,
- current: self,
- refLinkNdx: j,
- currentLinkNdx: i,
- angle,
- join: join!,
- offset: offset!,
- refDirection: ref.linkDirections[j],
- });
- }
- }
- return attractSnapInfos;
- };
- const moveSnap = (
- refInfos: ComponentSnapInfo[],
- selfInfos: ComponentSnapInfo[],
- filter: FilterAttrib
- ) => {
- filter.maxOffset = filter.maxOffset || 15;
- const exclude = new Map<AttractSnapInfo, AttractSnapInfo>();
- const addExclude = (nor: AttractSnapInfo, act: AttractSnapInfo) => {
- exclude.set(nor, act);
- exclude.set(act, nor);
- };
- const getAttractSnapJoin = (nor: AttractSnapInfo, act: AttractSnapInfo) => {
- if (nor === act || exclude.get(nor) === act) {
- return void 0;
- }
- if (eqPoint(nor.current.point, act.join)) {
- return addExclude(nor, act);
- }
- // console.log(nor.current.point, act.join)
- const norJoin = nor.join;
- const norDire = vector(nor.refDirection);
- const norLine = createLine(norJoin, norDire);
- // TODO 确保移动前后normal的 direction 保持一致
- const useDire = act.refDirection;
- if (eqNGDire(norDire, useDire)) {
- return addExclude(nor, act);
- }
- const useJoin = act.join;
- const useLine = createLine(useJoin, useDire);
- const nuJoin = lineIntersection(norLine, useLine);
- if (!nuJoin || lineLen(nuJoin, norJoin) > filter.maxOffset!) {
- return addExclude(nor, act);
- } else {
- return nuJoin;
- }
- };
- const useAttractSnaps: AttractSnapInfo[] = [];
- let end: Pos | null = null;
- let start: Pos | null = null;
- // TODO 最多参考2个信息
- const attractSnapGroups = filterAttractSnapInfos(
- refInfos,
- selfInfos,
- (self, ref, selfGroup) => {
- const attractSnapInfos = getAttractSnapInfos(ref, self, filter);
- const nor = selfGroup[0] || attractSnapInfos[0];
- const checks = [...selfGroup, ...attractSnapInfos];
- // TODO 尽快提前结束
- for (const check of checks) {
- const join = getAttractSnapJoin(nor, check);
- if (join) {
- end = join;
- start = nor.current.point;
- useAttractSnaps.push(nor, check);
- return { stop: true };
- }
- }
- return attractSnapInfos;
- },
- "offset"
- );
- if (!end) {
- if (!attractSnapGroups.length || !attractSnapGroups[0].length) return null;
- const nor = attractSnapGroups[0][0];
- end = nor.join;
- start = nor.current.point;
- useAttractSnaps.push(nor);
- // TODO 如果没有同一个点的两线段,则使用2垂直的两点线段
- const move = vector(end!).sub(start!);
- for (let i = 0; i < attractSnapGroups.length; i++) {
- let j = i === 0 ? 1 : 0;
- for (; j < attractSnapGroups[i].length; j++) {
- const attractSnap = attractSnapGroups[i][j];
- const rDire = attractSnap.refDirection;
- const angle = vector2IncludedAngle(nor.refDirection, rDire);
- if (!numEq(rangMod(angle, Math.PI), Math.PI / 2)) {
- continue;
- }
- const cPoint = vector(attractSnap.current.point).add(move);
- const rPoint = attractSnap.ref.point;
- const inter = lineIntersection(
- createLine(cPoint, nor.refDirection),
- createLine(rPoint, rDire)
- );
- if (inter) {
- useAttractSnaps.push(attractSnap);
- end = vector(end).add(inter.sub(cPoint));
- break;
- }
- }
- if (j !== attractSnapGroups[i].length) break;
- }
- }
- const norMove = vector(end!).sub(start!);
- return {
- useAttractSnaps,
- transform: new Transform().translate(norMove.x, norMove.y),
- };
- };
- type SelfAttitude = {
- rotation: number;
- origin: Pos;
- operTarget: Pos;
- center: Pos;
- };
- const scaleSnap = (
- refInfos: ComponentSnapInfo[],
- selfInfos: ComponentSnapInfo[],
- filter: Omit<FilterAttrib, "type">,
- type: ScaleVectorType,
- attitude: SelfAttitude,
- testPoints: Ref<Pos[]>
- ) => {
- const { origin, operTarget } = attitude;
- const attractSnaps: AttractSnapInfo[] = [];
- const operVector = vector(operTarget).sub(origin).normalize();
- const vOperVector = verticalVector(operVector);
- const vLine = createLine(origin, vOperVector);
- const proportional = [
- "top-right",
- "top-left",
- "bottom-right",
- "bottom-left",
- ].includes(type);
- const limitOffset = filter.maxOffset;
- const asFilter: FilterAttrib = { ...filter, type: "none" };
- delete asFilter.maxOffset;
- const exclude = new Set<AttractSnapInfo>();
- const getAttractSnapJoin = (nor: AttractSnapInfo) => {
- if (exclude.has(nor)) return;
- if (
- eqNGDire(nor.refDirection, operVector) ||
- eqNGDire(
- nor.refDirection,
- nor.current.linkDirections[nor.currentLinkNdx]
- ) ||
- Math.abs(
- vector(origin).sub(nor.current.point).normalize().dot(operVector)
- ) < 0.01
- ) {
- exclude.add(nor);
- return;
- }
- const cur = nor.current.point;
- if (
- eqNGDire(vOperVector, nor.refDirection) &&
- zeroEq(linePointLen(vLine, nor.ref.point))
- ) {
- exclude.add(nor);
- attractSnaps.push(nor);
- return;
- }
- const refLine = [
- nor.ref.point,
- vector(nor.ref.point).add(nor.refDirection),
- ];
- const norLine = proportional
- ? [origin, cur]
- : [cur, vector(cur).add(operVector)];
- const join = lineIntersection(refLine, norLine);
- if (!join || (limitOffset && lineLen(join, cur) > limitOffset)) {
- exclude.add(nor);
- return;
- }
- // testPoints.value = [origin, nor.ref.point];
- return join;
- };
- let useAttractSnap: AttractSnapInfo;
- let useJoin: Pos;
- // TODO 最多参考1个信息
- filterAttractSnapInfos(refInfos, selfInfos, (self, ref) => {
- if (eqPoint(self.point, origin)) return { stopSelf: true };
- const attractSnapInfos = getAttractSnapInfos(ref, self, asFilter);
- for (const info of attractSnapInfos) {
- const join = getAttractSnapJoin(info);
- if (join) {
- info.join = join;
- useJoin = join;
- useAttractSnap = info;
- return { stop: true };
- }
- }
- return attractSnapInfos;
- });
- if (!useAttractSnap! || !useJoin!) return null;
- const rotation = Math.atan2(operVector.y, operVector.x);
- const invRotateTransform = new Transform()
- .rotate(-rotation)
- .translate(-origin.x, -origin.y);
- const t = invRotateTransform.point(useJoin!);
- const c = invRotateTransform.point(useAttractSnap.current.point);
- const currentFactor = vector({
- x: numEq(t.x, c.x) ? 1 : t.x / c.x,
- y: numEq(t.y, c.y) ? 1 : t.y / c.y,
- });
- if (proportional) {
- currentFactor.y = currentFactor.x;
- }
- attractSnaps.push(useAttractSnap);
- return {
- useAttractSnaps: attractSnaps,
- transform: new Transform()
- .multiply(invRotateTransform.copy().invert())
- .scale(currentFactor.x, currentFactor.y)
- .multiply(invRotateTransform),
- };
- };
- export const useSnap = (
- snapInfos: { value: ComponentSnapInfo[] } = useGlobalSnapInfos()
- ) => {
- const snapResultInfo = useSnapResultInfo();
- const snapConfig = useSnapConfig();
- const testPoints = useTestPoints();
- const afterHandler = (result: ReturnType<typeof moveSnap>) => {
- if (result) {
- snapResultInfo.attractSnaps = result.useAttractSnaps;
- return result.transform;
- }
- return null;
- };
- const move = (selfSnapInfos: ComponentSnapInfo[]) => {
- const result = moveSnap(snapInfos.value, selfSnapInfos, {
- maxOffset: snapConfig.snapOffset,
- });
- snapResultInfo.selfSnaps.push(...selfSnapInfos);
- return afterHandler(result);
- };
- const scale = (
- selfSnapInfos: ComponentSnapInfo[],
- attitude: SelfAttitude & { type: ScaleVectorType }
- ) => {
- const result = scaleSnap(
- snapInfos.value,
- selfSnapInfos,
- { maxOffset: snapConfig.snapOffset },
- attitude.type,
- attitude,
- testPoints
- );
- snapResultInfo.selfSnaps = selfSnapInfos;
- return afterHandler(result);
- };
- return {
- move,
- scale,
- clear: snapResultInfo.clear,
- };
- };
- export const useComponentSnap = (componentId: string) => {
- const store = useStore();
- const type = componentId ? store.getType(componentId) : undefined;
- const comp = type && components[type];
- const api = type && comp?.getSnapInfos;
- if (!api) return null;
- const snapInfos = useGlobalSnapInfos();
- const refSnapInfos = computed(() =>
- snapInfos.value.filter((p) => !("id" in p) || p.id !== componentId)
- );
- const baseSnap = useSnap(refSnapInfos);
- const getOperType = useGetTransformerOperType();
- const getTransformerOperDirection = useGetTransformerOperDirection();
- const transformer = useTransformer();
- const viewerInvertTransform = useViewerInvertTransform();
- const snap = (item: BaseItem) => {
- const operateType = getOperType();
- const selfSnapInfos = api(item as any);
- baseSnap.clear();
- // move
- if (!operateType) {
- return baseSnap.move(selfSnapInfos);
- } else if (operateType !== "rotater") {
- const direction = getTransformerOperDirection()!;
- const node = transformer.nodes()[0];
- const rect = node.getClientRect();
- const attitude = {
- center: viewerInvertTransform.value.point({
- x: rect.x + rect.width / 2,
- y: rect.y + rect.height / 2,
- }),
- operTarget: direction[1],
- rotation: MathUtils.degToRad(node.rotation()),
- origin: direction[0],
- type: operateType,
- };
- return baseSnap.scale(selfSnapInfos, attitude);
- }
- };
- return [snap, baseSnap.clear] as const;
- };
|