bill 1 gadu atpakaļ
vecāks
revīzija
b007afd94d

+ 44 - 0
src/board/helper/coord.ts

@@ -0,0 +1,44 @@
+import { Node } from "konva/lib/Node";
+import { Transform } from "konva/lib/Util";
+
+export const getAbsoluteTransform = (shape: Node, includeSelf = false) => {
+  let current = shape;
+  const tf = includeSelf ? shape.getTransform().copy() : new Transform();
+  while ((current = current.parent)) {
+    tf.multiply(current.getTransform());
+  }
+  return tf;
+};
+
+export function getTfScaleFactors(tf: Transform) {
+  // 假设matrix是一个形如[a, c, Tx, b, d, Ty, 0, 0, 1]的数组
+  const [a, c, b, d] = tf.m;
+
+  const scaleX = Math.sqrt(a * a + b * b);
+  const scaleY = Math.sqrt(c * c + d * d);
+
+  const rotation = Math.atan2(b, a);
+  const directionX = rotation; // X轴缩放方向的旋转角度
+  const directionY = rotation + Math.PI / 2; // Y轴缩放方向的旋转角度(垂直于X轴)
+
+  console.log(directionX, directionY);
+  Math.sin(directionY);
+
+  return { x: scaleX, y: scaleY };
+}
+
+export const getRealAbsoluteSize = (
+  shape: Node,
+  size: number[],
+  includeSelf = false
+) => {
+  let current = shape;
+  let scale = includeSelf ? shape.scale() : { x: 1, y: 1 };
+  while ((current = current.parent)) {
+    const cScale = current.scale();
+
+    scale.x *= cScale.x;
+    scale.y *= cScale.y;
+  }
+  return [size[0] / scale.x, size[1] / scale.y];
+};

+ 57 - 0
src/board/helper/entity.ts

@@ -0,0 +1,57 @@
+import { Container, Entity } from "../packages";
+import {
+  createLineByDire,
+  generateId,
+  getLineInnerContinuityPoints,
+} from "../shared";
+import { Attrib } from "../type";
+import { singleClick } from "./mouse";
+
+export type CopyProps<T extends Entity = Entity, K = any> = {
+  entity: T;
+  count?: number;
+  dire?: number[];
+  size: number[];
+  factoryAttrib(pos: number[]): K;
+};
+export const copyEntityAttrib = <T extends Entity = Entity, K = any>(
+  props: CopyProps<T, K>
+): (K & Attrib)[] => {
+  const dire = props.dire || [1, 0];
+  const entity = props.entity;
+  const start = entity.shape.getPosition();
+  const items = entity.container.getSameLevelData(entity) as Attrib[];
+  const points = getLineInnerContinuityPoints(
+    createLineByDire(dire, [start.x, start.y], 10),
+    props.size,
+    props.count
+  );
+
+  const addAttribs = points.map((point) => {
+    const newAttrib = {
+      ...props.factoryAttrib(point),
+      id: generateId(items),
+    };
+    items.push(newAttrib as any);
+    return newAttrib;
+  });
+
+  return addAttribs;
+};
+
+export const addEntityAttrib = <T>(
+  container: Container,
+  factoryAttrib: (pos: number[]) => T,
+  callback: (data: T | null, err?: string) => void
+) => {
+  const interrupt = singleClick(container.config.dom, (coord, err) => {
+    if (coord) {
+      const pos = container.getRealFromStage([coord.offsetX, coord.offsetY]);
+      callback(factoryAttrib(pos));
+    } else {
+      callback(null, err);
+    }
+  });
+
+  return interrupt;
+};

+ 92 - 0
src/board/helper/mouse.ts

@@ -0,0 +1,92 @@
+import { hasTouchEvents } from "../env";
+
+export type MouseCoord = { offsetX: number; offsetY: number };
+
+export const getMouseOffsetCoord = (
+  ev: MouseEvent,
+  ref = ev.target as HTMLElement
+) => {
+  const coord = {} as MouseCoord;
+  if (ev instanceof TouchEvent) {
+    const rect = ref.getBoundingClientRect();
+    coord.offsetX = ev.changedTouches[0].pageX - rect.left;
+    coord.offsetY = ev.changedTouches[0].pageY - rect.top;
+  } else {
+    coord.offsetX = ev.offsetX;
+    coord.offsetY = ev.offsetY;
+  }
+
+  return coord;
+};
+
+const interrupts = new WeakMap<HTMLElement, () => void>();
+const oneClick = (
+  dom: HTMLElement,
+  callback: (result: {
+    data?: MouseCoord | null;
+    err?: string;
+    finish?: () => void;
+  }) => void
+) => {
+  const oldInterrupt = interrupts.get(dom);
+  if (oldInterrupt) {
+    oldInterrupt();
+  }
+
+  const clickHandler = (ev: MouseEvent) => {
+    callback({ finish, data: getMouseOffsetCoord(ev, dom) });
+  };
+
+  const interrupt = () => {
+    finish();
+    callback({ err: "cancel" });
+  };
+
+  let finished = false;
+  const finish = () => {
+    if (finished) return;
+    if (hasTouchEvents) {
+      dom.removeEventListener("touchend", clickHandler);
+    } else {
+      dom.removeEventListener("click", clickHandler);
+    }
+    interrupts.delete(dom);
+    finished = true;
+  };
+
+  interrupts.set(dom, interrupt);
+  if (hasTouchEvents) {
+    dom.addEventListener("touchend", clickHandler);
+  } else {
+    dom.addEventListener("click", clickHandler);
+  }
+
+  return interrupt;
+};
+
+export const singleClick = (
+  dom: HTMLElement,
+  callback: (data: MouseCoord | null, err?: string) => void
+) => {
+  return oneClick(dom, (result) => {
+    if (result.err) {
+      callback(null, result.err);
+    } else {
+      result.finish();
+      callback(result.data);
+    }
+  });
+};
+
+export const continueClick = (
+  dom: HTMLElement,
+  callback: (data: MouseCoord | null, err?: string) => void
+) => {
+  return oneClick(dom, (result) => {
+    if (result.err) {
+      callback(null, result.err);
+    } else {
+      callback(result.data);
+    }
+  });
+};

+ 39 - 0
src/board/helper/shape.ts

@@ -0,0 +1,39 @@
+import { ShapeType } from "../type";
+
+export const shapeTreeEq = (
+  parent: ShapeType,
+  eq: (shape: ShapeType) => boolean
+) => {
+  if (eq(parent)) {
+    return parent;
+  }
+
+  if ("children" in parent) {
+    for (const child of parent.children) {
+      const e = shapeTreeEq(child, eq);
+      if (e) {
+        return child;
+      }
+    }
+  }
+
+  return null;
+};
+
+export const shapeParentsEq = (
+  target: ShapeType,
+  eq: (shape: ShapeType) => boolean
+) => {
+  while (target) {
+    if (eq(target)) {
+      return target;
+    }
+    target = target.parent as any;
+  }
+  return null;
+};
+
+export const shapeTreeContain = (parent: ShapeType, target: ShapeType) => {
+  const eqShape = shapeTreeEq(parent, (shape) => shape === target);
+  return !!eqShape;
+};

+ 85 - 0
src/board/helper/stack-var.ts

@@ -0,0 +1,85 @@
+import { Container } from "../packages";
+
+type Var = { [key in string]: any };
+type Key = Container;
+
+const stackMap = new WeakMap<Key, Var[]>();
+const getStack = (key: Key) => {
+  const stack = stackMap.get(key);
+  return stack || null;
+};
+
+const getCurrentVar = (key: Key) => {
+  const stack = getStack(key);
+  if (stack) {
+    return stack[stack.length - 1];
+  } else {
+    throw "当前 key 不存在变量桟";
+  }
+};
+
+export const varMount = (key: Key) => {
+  const stack = getStack(key);
+  if (!stack) {
+    stackMap.set(key, []);
+  } else {
+    throw "当前 key 已挂载变量";
+  }
+};
+
+export const varPush = (key: Key, data: Var) => {
+  const stack = getStack(key);
+  if (stack) {
+    stack.push(data);
+  }
+};
+
+export const varPop = (key: Key) => {
+  const stack = getStack(key);
+  if (stack?.length) {
+    stack.pop();
+  }
+};
+
+export const varAdd = (key: Key, k: string, v: any) => {
+  const current = getCurrentVar(key);
+  if (k in current) {
+    throw `${key}变量已存在${current}`;
+  } else {
+    current[k] = v;
+  }
+};
+
+export const varSet = (key: Key, k: string, v: any) => {
+  const current = getCurrentVar(key);
+  if (k in current) {
+    throw `${key}变量已存在${current}`;
+  } else {
+    current[k] = v;
+  }
+};
+
+export const varDel = (key: Key, k: string) => {
+  const current = getCurrentVar(key);
+  if (!(k in current)) {
+    throw `${key}变量不存在${current}`;
+  } else {
+    delete current[k];
+  }
+};
+
+const keyStack: Key[] = [];
+export let currentVar: Var;
+export const useVar = (key: Key) => {
+  keyStack.push(key);
+  currentVar = getCurrentVar(keyStack[keyStack.length - 1]);
+
+  return () => {
+    keyStack.pop();
+    if (keyStack.length) {
+      currentVar = getCurrentVar(keyStack[keyStack.length - 1]);
+    } else {
+      currentVar = null;
+    }
+  };
+};

+ 91 - 0
src/board/helper/svg.ts

@@ -0,0 +1,91 @@
+import { Path } from "konva/lib/shapes/Path";
+import { Rect } from "konva/lib/shapes/Rect";
+import { getRealAbsoluteSize } from "./coord";
+import { Group } from "konva/lib/Group";
+import { KonvaEventObject } from "konva/lib/Node";
+
+export type PathsToActShapeProps = {
+  paths: string[];
+  size: number[];
+  realWidth?: number;
+  offset?: number[];
+  strokeWidth?: number;
+  fixed?: boolean;
+  strokeColor?: string;
+};
+export const pathsToActShape = (props: PathsToActShapeProps, test = false) => {
+  const size = props.size;
+  const realSize = props.realWidth || props.size[0];
+  const scale = realSize / size[0];
+  const realBound = size.map((p) => p * scale);
+  const offset = (props.offset || [0, 0]).map((v) => v * scale);
+  const strokeWidth = props.strokeWidth ? props.strokeWidth * scale : 1;
+  const strokeColor = props.strokeColor || "#000";
+  const paths = props.paths.map(
+    (data, ndx) =>
+      new Path({
+        data,
+        id: `path-${ndx}`,
+        name: `path`,
+        strokeScaleEnabled: !!props.fixed,
+        stroke: strokeColor,
+        fill: strokeColor,
+        scale: { x: scale, y: scale },
+        strokeWidth,
+      })
+  );
+  const rect = new Rect({
+    x: offset[0],
+    y: offset[1],
+    name: "rect",
+    width: realBound[0],
+    height: realBound[1],
+    fill: `rgba(0, 0, 0, ${test ? 0.3 : 0})`,
+  });
+
+  const setStyle = () => {
+    let [width, height] = getRealAbsoluteSize(group, [1, 1]);
+    group.scale({ x: width, y: height });
+  };
+
+  const offsetGroup = new Group();
+  offsetGroup.add(...paths, rect);
+  offsetGroup.x(-realBound[0] / 2);
+  offsetGroup.y(-realBound[1] / 2);
+
+  const group = new Group();
+  group.add(offsetGroup);
+
+  return {
+    getSize: () => {
+      const size = rect.getSize();
+      if (props.fixed) {
+        let [scale] = getRealAbsoluteSize(group, [1, 1]);
+        return [size.width * scale, size.height * scale];
+      }
+      return [size.width, size.height];
+    },
+    shape: group,
+    setData(data) {
+      group.position(data);
+      props.fixed && setStyle();
+    },
+    common() {
+      paths.forEach((path) => {
+        path.fill(strokeColor);
+      });
+    },
+  };
+};
+
+export const getTouchOffset = (ev: KonvaEventObject<any>) => {
+  const dom = ev.evt.target as HTMLElement;
+  const rect = dom.getBoundingClientRect();
+  const offsetX = ev.evt.changedTouches[0].pageX - rect.left;
+  const offsetY = ev.evt.changedTouches[0].pageY - rect.top;
+
+  return {
+    offsetX,
+    offsetY,
+  };
+};

+ 8 - 4
src/board/plugins/camera-plugin.ts

@@ -163,7 +163,7 @@ export class CameraQueryPlugin {
   }
 
   realBound: number[];
-  setBound(bound: number[], padding?: number | number[]) {
+  setBound(bound: number[], padding?: number | number[], retainScale = false) {
     padding = !Array.isArray(padding) ? [padding || 0, padding || 0] : padding;
     if (padding.length === 1) {
       padding.push(padding[0]);
@@ -184,9 +184,13 @@ export class CameraQueryPlugin {
     const effectiveHeight = this.tree.stage.height() - padding[1] * 2;
 
     // 计算缩放比例
-    const scaleX = effectiveWidth / realWidth;
-    const scaleY = effectiveHeight / realHeight;
-    const scale = Math.min(scaleX, scaleY); // 选择较小的比例以保持内容比例
+    let scaleX = effectiveWidth / realWidth;
+    let scaleY = effectiveHeight / realHeight;
+    if (retainScale) {
+      const scale = Math.min(scaleX, scaleY); // 选择较小的比例以保持内容比例
+      scaleX = scale;
+      scaleY = scale;
+    }
 
     const offsetX = (screenWidth - realWidth * scaleX) / 2 - bound[0] * scaleX;
     const offsetY =

+ 1 - 6
src/board/shared/entity-utils.ts

@@ -4,12 +4,7 @@ import { Container } from "../packages";
 import { EntityType, Entity } from "../packages/entity";
 import { Attrib, ShapeType } from "../type";
 import { getTouchOffset } from "./act";
-import {
-  createLineByDire,
-  getDireDist,
-  getLineIntersection,
-  getLineProjection,
-} from "./math";
+import { createLineByDire, getLineProjection } from "./math";
 import { inRevise } from "./public";
 import { disableMouse, enableMouse } from "./shape-mose";
 import { generateId, getChangePart } from "./util";

+ 29 - 0
src/board/shared/math.ts

@@ -149,10 +149,21 @@ export const getLineDireAngle = (line: number[], dire: number[]) => {
   return getDire2Angle(dire, getLineDire(line));
 };
 
+/**
+ * 获取线段中心点
+ * @param line
+ * @returns
+ */
 export const getLineCenter = (line: number[]) => {
   return [(line[0] + line[2]) / 2, (line[1] + line[3]) / 2];
 };
 
+/**
+ * 四舍五入
+ * @param num
+ * @param b 保留位数
+ * @returns
+ */
 export const round = (num: number, b: number = 2) => {
   const scale = Math.pow(10, b);
   return Math.round(num * scale) / scale;
@@ -371,3 +382,21 @@ export const polygonCounterclockwise = (points: number[][]) => {
   const polygon = points.map((point) => new Vector2(point[0], point[1]));
   return ShapeUtils.isClockWise(polygon);
 };
+
+export const getLineInnerContinuityPoints = (
+  line: number[],
+  unitSize: number[],
+  count = 5
+) => {
+  const points: number[][] = [];
+  for (let i = 0; i < count; i++) {
+    const offset = unitSize.map((p) => (p *= i + 1));
+    const point = getLineProjection(line, [
+      line[0] + offset[0],
+      line[1] + offset[1],
+    ]).point;
+
+    points.push(point);
+  }
+  return points;
+};