Selaa lähdekoodia

Canvas画布学习

zhanghaijun 2 vuotta sitten
vanhempi
commit
d6f50afdea

+ 11 - 0
train-web/src/pages/clothes/index.vue

@@ -239,6 +239,7 @@ function drop({ dataTransfer }: DragEvent, index: number) {
   flex-direction: column;
   border-right: 1px solid #ddd;
 }
+
 .tit {
   font-weight: bold;
   font-size: 18px;
@@ -252,15 +253,18 @@ function drop({ dataTransfer }: DragEvent, index: number) {
   height: 100%;
   overflow-y: scroll;
 }
+
 .patternWrapper {
   flex-wrap: wrap;
   margin: 15px 0px;
 }
+
 .pattern {
   width: 100%;
   cursor: pointer;
   transition: transform 100ms;
 }
+
 .pattern:hover {
   transform: scale(1.1);
 }
@@ -270,6 +274,7 @@ function drop({ dataTransfer }: DragEvent, index: number) {
   background-image: url("https://yxyy-pandora.oss-cn-beijing.aliyuncs.com/stamp-images/alpha_back.png");
   position: relative;
 }
+
 .tips {
   background-color: rgba(0, 0, 0, 0.6);
   color: #fff;
@@ -285,16 +290,19 @@ function drop({ dataTransfer }: DragEvent, index: number) {
   width: 350px;
   border-left: 1px solid #ddd;
 }
+
 #effect {
   background-color: antiquewhite;
   height: 350px;
 }
+
 #layers {
   flex: 1;
   padding: 0;
   margin: 0;
   overflow-y: scroll;
 }
+
 .layer {
   display: flex;
   align-items: center;
@@ -310,6 +318,7 @@ function drop({ dataTransfer }: DragEvent, index: number) {
   background-color: #f5f5f5;
   box-shadow: inset #ccc 0 1px 2px;
 }
+
 .thumbnail {
   width: 50px;
   background-image: url("https://yxyy-pandora.oss-cn-beijing.aliyuncs.com/stamp-images/alpha_back.png");
@@ -322,10 +331,12 @@ ul::-webkit-scrollbar,
 div::-webkit-scrollbar {
   width: 8px;
 }
+
 ul::-webkit-scrollbar-thumb,
 div::-webkit-scrollbar-thumb {
   -webkit-box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.2);
 }
+
 ul::-webkit-scrollbar-track,
 div::-webkit-scrollbar-track {
   -webkit-box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.2);

+ 16 - 16
train-web/src/pages/demo/examples/Camera.vue

@@ -3,37 +3,37 @@ import { onMounted, ref } from 'vue'
 import { Camera } from '../lmm/core/Camera'
 
 defineProps({
-	size: {
-		type: Object,
-		default: {
-			width: 0,
-			height: 0,
-		},
-	},
+  size: {
+    type: Object,
+    default: {
+      width: 1000,
+      height: 500,
+    },
+  },
 })
 
 const canvasRef = ref<HTMLCanvasElement>()
 
 /* 相机 */
-const camera = new Camera(-200, -200, 0.5)
+const camera = new Camera(-100, -20, 0.5)
 
 /* 矩阵测试 */
 function matrixTest(ctx: CanvasRenderingContext2D) {
-	ctx.save()
-	camera.transformInvert(ctx)
-	ctx.fillRect(0, 0, 200, 100)
-	ctx.restore()
+  ctx.save()
+  camera.transformInvert(ctx)
+  ctx.fillRect(0, 0, 200, 100)
+  ctx.restore()
 }
 
 onMounted(() => {
-	const canvas = canvasRef.value
-	const ctx = canvas?.getContext('2d')
-	ctx && matrixTest(ctx)
+  const canvas = canvasRef.value
+  const ctx = canvas?.getContext('2d')
+  ctx && matrixTest(ctx)
 })
 </script>
 
 <template>
-	<canvas ref="canvasRef" :width="size.width" :height="size.height"></canvas>
+  <canvas ref="canvasRef" :width="size.width" :height="size.height"></canvas>
 </template>
 
 <style scoped></style>

+ 28 - 28
train-web/src/pages/demo/examples/Group.vue

@@ -7,7 +7,7 @@ import { ImagePromises } from '../lmm/objects/ObjectUtils'
 
 // 获取父级属性
 defineProps({
-	size: { type: Object, default: { width: 0, height: 0 } },
+  size: { type: Object, default: { width: 1000, height: 500 } },
 })
 
 // 对应canvas 画布的Ref对象
@@ -15,46 +15,46 @@ const canvasRef = ref<HTMLCanvasElement>()
 
 const images: HTMLImageElement[] = []
 for (let i = 1; i < 8; i++) {
-	const image = new Image()
-	image.src = `https://yxyy-pandora.oss-cn-beijing.aliyuncs.com/stamp-images/${i}.png`
-	images.push(image)
+  const image = new Image()
+  image.src = `https://yxyy-pandora.oss-cn-beijing.aliyuncs.com/stamp-images/${i}.png`
+  images.push(image)
 }
 
 const group = new Group()
 
 /* 渲染 */
 function render(ctx: CanvasRenderingContext2D) {
-	group.add(
-		...images.map((image, i) => {
-			return new Img2D({
-				image,
-				position: new Vector2(200, 80 * i + 50),
-				size: new Vector2(image.width, image.height).multiplyScalar(0.3),
-				style: {
-					shadowColor: 'rgba(0,0,0,0.5)',
-					shadowBlur: 5,
-					shadowOffsetY: 20,
-				},
-			})
-		})
-	)
-	group.draw(ctx)
+  group.add(
+    ...images.map((image, i) => {
+      return new Img2D({
+        image,
+        position: new Vector2(200, 80 * i + 50),
+        size: new Vector2(image.width, image.height).multiplyScalar(0.3),
+        style: {
+          shadowColor: 'rgba(0,0,0,0.5)',
+          shadowBlur: 5,
+          shadowOffsetY: 20,
+        },
+      })
+    })
+  )
+  group.draw(ctx)
 }
 
 onMounted(() => {
-	const canvas = canvasRef.value
-	const ctx = canvas?.getContext('2d')
-	if (!ctx) {
-		return
-	}
-	Promise.all(ImagePromises(images)).then(() => {
-		render(ctx)
-	})
+  const canvas = canvasRef.value
+  const ctx = canvas?.getContext('2d')
+  if (!ctx) {
+    return
+  }
+  Promise.all(ImagePromises(images)).then(() => {
+    render(ctx)
+  })
 })
 </script>
 
 <template>
-	<canvas ref="canvasRef" :width="size.width" :height="size.height"></canvas>
+  <canvas ref="canvasRef" :width="size.width" :height="size.height"></canvas>
 </template>
 
 <style scoped></style>

+ 2 - 2
train-web/src/pages/demo/examples/HelloWorld.vue

@@ -6,8 +6,8 @@ defineProps({
   size: {
     type: Object,
     default: {
-      width: 0,
-      height: 0,
+      width: 1000,
+      height: 500,
     },
   },
 });

+ 1 - 1
train-web/src/pages/demo/examples/Img.vue

@@ -5,7 +5,7 @@ import { Img } from "../lmm/objects/Img";
 
 // 获取父级属性
 const props = defineProps({
-  size: { type: Object, default: { width: 0, height: 0 } },
+  size: { type: Object, default: { width: 1000, height: 500 } },
 });
 
 // 对应canvas 画布的Ref对象

+ 1 - 1
train-web/src/pages/demo/examples/ImgControler.vue

@@ -15,7 +15,7 @@ import { Object2D } from "../lmm/objects/Object2D";
 
 // 获取父级属性
 defineProps({
-  size: { type: Object, default: { width: 0, height: 0 } },
+  size: { type: Object, default: { width: 1000, height: 500 } },
 });
 // 鼠标样式
 const cursor = ref("default");

+ 70 - 70
train-web/src/pages/demo/examples/MatrixOfCanvas.vue

@@ -4,27 +4,27 @@ import { Vector2 } from '../lmm/math/Vector2'
 import { Matrix3 } from '../lmm/math/Matrix3'
 
 defineProps({
-	size: {
-		type: Object,
-		default: {
-			width: 0,
-			height: 0,
-		},
-	},
+  size: {
+    type: Object,
+    default: {
+      width: 1000,
+      height: 500,
+    },
+  },
 })
 
 const canvasRef = ref<HTMLCanvasElement>()
 
 /* 矩形的初始顶点 */
 const vertices = [
-	//1
-	0, 0,
-	//2
-	100, 0,
-	//3
-	100, 50,
-	//4
-	0, 50,
+  //1
+  0, 0,
+  //2
+  100, 0,
+  //3
+  100, 50,
+  //4
+  0, 50,
 ]
 
 /* 矩形的模型矩阵数据 */
@@ -34,85 +34,85 @@ const scale = new Vector2(1, 2)
 
 /* 矩阵 */
 /* const [pm, rm, sm] = [
-	new Matrix3().makeTranslation(position.x, position.y),
-	new Matrix3().makeRotation(rotate),
-	new Matrix3().makeScale(scale.x, scale.y),
+  new Matrix3().makeTranslation(position.x, position.y),
+  new Matrix3().makeRotation(rotate),
+  new Matrix3().makeScale(scale.x, scale.y),
 ]
 const matrix = pm.multiply(rm).multiply(sm) */
 const matrix = new Matrix3()
-	.scale(scale.x, scale.y)
-	.rotate(rotate)
-	.translate(position.x, position.y)
+  .scale(scale.x, scale.y)
+  .rotate(rotate)
+  .translate(position.x, position.y)
 
 /* 绘制矩形 */
 function drawRect(
-	ctx: CanvasRenderingContext2D,
-	vertices: number[],
-	color: string = 'black',
-	lineWidth: number = 0
+  ctx: CanvasRenderingContext2D,
+  vertices: number[],
+  color: string = 'black',
+  lineWidth: number = 0
 ) {
-	ctx.fillStyle = color
-	ctx.strokeStyle = color
-	ctx.lineWidth = lineWidth
-	ctx.beginPath()
-	ctx.moveTo(vertices[0], vertices[1])
-	for (let i = 2, len = vertices.length; i < len; i += 2) {
-		ctx.lineTo(vertices[i], vertices[i + 1])
-	}
-	ctx.closePath()
-	ctx.stroke()
-	ctx.fill()
+  ctx.fillStyle = color
+  ctx.strokeStyle = color
+  ctx.lineWidth = lineWidth
+  ctx.beginPath()
+  ctx.moveTo(vertices[0], vertices[1])
+  for (let i = 2, len = vertices.length; i < len; i += 2) {
+    ctx.lineTo(vertices[i], vertices[i + 1])
+  }
+  ctx.closePath()
+  ctx.stroke()
+  ctx.fill()
 }
 
 /* 矩阵测试 */
 function matrixTest(ctx: CanvasRenderingContext2D) {
-	/* translate(),rotate(),scale()测试 */
-	ctx.save()
-	ctx.translate(position.x, position.y)
-	/* 先缩放,后旋转 > 矩形 */
-	ctx.rotate(rotate)
-	ctx.scale(scale.x, scale.y)
-	/* 先旋转,后缩放 > 平行四边形 */
-	/* ctx.scale(scale.x, scale.y)
-	ctx.rotate(rotate) */
-	drawRect(ctx, vertices, 'black', 40)
-	ctx.restore()
+  /* translate(),rotate(),scale()测试 */
+  ctx.save()
+  ctx.translate(position.x, position.y)
+  /* 先缩放,后旋转 > 矩形 */
+  ctx.rotate(rotate)
+  ctx.scale(scale.x, scale.y)
+  /* 先旋转,后缩放 > 平行四边形 */
+  /* ctx.scale(scale.x, scale.y)
+  ctx.rotate(rotate) */
+  drawRect(ctx, vertices, 'black', 40)
+  ctx.restore()
 
-	/* 矩阵测试 */
-	const { elements: e } = matrix
-	console.log(e)
-	ctx.save()
-	ctx.transform(e[0], e[1], e[3], e[4], e[6], e[7])
-	drawRect(ctx, vertices, '#00acec', 20)
-	ctx.restore()
+  /* 矩阵测试 */
+  const { elements: e } = matrix
+  console.log(e)
+  ctx.save()
+  ctx.transform(e[0], e[1], e[3], e[4], e[6], e[7])
+  drawRect(ctx, vertices, '#00acec', 20)
+  ctx.restore()
 
-	/* 顶点变换 */
-	ctx.save()
-	drawRect(ctx, tranformVertices(vertices, matrix), '#acec00', 0)
-	ctx.restore()
+  /* 顶点变换 */
+  ctx.save()
+  drawRect(ctx, tranformVertices(vertices, matrix), '#acec00', 0)
+  ctx.restore()
 }
 
 /* 顶点变换 */
 function tranformVertices(vertices: number[], matrix: Matrix3) {
-	const worldVertives: number[] = []
-	for (let i = 0, len = vertices.length; i < len; i += 2) {
-		const { x, y } = new Vector2(vertices[i], vertices[i + 1]).applyMatrix3(
-			matrix
-		)
-		worldVertives.push(x, y)
-	}
-	return worldVertives
+  const worldVertives: number[] = []
+  for (let i = 0, len = vertices.length; i < len; i += 2) {
+    const { x, y } = new Vector2(vertices[i], vertices[i + 1]).applyMatrix3(
+      matrix
+    )
+    worldVertives.push(x, y)
+  }
+  return worldVertives
 }
 
 onMounted(() => {
-	const canvas = canvasRef.value
-	const ctx = canvas?.getContext('2d')
-	ctx && matrixTest(ctx)
+  const canvas = canvasRef.value
+  const ctx = canvas?.getContext('2d')
+  ctx && matrixTest(ctx)
 })
 </script>
 
 <template>
-	<canvas ref="canvasRef" :width="size.width" :height="size.height"></canvas>
+  <canvas ref="canvasRef" :width="size.width" :height="size.height"></canvas>
 </template>
 
 <style scoped></style>

+ 43 - 43
train-web/src/pages/demo/examples/OrbitControler.vue

@@ -7,7 +7,7 @@ import { Img2D } from '../lmm/objects/Img2D'
 
 // 获取父级属性
 defineProps({
-	size: { type: Object, default: { width: 0, height: 0 } },
+  size: { type: Object, default: { width: 1000, height: 500 } },
 })
 
 // 对应canvas 画布的Ref对象
@@ -27,60 +27,60 @@ scene.add(pattern)
 
 /* 测试 */
 function test(canvas: HTMLCanvasElement) {
-	const imgSize = new Vector2(image.width, image.height).multiplyScalar(0.6)
-	pattern.setOption({
-		/* 模型矩阵 */
-		rotate: 0.4,
-		position: new Vector2(0, -50),
-		scale: new Vector2(0.5),
+  const imgSize = new Vector2(image.width, image.height).multiplyScalar(0.6)
+  pattern.setOption({
+    /* 模型矩阵 */
+    rotate: 0.4,
+    position: new Vector2(0, -50),
+    scale: new Vector2(0.5),
 
-		/* Img属性 */
-		size: imgSize.clone(),
-		offset: imgSize.clone().multiplyScalar(-0.5),
-	})
+    /* Img属性 */
+    size: imgSize.clone(),
+    offset: imgSize.clone().multiplyScalar(-0.5),
+  })
 
-	/* 按需渲染 */
-	orbitControler.addEventListener('change', () => {
-		scene.render()
-	})
+  /* 按需渲染 */
+  orbitControler.addEventListener('change', () => {
+    scene.render()
+  })
 
-	/* 滑动滚轮缩放 */
-	canvas.addEventListener('wheel', ({ deltaY }) => {
-		orbitControler.doScale(deltaY)
-	})
+  /* 滑动滚轮缩放 */
+  canvas.addEventListener('wheel', ({ deltaY }) => {
+    orbitControler.doScale(deltaY)
+  })
 
-	/* 按住滚轮平移 */
-	canvas.addEventListener('pointerdown', (event: PointerEvent) => {
-		if (event.button == 1) {
-			orbitControler.pointerdown(event.clientX, event.clientY)
-		}
-	})
-	canvas.addEventListener('pointermove', (event: PointerEvent) => {
-		orbitControler.pointermove(event.clientX, event.clientY)
-	})
-	window.addEventListener('pointerup', (event: PointerEvent) => {
-		if (event.button == 1) {
-			orbitControler.pointerup()
-		}
-	})
+  /* 按住滚轮平移 */
+  canvas.addEventListener('pointerdown', (event: PointerEvent) => {
+    if (event.button == 1) {
+      orbitControler.pointerdown(event.clientX, event.clientY)
+    }
+  })
+  canvas.addEventListener('pointermove', (event: PointerEvent) => {
+    orbitControler.pointermove(event.clientX, event.clientY)
+  })
+  window.addEventListener('pointerup', (event: PointerEvent) => {
+    if (event.button == 1) {
+      orbitControler.pointerup()
+    }
+  })
 
-	/* 渲染 */
-	scene.render()
+  /* 渲染 */
+  scene.render()
 }
 
 onMounted(() => {
-	const canvas = canvasRef.value
-	if (canvas) {
-		scene.setOption({ canvas })
-		image.onload = function () {
-			test(canvas)
-		}
-	}
+  const canvas = canvasRef.value
+  if (canvas) {
+    scene.setOption({ canvas })
+    image.onload = function () {
+      test(canvas)
+    }
+  }
 })
 </script>
 
 <template>
-	<canvas ref="canvasRef" :width="size.width" :height="size.height"></canvas>
+  <canvas ref="canvasRef" :width="size.width" :height="size.height"></canvas>
 </template>
 
 <style scoped></style>

+ 47 - 47
train-web/src/pages/demo/examples/Scene.vue

@@ -6,7 +6,7 @@ import { Img2D } from '../lmm/objects/Img2D'
 
 // 获取父级属性
 const props = defineProps({
-	size: { type: Object, default: { width: 0, height: 0 } },
+  size: { type: Object, default: { width: 1000, height: 500 } },
 })
 
 // 对应canvas 画布的Ref对象
@@ -18,7 +18,7 @@ const scene = new Scene()
 /* 图案 */
 const image = new Image()
 image.src =
-	'https://yxyy-pandora.oss-cn-beijing.aliyuncs.com/stamp-images/1.png'
+  'https://yxyy-pandora.oss-cn-beijing.aliyuncs.com/stamp-images/1.png'
 const pattern = new Img2D({ image })
 scene.add(pattern)
 
@@ -27,66 +27,66 @@ const mouseClipPos = new Vector2(Infinity)
 
 /* 测试 */
 function test(canvas: HTMLCanvasElement) {
-	const imgSize = new Vector2(image.width, image.height).multiplyScalar(0.6)
-	pattern.setOption({
-		/* 模型矩阵 */
-		rotate: 0.4,
-		position: new Vector2(0, -50),
-		scale: new Vector2(0.5),
+  const imgSize = new Vector2(image.width, image.height).multiplyScalar(0.6)
+  pattern.setOption({
+    /* 模型矩阵 */
+    rotate: 0.4,
+    position: new Vector2(0, -50),
+    scale: new Vector2(0.5),
 
-		/* Img属性 */
-		size: imgSize.clone(),
-		offset: imgSize.clone().multiplyScalar(-0.5),
+    /* Img属性 */
+    size: imgSize.clone(),
+    offset: imgSize.clone().multiplyScalar(-0.5),
 
-		/* 样式 */
-		style: {
-			globalAlpha: 0.8,
-			shadowColor: 'rgba(0,0,0,0.5)',
-			shadowBlur: 0,
-		},
-	})
+    /* 样式 */
+    style: {
+      globalAlpha: 0.8,
+      shadowColor: 'rgba(0,0,0,0.5)',
+      shadowBlur: 0,
+    },
+  })
 
-	/* 相机位移测试 */
-	scene.camera.position.set(0, 100)
+  /* 相机位移测试 */
+  scene.camera.position.set(0, 100)
 
-	/* 记录鼠标的裁剪坐标位 */
-	canvas.addEventListener('mousemove', ({ clientX, clientY }) => {
-		mouseClipPos.copy(scene.clientToClip(clientX, clientY))
-	})
+  /* 记录鼠标的裁剪坐标位 */
+  canvas.addEventListener('mousemove', ({ clientX, clientY }) => {
+    mouseClipPos.copy(scene.clientToClip(clientX, clientY))
+  })
 
-	/* 动画 */
-	ani()
+  /* 动画 */
+  ani()
 }
 
 function ani(time = 0) {
-	/* 相机缩放测试 */
-	const inter = (Math.sin(time * 0.002) + 1) / 2
-	scene.camera.zoom = inter + 0.5
-	/* 投影 */
-	pattern.style.shadowOffsetY = 80 * (1 - inter)
-	pattern.style.shadowBlur = 10 * (1 - inter)
-	/* 选择测试 */
-	if (scene.isPointInObj(pattern, mouseClipPos, pattern.pvmoMatrix)) {
-		pattern.rotate += 0.02
-	}
-	/* 渲染 */
-	scene.render()
-	requestAnimationFrame(ani)
+  /* 相机缩放测试 */
+  const inter = (Math.sin(time * 0.002) + 1) / 2
+  scene.camera.zoom = inter + 0.5
+  /* 投影 */
+  pattern.style.shadowOffsetY = 80 * (1 - inter)
+  pattern.style.shadowBlur = 10 * (1 - inter)
+  /* 选择测试 */
+  if (scene.isPointInObj(pattern, mouseClipPos, pattern.pvmoMatrix)) {
+    pattern.rotate += 0.02
+  }
+  /* 渲染 */
+  scene.render()
+  requestAnimationFrame(ani)
 }
 
 onMounted(() => {
-	const canvas = canvasRef.value
-	if (canvas) {
-		scene.setOption({ canvas })
-		image.onload = function () {
-			test(canvas)
-		}
-	}
+  const canvas = canvasRef.value
+  if (canvas) {
+    scene.setOption({ canvas })
+    image.onload = function () {
+      test(canvas)
+    }
+  }
 })
 </script>
 
 <template>
-	<canvas ref="canvasRef" :width="size.width" :height="size.height"></canvas>
+  <canvas ref="canvasRef" :width="size.width" :height="size.height"></canvas>
 </template>
 
 <style scoped></style>

+ 263 - 0
train-web/src/pages/demo/lmm/controler/Frame.ts

@@ -0,0 +1,263 @@
+import { Matrix3 } from '../math/Matrix3'
+import { Vector2 } from '../math/Vector2'
+import { Img } from '../objects/Img'
+import { crtPath, crtPathByMatrix } from '../objects/ObjectUtils'
+
+const pi2 = Math.PI * 2
+
+//在哪个坐标系绘制控制框
+//moMatrix:偏移坐标转世界坐标
+//pvmoMatrix:偏移坐标转裁剪坐标
+type Leve = 'moMatrix' | 'pvmoMatrix'
+
+//参数类型
+type FrameType = {
+  img?: Img
+  level?: Leve
+}
+
+/* 鼠标状态 */
+export type State = 'scale' | 'scaleX' | 'scaleY' | 'rotate' | 'move' | null
+
+/* 布尔变量 */
+let _bool: Boolean = false
+
+/* 虚拟canvas上下文对象 */
+const ctx = document
+  .createElement('canvas')
+  .getContext('2d') as CanvasRenderingContext2D
+
+class Frame {
+  _img = new Img()
+  // 图案边框的顶点集合
+  vertives: number[] = []
+  // 图案中点
+  center = new Vector2()
+  // 路径变换矩阵
+  matrix = new Matrix3()
+  // 要把路径变换到哪个坐标系中,默认裁剪坐标系
+  level = 'pvmoMatrix'
+  // 对面节点
+  opposite = new Vector2()
+
+  // 描边色
+  strokeStyle = '#558ef0'
+  // 填充色
+  fillStyle = '#fff'
+
+  constructor(attr: FrameType = {}) {
+    for (let [key, val] of Object.entries(attr)) {
+      this[key] = val
+    }
+  }
+
+  get img() {
+    return this._img
+  }
+  set img(val) {
+    this._img = val
+    this.updateShape()
+  }
+
+  /* 更新矩阵、路径初始顶点、中点 */
+  updateShape() {
+    const {
+      vertives: fv,
+      center,
+      img,
+      level,
+      img: {
+        size: { x: imgW, y: imgH },
+      },
+    } = this
+
+    const vertices = [
+      0,
+      0,
+      imgW / 2,
+      0,
+      imgW,
+      0,
+      imgW,
+      imgH / 2,
+      imgW,
+      imgH,
+      imgW / 2,
+      imgH,
+      0,
+      imgH,
+      0,
+      imgH / 2,
+    ]
+
+    /* 更新路径变换矩阵 */
+    this.matrix = img[level]
+    for (let i = 0, len = vertices.length; i < len; i += 2) {
+      const { x, y } = new Vector2(vertices[i], vertices[i + 1]).applyMatrix3(
+        this.matrix
+      )
+      /* 更新路径顶点 */
+      fv[i] = x
+      fv[i + 1] = y
+    }
+    /* 更新中点 */
+    center.copy(new Vector2(fv[0], fv[1]).lerp(new Vector2(fv[8], fv[9]), 0.5))
+  }
+
+  /* 获取变换状态 */
+  getMouseState(mp: Vector2): State {
+    const { vertives: fv, opposite } = this
+
+    /* 对角线距离 */
+    const diagonal = new Vector2(fv[0] - fv[8], fv[1] - fv[9]).length()
+
+    /* 判断缩放的距离 */
+    const scaleDist = Math.min(24, diagonal / 3)
+
+    /* x,y缩放 */
+    for (let i = 0, len = fv.length; i < len; i += 4) {
+      if (new Vector2(fv[i], fv[i + 1]).sub(mp).length() < scaleDist) {
+        const ind = (i + 8) % 16
+        opposite.set(fv[ind], fv[ind + 1])
+        return 'scale'
+      }
+    }
+
+    /* y向缩放 */
+    ctx.save()
+    ctx.lineWidth = scaleDist
+    ctx.beginPath()
+    crtPath(ctx, [fv[0], fv[1], fv[4], fv[5]])
+    _bool = ctx.isPointInStroke(mp.x, mp.y)
+    ctx.restore()
+    if (_bool) {
+      opposite.set(fv[10], fv[11])
+      return 'scaleY'
+    }
+
+    ctx.save()
+    ctx.lineWidth = scaleDist
+    ctx.beginPath()
+    crtPath(ctx, [fv[8], fv[9], fv[12], fv[13]])
+    _bool = ctx.isPointInStroke(mp.x, mp.y)
+    ctx.restore()
+    if (_bool) {
+      opposite.set(fv[2], fv[3])
+      return 'scaleY'
+    }
+
+    /* x向缩放 */
+    ctx.save()
+    ctx.lineWidth = scaleDist
+    ctx.beginPath()
+    crtPath(ctx, [fv[12], fv[13], fv[0], fv[1]])
+    _bool = ctx.isPointInStroke(mp.x, mp.y)
+    ctx.restore()
+    if (_bool) {
+      opposite.set(fv[6], fv[7])
+      return 'scaleX'
+    }
+
+    ctx.save()
+    ctx.lineWidth = scaleDist
+    ctx.beginPath()
+    crtPath(ctx, [fv[4], fv[5], fv[8], fv[9]])
+    _bool = ctx.isPointInStroke(mp.x, mp.y)
+    ctx.restore()
+    if (_bool) {
+      opposite.set(fv[14], fv[15])
+      return 'scaleX'
+    }
+
+    /* 移动 */
+    ctx.beginPath()
+    crtPath(ctx, fv)
+    if (ctx.isPointInPath(mp.x, mp.y)) {
+      return 'move'
+    }
+
+    /* 旋转 */
+    ctx.save()
+    ctx.lineWidth = 80
+    ctx.beginPath()
+    crtPath(ctx, fv, true)
+    _bool = ctx.isPointInStroke(mp.x, mp.y)
+    ctx.restore()
+    if (_bool) {
+      return 'rotate'
+    }
+
+    /* 无状态 */
+    return null
+  }
+
+  draw(ctx: CanvasRenderingContext2D) {
+    this.updateShape()
+    const {
+      img: { size },
+      vertives: fv,
+      center,
+      matrix,
+      strokeStyle,
+      fillStyle,
+    } = this
+
+    /* 图案尺寸的一半 */
+    const [halfWidth, halfheight] = [size.width / 2, size.height / 2]
+
+    /* 绘图 */
+    ctx.save()
+    ctx.strokeStyle = strokeStyle
+    ctx.fillStyle = fillStyle
+
+    /* 矩形框 */
+    ctx.beginPath()
+    crtPath(
+      ctx,
+      [fv[0], fv[1], fv[4], fv[5], fv[8], fv[9], fv[12], fv[13]],
+      true
+    )
+    ctx.stroke()
+
+    /* 矩形节点 */
+    const { elements: e } = matrix
+    // 矩阵内的缩放量
+    const sx = new Vector2(e[0], e[1]).length()
+    const sy = new Vector2(e[3], e[4]).length()
+    // 节点尺寸,消去缩放量
+    const pointSize = new Vector2(8 / sx, 8 / sy)
+
+    //节点尺寸的一半
+    const [w, h] = [pointSize.x / 2, pointSize.y / 2]
+
+    // 绘制节点
+    ctx.beginPath()
+    for (let y = 0; y < 3; y++) {
+      for (let x = 0; x < 3; x++) {
+        if (y === 1 && x === 1) {
+          continue
+        }
+        //节点位置
+        const [bx, by] = [halfWidth * x, halfheight * y]
+        crtPathByMatrix(
+          ctx,
+          [bx - w, by - h, bx + w, by - h, bx + w, by + h, bx - w, by + h],
+          matrix,
+          true
+        )
+      }
+    }
+    ctx.fill()
+    ctx.stroke()
+
+    /* 中点 */
+    ctx.beginPath()
+    ctx.arc(center.x, center.y, 5, 0, pi2)
+    ctx.fill()
+    ctx.stroke()
+
+    ctx.restore()
+  }
+}
+
+export { Frame }

+ 291 - 292
train-web/src/pages/demo/lmm/controler/ImgControler.ts

@@ -1,8 +1,7 @@
 import { Vector2 } from '../math/Vector2'
-import { Object2D, Object2DType } from '../objects/Object2D'
+import { Object2D } from '../objects/Object2D'
 import { Img } from '../objects/Img'
 import { Matrix3 } from '../math/Matrix3'
-import { Scene } from '../core/Scene'
 import { Frame, State } from './Frame'
 import { MouseShape } from './MouseShape'
 import { ImgTransformer, ImgData } from './ImgTransformer'
@@ -11,320 +10,320 @@ const _changeEvent = { type: 'change' }
 
 /* 变换之前的暂存数据类型 */
 type TransformStage = {
-	clipCenter: Vector2
-	clipOpposite: Vector2
-	parentPvmInvert: Matrix3
+  clipCenter: Vector2
+  clipOpposite: Vector2
+  parentPvmInvert: Matrix3
 }
 
 class ImgControler extends Object2D {
-	// 要控制的图片
-	_img: Img | null = null
-	// 图案控制框
-	frame = new Frame()
-	// 渲染顺序
-	index = Infinity
-	// 不受相机影响
-	enableCamera = false
+  // 要控制的图片
+  _img: Img | null = null
+  // 图案控制框
+  frame = new Frame()
+  // 渲染顺序
+  index = Infinity
+  // 不受相机影响
+  enableCamera = false
 
-	// 鼠标状态
-	mouseState: State = null
+  // 鼠标状态
+  mouseState: State = null
 
-	// 鼠标的裁剪坐标位
-	clipMousePos = new Vector2()
+  // 鼠标的裁剪坐标位
+  clipMousePos = new Vector2()
 
-	// 鼠标图案
-	mouseShape = new MouseShape({
-		vertives: this.frame.vertives,
-		center: this.frame.center,
-		mousePos: this.clipMousePos,
-	})
+  // 鼠标图案
+  mouseShape = new MouseShape({
+    vertives: this.frame.vertives,
+    center: this.frame.center,
+    mousePos: this.clipMousePos,
+  })
 
-	// 控制状态
-	_controlState: State = null
-	// alt 键是否按下
-	_altKey = false
-	// shift 键是否按下
-	shiftKey = false
-	// 图案在父级坐标系内的变换基点
-	origin = new Vector2()
-	// 图案在裁剪坐标系内的变换基点
-	clipOrigin = new Vector2()
+  // 控制状态
+  _controlState: State = null
+  // alt 键是否按下
+  _altKey = false
+  // shift 键是否按下
+  shiftKey = false
+  // 图案在父级坐标系内的变换基点
+  origin = new Vector2()
+  // 图案在裁剪坐标系内的变换基点
+  clipOrigin = new Vector2()
 
-	// 鼠标在图案父级坐标系内的坐标位
-	parentMousePos = new Vector2()
-	// 选中图案时的暂存数据,用于取消变换
-	controlStage: ImgData = {
-		position: new Vector2(),
-		scale: new Vector2(1, 1),
-		rotate: 0,
-		offset: new Vector2(),
-	}
-	// 变换前的暂存数据,用于设置变换基点,将裁剪坐标转图案父级坐标
-	transformStage: TransformStage = {
-		clipCenter: new Vector2(),
-		clipOpposite: new Vector2(),
-		parentPvmInvert: new Matrix3(),
-	}
-	// 图案变换器
-	imgTransformer = new ImgTransformer({
-		mousePos: this.parentMousePos,
-		orign: this.origin,
-	})
+  // 鼠标在图案父级坐标系内的坐标位
+  parentMousePos = new Vector2()
+  // 选中图案时的暂存数据,用于取消变换
+  controlStage: ImgData = {
+    position: new Vector2(),
+    scale: new Vector2(1, 1),
+    rotate: 0,
+    offset: new Vector2(),
+  }
+  // 变换前的暂存数据,用于设置变换基点,将裁剪坐标转图案父级坐标
+  transformStage: TransformStage = {
+    clipCenter: new Vector2(),
+    clipOpposite: new Vector2(),
+    parentPvmInvert: new Matrix3(),
+  }
+  // 图案变换器
+  imgTransformer = new ImgTransformer({
+    mousePos: this.parentMousePos,
+    orign: this.origin,
+  })
 
-	get img() {
-		return this._img
-	}
-	set img(val) {
-		if (this._img === val) {
-			return
-		}
-		this._img = val
-		if (val) {
-			this.imgTransformer.setOption({ img: val })
-			this.imgTransformer.passImgDataTo(this.controlStage)
-			this.frame.img = val
-			this.dispatchEvent({ type: 'selected', img: val })
-		} else {
-			this.mouseState = null
-			this.controlState = null
-		}
-		this.dispatchEvent(_changeEvent)
-	}
+  get img() {
+    return this._img
+  }
+  set img(val) {
+    if (this._img === val) {
+      return
+    }
+    this._img = val
+    if (val) {
+      this.imgTransformer.setOption({ img: val })
+      this.imgTransformer.passImgDataTo(this.controlStage)
+      this.frame.img = val
+      this.dispatchEvent({ type: 'selected', img: val })
+    } else {
+      this.mouseState = null
+      this.controlState = null
+    }
+    this.dispatchEvent(_changeEvent)
+  }
 
-	get controlState() {
-		return this._controlState
-	}
-	set controlState(val) {
-		if (this._controlState === val) {
-			return
-		}
-		this._controlState = val
-		const { img } = this
-		if (!val || !img) {
-			return
-		}
-		// 暂存变换数据
-		this.saveTransformData(img)
-		if (val === 'move') {
-			return
-		}
-		// 设置变换基点
-		if (val === 'rotate') {
-			this.setRotateOrigin()
-		} else if (val?.includes('scale')) {
-			this.setScaleOrigin()
-		}
-		// 在不改变图案世界位的前提下,基于变换基点,偏移图案
-		this.offsetImgByOrigin(img)
-	}
+  get controlState() {
+    return this._controlState
+  }
+  set controlState(val) {
+    if (this._controlState === val) {
+      return
+    }
+    this._controlState = val
+    const { img } = this
+    if (!val || !img) {
+      return
+    }
+    // 暂存变换数据
+    this.saveTransformData(img)
+    if (val === 'move') {
+      return
+    }
+    // 设置变换基点
+    if (val === 'rotate') {
+      this.setRotateOrigin()
+    } else if (val?.includes('scale')) {
+      this.setScaleOrigin()
+    }
+    // 在不改变图案世界位的前提下,基于变换基点,偏移图案
+    this.offsetImgByOrigin(img)
+  }
 
-	get altKey() {
-		return this._altKey
-	}
-	set altKey(val) {
-		if (this._altKey === val) {
-			return
-		}
-		this._altKey = val
-		const { img, controlState, origin, imgTransformer } = this
-		if (!img) {
-			return
-		}
-		if (controlState?.includes('scale')) {
-			// 将图案回退到变换之前的状态
-			imgTransformer.restoreImg()
-			// 根据alt键设置缩放基点
-			this.setScaleOrigin()
-			// 根据变换基点,偏移图案
-			this.offsetImgByOrigin(img)
-			// 变换图案
-			this.transformImg()
-		}
-	}
+  get altKey() {
+    return this._altKey
+  }
+  set altKey(val) {
+    if (this._altKey === val) {
+      return
+    }
+    this._altKey = val
+    const { img, controlState, origin, imgTransformer } = this
+    if (!img) {
+      return
+    }
+    if (controlState?.includes('scale')) {
+      // 将图案回退到变换之前的状态
+      imgTransformer.restoreImg()
+      // 根据alt键设置缩放基点
+      this.setScaleOrigin()
+      // 根据变换基点,偏移图案
+      this.offsetImgByOrigin(img)
+      // 变换图案
+      this.transformImg()
+    }
+  }
 
-	/* 暂存变换数据 */
-	saveTransformData(img: Img) {
-		const {
-			clipMousePos,
-			imgTransformer,
-			frame,
-			transformStage: { clipCenter, clipOpposite, parentPvmInvert },
-		} = this
-		const { parent } = img
-		parent && parentPvmInvert.copy(parent.pvmMatrix.invert())
-		clipCenter.copy(frame.center)
-		clipOpposite.copy(frame.opposite)
-		imgTransformer.setOption({
-			img,
-			mouseStart: clipMousePos.clone().applyMatrix3(parentPvmInvert),
-		})
-	}
+  /* 暂存变换数据 */
+  saveTransformData(img: Img) {
+    const {
+      clipMousePos,
+      imgTransformer,
+      frame,
+      transformStage: { clipCenter, clipOpposite, parentPvmInvert },
+    } = this
+    const { parent } = img
+    parent && parentPvmInvert.copy(parent.pvmMatrix.invert())
+    clipCenter.copy(frame.center)
+    clipOpposite.copy(frame.opposite)
+    imgTransformer.setOption({
+      img,
+      mouseStart: clipMousePos.clone().applyMatrix3(parentPvmInvert),
+    })
+  }
 
-	/* 设置缩放基点 */
-	setScaleOrigin() {
-		const {
-			altKey,
-			origin,
-			imgTransformer,
-			clipOrigin,
-			transformStage: { clipCenter, clipOpposite, parentPvmInvert },
-		} = this
-		// 根据altKey,将图案中心点或对点从裁剪坐标系转图案父级坐标系
-		if (altKey) {
-			clipOrigin.copy(clipCenter)
-			origin.copy(clipCenter.clone().applyMatrix3(parentPvmInvert))
-		} else {
-			clipOrigin.copy(clipOpposite)
-			origin.copy(clipOpposite.clone().applyMatrix3(parentPvmInvert))
-		}
-		// 更新父级坐标系里基点到鼠标起点的向量
-		imgTransformer.updateOriginToMouseStart()
-	}
+  /* 设置缩放基点 */
+  setScaleOrigin() {
+    const {
+      altKey,
+      origin,
+      imgTransformer,
+      clipOrigin,
+      transformStage: { clipCenter, clipOpposite, parentPvmInvert },
+    } = this
+    // 根据altKey,将图案中心点或对点从裁剪坐标系转图案父级坐标系
+    if (altKey) {
+      clipOrigin.copy(clipCenter)
+      origin.copy(clipCenter.clone().applyMatrix3(parentPvmInvert))
+    } else {
+      clipOrigin.copy(clipOpposite)
+      origin.copy(clipOpposite.clone().applyMatrix3(parentPvmInvert))
+    }
+    // 更新父级坐标系里基点到鼠标起点的向量
+    imgTransformer.updateOriginToMouseStart()
+  }
 
-	/* 设置旋转基点 */
-	setRotateOrigin() {
-		const {
-			origin,
-			imgTransformer,
-			clipOrigin,
-			transformStage: { clipCenter, parentPvmInvert },
-		} = this
-		// 图案基点在裁剪坐标系中的位置
-		clipOrigin.copy(clipCenter)
-		// 将图案中心点从裁剪坐标系转父级坐标系
-		origin.copy(clipCenter.clone().applyMatrix3(parentPvmInvert))
-		// 更新父级坐标系里基点到鼠标起点的向量
-		imgTransformer.updateOriginToMouseStart()
-	}
+  /* 设置旋转基点 */
+  setRotateOrigin() {
+    const {
+      origin,
+      imgTransformer,
+      clipOrigin,
+      transformStage: { clipCenter, parentPvmInvert },
+    } = this
+    // 图案基点在裁剪坐标系中的位置
+    clipOrigin.copy(clipCenter)
+    // 将图案中心点从裁剪坐标系转父级坐标系
+    origin.copy(clipCenter.clone().applyMatrix3(parentPvmInvert))
+    // 更新父级坐标系里基点到鼠标起点的向量
+    imgTransformer.updateOriginToMouseStart()
+  }
 
-	/* 基点变换 */
-	offsetImgByOrigin(img: Img) {
-		const { offset, position, scale, rotate, pvmMatrix } = img
-		// 偏移量
-		const curOffset = new Vector2().subVectors(
-			offset,
-			this.clipOrigin.clone().applyMatrix3(pvmMatrix.invert())
-		)
-		// 当前偏移和原有偏移的向量差
-		const diff = new Vector2().subVectors(curOffset, offset)
-		// 图案的offset需要基于curOffset 做反向偏移
-		offset.copy(curOffset)
-		// 上一级的position 再偏移回来,以确保图案的世界位不变
-		position.sub(diff.multiply(scale).rotate(rotate))
-	}
+  /* 基点变换 */
+  offsetImgByOrigin(img: Img) {
+    const { offset, position, scale, rotate, pvmMatrix } = img
+    // 偏移量
+    const curOffset = new Vector2().subVectors(
+      offset,
+      this.clipOrigin.clone().applyMatrix3(pvmMatrix.invert())
+    )
+    // 当前偏移和原有偏移的向量差
+    const diff = new Vector2().subVectors(curOffset, offset)
+    // 图案的offset需要基于curOffset 做反向偏移
+    offset.copy(curOffset)
+    // 上一级的position 再偏移回来,以确保图案的世界位不变
+    position.sub(diff.multiply(scale).rotate(rotate))
+  }
 
-	/* 鼠标按下 */
-	pointerdown(img: Img | null, mp: Vector2) {
-		if (!this.mouseState) {
-			this.img = img
-			if (!this.img) {
-				return
-			}
-		}
-		// 更新鼠标裁剪坐标位
-		this.clipMousePos.copy(mp)
-		// 获取鼠标状态
-		this.mouseState = this.frame.getMouseState(mp)
-		if (this.mouseState) {
-			// 控制状态等于鼠标状态
-			this.controlState = this.mouseState
-			// 更新鼠标父级位
-			this.updateParentMousePos()
-		}
+  /* 鼠标按下 */
+  pointerdown(img: Img | null, mp: Vector2) {
+    if (!this.mouseState) {
+      this.img = img
+      if (!this.img) {
+        return
+      }
+    }
+    // 更新鼠标裁剪坐标位
+    this.clipMousePos.copy(mp)
+    // 获取鼠标状态
+    this.mouseState = this.frame.getMouseState(mp)
+    if (this.mouseState) {
+      // 控制状态等于鼠标状态
+      this.controlState = this.mouseState
+      // 更新鼠标父级位
+      this.updateParentMousePos()
+    }
 
-		this.dispatchEvent(_changeEvent)
-	}
+    this.dispatchEvent(_changeEvent)
+  }
 
-	/* 键盘按下 */
-	keydown(key: string, altKey: boolean, shiftKey: boolean) {
-		this.shiftKey = shiftKey
-		this.altKey = altKey
-		if (this.img) {
-			switch (key) {
-				case 'Escape':
-					// 将选中图案时存储的图案变换数据controlStage 拷贝到图案中
-					this.imgTransformer.copyImgData(this.controlStage)
-					// 图案置空
-					this.img = null
-					break
-				case 'Enter':
-					// 图案置空
-					this.img = null
-					break
-				case 'Delete':
-					// 将img从其所在的group中删除
-					this.img.remove()
-					// 图案置空
-					this.img = null
-					break
-			}
-		}
-		this.dispatchEvent(_changeEvent)
-	}
+  /* 键盘按下 */
+  keydown(key: string, altKey: boolean, shiftKey: boolean) {
+    this.shiftKey = shiftKey
+    this.altKey = altKey
+    if (this.img) {
+      switch (key) {
+        case 'Escape':
+          // 将选中图案时存储的图案变换数据controlStage 拷贝到图案中
+          this.imgTransformer.copyImgData(this.controlStage)
+          // 图案置空
+          this.img = null
+          break
+        case 'Enter':
+          // 图案置空
+          this.img = null
+          break
+        case 'Delete':
+          // 将img从其所在的group中删除
+          this.img.remove()
+          // 图案置空
+          this.img = null
+          break
+      }
+    }
+    this.dispatchEvent(_changeEvent)
+  }
 
-	/* 键盘抬起 */
-	keyup(altKey: boolean, shiftKey: boolean) {
-		this.shiftKey = shiftKey
-		this.altKey = altKey
-		this.dispatchEvent(_changeEvent)
-	}
+  /* 键盘抬起 */
+  keyup(altKey: boolean, shiftKey: boolean) {
+    this.shiftKey = shiftKey
+    this.altKey = altKey
+    this.dispatchEvent(_changeEvent)
+  }
 
-	/* 更新鼠标在图案父级坐标系中的位置 */
-	updateParentMousePos() {
-		const {
-			clipMousePos,
-			parentMousePos,
-			transformStage: { parentPvmInvert },
-		} = this
-		parentMousePos.copy(clipMousePos.clone().applyMatrix3(parentPvmInvert))
-	}
+  /* 更新鼠标在图案父级坐标系中的位置 */
+  updateParentMousePos() {
+    const {
+      clipMousePos,
+      parentMousePos,
+      transformStage: { parentPvmInvert },
+    } = this
+    parentMousePos.copy(clipMousePos.clone().applyMatrix3(parentPvmInvert))
+  }
 
-	/* 鼠标移动 */
-	pointermove(mp: Vector2) {
-		if (!this.img) {
-			return
-		}
-		// 更新鼠标裁剪坐标位
-		this.clipMousePos.copy(mp)
-		if (this.controlState) {
-			// 更新鼠标在图案父级坐标系中的位置
-			this.updateParentMousePos()
-			// 变换图案
-			this.transformImg()
-		} else {
-			// 获取鼠标状态
-			this.mouseState = this.frame.getMouseState(mp)
-		}
+  /* 鼠标移动 */
+  pointermove(mp: Vector2) {
+    if (!this.img) {
+      return
+    }
+    // 更新鼠标裁剪坐标位
+    this.clipMousePos.copy(mp)
+    if (this.controlState) {
+      // 更新鼠标在图案父级坐标系中的位置
+      this.updateParentMousePos()
+      // 变换图案
+      this.transformImg()
+    } else {
+      // 获取鼠标状态
+      this.mouseState = this.frame.getMouseState(mp)
+    }
 
-		this.dispatchEvent(_changeEvent)
-	}
+    this.dispatchEvent(_changeEvent)
+  }
 
-	/* 鼠标抬起 */
-	pointerup() {
-		if (this.controlState) {
-			this.controlState = null
-			this.dispatchEvent(_changeEvent)
-		}
-	}
+  /* 鼠标抬起 */
+  pointerup() {
+    if (this.controlState) {
+      this.controlState = null
+      this.dispatchEvent(_changeEvent)
+    }
+  }
 
-	/* 变换图案 */
-	transformImg() {
-		const { imgTransformer, controlState, shiftKey, img } = this
-		controlState && imgTransformer[controlState + Number(shiftKey)]()
-		this.dispatchEvent({ type: 'transformed', img })
-	}
+  /* 变换图案 */
+  transformImg() {
+    const { imgTransformer, controlState, shiftKey, img } = this
+    controlState && imgTransformer[controlState + Number(shiftKey)]()
+    this.dispatchEvent({ type: 'transformed', img })
+  }
 
-	/* 绘图 */
-	draw(ctx: CanvasRenderingContext2D) {
-		const { img } = this
-		if (!img) {
-			return
-		}
-		const { frame, mouseShape, mouseState } = this
-		/* 绘制外框 */
-		frame.draw(ctx)
-		/* 绘制鼠标图案 */
-		mouseShape.draw(ctx, mouseState)
-	}
+  /* 绘图 */
+  draw(ctx: CanvasRenderingContext2D) {
+    const { img } = this
+    if (!img) {
+      return
+    }
+    const { frame, mouseShape, mouseState } = this
+    /* 绘制外框 */
+    frame.draw(ctx)
+    /* 绘制鼠标图案 */
+    mouseShape.draw(ctx, mouseState)
+  }
 }
 export { ImgControler }

+ 176 - 0
train-web/src/pages/demo/lmm/controler/ImgTransformer.ts

@@ -0,0 +1,176 @@
+import { Vector2 } from '../math/Vector2'
+import { Img } from '../objects/Img'
+
+/* PI*2 */
+const pi2 = Math.PI * 2
+
+/* 图案数据类型 */
+export type ImgData = {
+  position: Vector2
+  scale: Vector2
+  rotate: number
+  offset: Vector2
+}
+
+type ImgTransformerType = {
+  img?: Img
+  orign?: Vector2
+  mousePos?: Vector2
+  mouseStart?: Vector2
+  uniformRotateAng?: number
+}
+
+class ImgTransformer {
+  /* 变换图案 */
+  img = new Img()
+
+  /* 暂存图案的变换信息 */
+  position = new Vector2()
+  scale = new Vector2(1, 1)
+  rotate = 0
+  offset = new Vector2()
+
+  /* 图案的变换基点 */
+  orign = new Vector2()
+
+  /* 图案父级坐标系里的鼠标数据 */
+  // 鼠标位置
+  mousePos = new Vector2()
+  // 鼠标起始位
+  mouseStart = new Vector2()
+  // mouseStart减orign
+  originToMouseStart = new Vector2()
+
+  /* 等量旋转时的旋转弧度 */
+  uniformRotateAng = pi2 / 24
+
+  constructor(attr: ImgTransformerType = {}) {
+    this.setOption(attr)
+  }
+
+  /* 设置属性 */
+  setOption(attr: ImgTransformerType = {}) {
+    Object.assign(this, attr)
+    const { img, mouseStart, orign } = attr
+    img && this.passImgDataTo()
+    if (orign || mouseStart) {
+      this.updateOriginToMouseStart(mouseStart, orign)
+    }
+  }
+
+  /* 变换基点到鼠标起点的向量 */
+  updateOriginToMouseStart(mouseStart = this.mouseStart, orign = this.orign) {
+    this.originToMouseStart.subVectors(mouseStart, orign)
+  }
+
+  /* 把img变换数据传递给obj */
+  passImgDataTo(obj: ImgData = this) {
+    const { position, scale, rotate, offset } = this.img
+    obj.position.copy(position)
+    obj.scale.copy(scale)
+    obj.rotate = rotate
+    obj.offset.copy(offset)
+  }
+
+  /* 将图案回退到变换之前的状态 */
+  restoreImg() {
+    this.copyImgData(this)
+  }
+
+  // 将obj中的变换数据拷贝到img中
+  copyImgData(obj: ImgData) {
+    const { position, scale, rotate, offset } = obj
+    const { img } = this
+    img.position.copy(position)
+    img.scale.copy(scale)
+    img.rotate = rotate
+    img.offset.copy(offset)
+  }
+
+  /* 双向缩放 */
+  scale0() {
+    const { img, scale } = this
+    img.scale.copy(scale.clone().multiply(this.getLocalScale()))
+  }
+
+  /* 获取图案本地的缩放量 */
+  getLocalScale() {
+    const { img, orign, originToMouseStart, mousePos } = this
+    const rotateInvert = -img.rotate
+    return mousePos
+      .clone()
+      .sub(orign)
+      .rotate(rotateInvert)
+      .divide(originToMouseStart.clone().rotate(rotateInvert))
+  }
+
+  /* 双向等比缩放 */
+  scale1() {
+    const { img, scale } = this
+    const s = this.getLocalScale()
+    img.scale.copy(scale.clone().multiplyScalar((s.x + s.y) / 2))
+  }
+
+  /* 单向缩放 */
+  scaleX0() {
+    this.doScaleSigleDir('x')
+  }
+  scaleY0() {
+    this.doScaleSigleDir('y')
+  }
+
+  doScaleSigleDir(dir: 'x' | 'y') {
+    const { img, scale } = this
+    const s = this.getLocalScale()
+    img.scale[dir] = scale[dir] * s[dir]
+  }
+
+  /* 单向等比缩放 */
+  scaleX1() {
+    this.doUniformScaleSigleDir('x')
+  }
+  scaleY1() {
+    this.doUniformScaleSigleDir('y')
+  }
+
+  doUniformScaleSigleDir(dir: 'x' | 'y') {
+    const { img, scale } = this
+    const s = this.getLocalScale()
+    img.scale.copy(scale.clone().multiplyScalar(s[dir]))
+  }
+
+  /* 旋转 */
+  rotate0() {
+    const { img, rotate, orign, originToMouseStart, mousePos } = this
+    img.rotate =
+      rotate + mousePos.clone().sub(orign).angle() - originToMouseStart.angle()
+  }
+
+  /* 等量旋转 */
+  rotate1() {
+    const {
+      img,
+      rotate,
+      orign,
+      originToMouseStart,
+      mousePos,
+      uniformRotateAng,
+    } = this
+    const ang = mousePos.clone().sub(orign).angle() - originToMouseStart.angle()
+    img.rotate =
+      rotate +
+      Math.floor((ang + uniformRotateAng / 2) / uniformRotateAng) *
+      uniformRotateAng
+  }
+
+  /* 位移 */
+  // 自由位移
+  move0() {
+    const { img, position, mouseStart, mousePos } = this
+    img.position.copy(position.clone().add(mousePos.clone().sub(mouseStart)))
+  }
+  // 正交位移-作业,留给同学们实现
+  move1() { }
+}
+
+export { ImgTransformer }

+ 24 - 24
train-web/src/pages/demo/lmm/core/Camera.ts

@@ -2,30 +2,30 @@ import { Matrix3 } from '../math/Matrix3'
 import { Vector2 } from '../math/Vector2'
 
 class Camera {
-	position: Vector2
-	zoom: number
+  position: Vector2
+  zoom: number
 
-	constructor(x = 0, y = 0, zoom = 1) {
-		this.position = new Vector2(x, y)
-		this.zoom = zoom
-	}
-	/* 视图投影矩阵:先逆向缩放,再逆向位移 */
-	get pvMatrix() {
-		const {
-			position: { x, y },
-			zoom,
-		} = this
-		return new Matrix3().scale(1 / zoom).translate(-x, -y)
-	}
-	/* 使用视图投影矩阵变换物体*/
-	transformInvert(ctx: CanvasRenderingContext2D) {
-		const {
-			position: { x, y },
-			zoom,
-		} = this
-		const scale = 1 / zoom
-		ctx.translate(-x, -y)
-		ctx.scale(scale, scale)
-	}
+  constructor(x = 0, y = 0, zoom = 1) {
+    this.position = new Vector2(x, y)
+    this.zoom = zoom
+  }
+  /* 视图投影矩阵:先逆向缩放,再逆向位移 */
+  get pvMatrix() {
+    const {
+      position: { x, y },
+      zoom,
+    } = this
+    return new Matrix3().scale(1 / zoom).translate(-x, -y)
+  }
+  /* 使用视图投影矩阵变换物体*/
+  transformInvert(ctx: CanvasRenderingContext2D) {
+    const {
+      position: { x, y },
+      zoom,
+    } = this
+    const scale = 1 / zoom
+    ctx.translate(-x, -y)
+    ctx.scale(scale, scale)
+  }
 }
 export { Camera }

+ 296 - 296
train-web/src/pages/demo/lmm/math/Matrix3.ts

@@ -1,307 +1,307 @@
 class Matrix3 {
-	elements: number[] = [1, 0, 0, 0, 1, 0, 0, 0, 1]
-	set(
-		n11: number,
-		n12: number,
-		n13: number,
-		n21: number,
-		n22: number,
-		n23: number,
-		n31: number,
-		n32: number,
-		n33: number
-	) {
-		const te = this.elements
-		te[0] = n11
-		te[1] = n21
-		te[2] = n31
-		te[3] = n12
-		te[4] = n22
-		te[5] = n32
-		te[6] = n13
-		te[7] = n23
-		te[8] = n33
-		return this
-	}
-	identity() {
-		this.set(1, 0, 0, 0, 1, 0, 0, 0, 1)
-
-		return this
-	}
-
-	copy(m: Matrix3) {
-		const te = this.elements
-		const me = m.elements
-
-		te[0] = me[0]
-		te[1] = me[1]
-		te[2] = me[2]
-		te[3] = me[3]
-		te[4] = me[4]
-		te[5] = me[5]
-		te[6] = me[6]
-		te[7] = me[7]
-		te[8] = me[8]
-
-		return this
-	}
-
-	setFromMatrix4(m: Matrix3) {
-		const me = m.elements
-
-		this.set(me[0], me[4], me[8], me[1], me[5], me[9], me[2], me[6], me[10])
-
-		return this
-	}
-
-	multiply(m: Matrix3) {
-		return this.multiplyMatrices(this, m)
-	}
-
-	premultiply(m: Matrix3) {
-		return this.multiplyMatrices(m, this)
-	}
-
-	multiplyMatrices(a: Matrix3, b: Matrix3) {
-		const ae = a.elements
-		const be = b.elements
-		const te = this.elements
-
-		const a11 = ae[0],
-			a12 = ae[3],
-			a13 = ae[6]
-		const a21 = ae[1],
-			a22 = ae[4],
-			a23 = ae[7]
-		const a31 = ae[2],
-			a32 = ae[5],
-			a33 = ae[8]
-
-		const b11 = be[0],
-			b12 = be[3],
-			b13 = be[6]
-		const b21 = be[1],
-			b22 = be[4],
-			b23 = be[7]
-		const b31 = be[2],
-			b32 = be[5],
-			b33 = be[8]
-
-		te[0] = a11 * b11 + a12 * b21 + a13 * b31
-		te[3] = a11 * b12 + a12 * b22 + a13 * b32
-		te[6] = a11 * b13 + a12 * b23 + a13 * b33
-
-		te[1] = a21 * b11 + a22 * b21 + a23 * b31
-		te[4] = a21 * b12 + a22 * b22 + a23 * b32
-		te[7] = a21 * b13 + a22 * b23 + a23 * b33
-
-		te[2] = a31 * b11 + a32 * b21 + a33 * b31
-		te[5] = a31 * b12 + a32 * b22 + a33 * b32
-		te[8] = a31 * b13 + a32 * b23 + a33 * b33
-
-		return this
-	}
-
-	multiplyScalar(s: number) {
-		const te = this.elements
-
-		te[0] *= s
-		te[3] *= s
-		te[6] *= s
-		te[1] *= s
-		te[4] *= s
-		te[7] *= s
-		te[2] *= s
-		te[5] *= s
-		te[8] *= s
-
-		return this
-	}
-
-	determinant() {
-		const te = this.elements
-
-		const a = te[0],
-			b = te[1],
-			c = te[2],
-			d = te[3],
-			e = te[4],
-			f = te[5],
-			g = te[6],
-			h = te[7],
-			i = te[8]
-
-		return a * e * i - a * f * h - b * d * i + b * f * g + c * d * h - c * e * g
-	}
-
-	invert() {
-		const te = this.elements,
-			n11 = te[0],
-			n21 = te[1],
-			n31 = te[2],
-			n12 = te[3],
-			n22 = te[4],
-			n32 = te[5],
-			n13 = te[6],
-			n23 = te[7],
-			n33 = te[8],
-			t11 = n33 * n22 - n32 * n23,
-			t12 = n32 * n13 - n33 * n12,
-			t13 = n23 * n12 - n22 * n13,
-			det = n11 * t11 + n21 * t12 + n31 * t13
-
-		if (det === 0) return this.set(0, 0, 0, 0, 0, 0, 0, 0, 0)
-
-		const detInv = 1 / det
-
-		te[0] = t11 * detInv
-		te[1] = (n31 * n23 - n33 * n21) * detInv
-		te[2] = (n32 * n21 - n31 * n22) * detInv
-
-		te[3] = t12 * detInv
-		te[4] = (n33 * n11 - n31 * n13) * detInv
-		te[5] = (n31 * n12 - n32 * n11) * detInv
-
-		te[6] = t13 * detInv
-		te[7] = (n21 * n13 - n23 * n11) * detInv
-		te[8] = (n22 * n11 - n21 * n12) * detInv
-
-		return this
-	}
-
-	transpose() {
-		let tmp
-		const m = this.elements
-
-		tmp = m[1]
-		m[1] = m[3]
-		m[3] = tmp
-		tmp = m[2]
-		m[2] = m[6]
-		m[6] = tmp
-		tmp = m[5]
-		m[5] = m[7]
-		m[7] = tmp
-
-		return this
-	}
-
-	transposeIntoArray(r: number[]) {
-		const m = this.elements
-
-		r[0] = m[0]
-		r[1] = m[3]
-		r[2] = m[6]
-		r[3] = m[1]
-		r[4] = m[4]
-		r[5] = m[7]
-		r[6] = m[2]
-		r[7] = m[5]
-		r[8] = m[8]
-
-		return this
-	}
-
-	setUvTransform(
-		tx: number,
-		ty: number,
-		sx: number,
-		sy: number,
-		rotation: number,
-		cx: number,
-		cy: number
-	) {
-		const c = Math.cos(rotation)
-		const s = Math.sin(rotation)
-
-		this.set(
-			sx * c,
-			sx * s,
-			-sx * (c * cx + s * cy) + cx + tx,
-			-sy * s,
-			sy * c,
-			-sy * (-s * cx + c * cy) + cy + ty,
-			0,
-			0,
-			1
-		)
-
-		return this
-	}
-
-	scale(sx: number, sy: number = sx) {
-		this.premultiply(_m3.makeScale(sx, sy))
-
-		return this
-	}
-
-	rotate(theta: number) {
-		this.premultiply(_m3.makeRotation(theta))
-
-		return this
-	}
-
-	translate(tx: number, ty: number) {
-		this.premultiply(_m3.makeTranslation(tx, ty))
-
-		return this
-	}
-
-	// for 2D Transforms
+  elements: number[] = [1, 0, 0, 0, 1, 0, 0, 0, 1]
+  set(
+    n11: number,
+    n12: number,
+    n13: number,
+    n21: number,
+    n22: number,
+    n23: number,
+    n31: number,
+    n32: number,
+    n33: number
+  ) {
+    const te = this.elements
+    te[0] = n11
+    te[1] = n21
+    te[2] = n31
+    te[3] = n12
+    te[4] = n22
+    te[5] = n32
+    te[6] = n13
+    te[7] = n23
+    te[8] = n33
+    return this
+  }
+  identity() {
+    this.set(1, 0, 0, 0, 1, 0, 0, 0, 1)
+
+    return this
+  }
+
+  copy(m: Matrix3) {
+    const te = this.elements
+    const me = m.elements
+
+    te[0] = me[0]
+    te[1] = me[1]
+    te[2] = me[2]
+    te[3] = me[3]
+    te[4] = me[4]
+    te[5] = me[5]
+    te[6] = me[6]
+    te[7] = me[7]
+    te[8] = me[8]
+
+    return this
+  }
+
+  setFromMatrix4(m: Matrix3) {
+    const me = m.elements
+
+    this.set(me[0], me[4], me[8], me[1], me[5], me[9], me[2], me[6], me[10])
+
+    return this
+  }
+
+  multiply(m: Matrix3) {
+    return this.multiplyMatrices(this, m)
+  }
+
+  premultiply(m: Matrix3) {
+    return this.multiplyMatrices(m, this)
+  }
+
+  multiplyMatrices(a: Matrix3, b: Matrix3) {
+    const ae = a.elements
+    const be = b.elements
+    const te = this.elements
+
+    const a11 = ae[0],
+      a12 = ae[3],
+      a13 = ae[6]
+    const a21 = ae[1],
+      a22 = ae[4],
+      a23 = ae[7]
+    const a31 = ae[2],
+      a32 = ae[5],
+      a33 = ae[8]
+
+    const b11 = be[0],
+      b12 = be[3],
+      b13 = be[6]
+    const b21 = be[1],
+      b22 = be[4],
+      b23 = be[7]
+    const b31 = be[2],
+      b32 = be[5],
+      b33 = be[8]
+
+    te[0] = a11 * b11 + a12 * b21 + a13 * b31
+    te[3] = a11 * b12 + a12 * b22 + a13 * b32
+    te[6] = a11 * b13 + a12 * b23 + a13 * b33
+
+    te[1] = a21 * b11 + a22 * b21 + a23 * b31
+    te[4] = a21 * b12 + a22 * b22 + a23 * b32
+    te[7] = a21 * b13 + a22 * b23 + a23 * b33
+
+    te[2] = a31 * b11 + a32 * b21 + a33 * b31
+    te[5] = a31 * b12 + a32 * b22 + a33 * b32
+    te[8] = a31 * b13 + a32 * b23 + a33 * b33
+
+    return this
+  }
+
+  multiplyScalar(s: number) {
+    const te = this.elements
+
+    te[0] *= s
+    te[3] *= s
+    te[6] *= s
+    te[1] *= s
+    te[4] *= s
+    te[7] *= s
+    te[2] *= s
+    te[5] *= s
+    te[8] *= s
+
+    return this
+  }
+
+  determinant() {
+    const te = this.elements
+
+    const a = te[0],
+      b = te[1],
+      c = te[2],
+      d = te[3],
+      e = te[4],
+      f = te[5],
+      g = te[6],
+      h = te[7],
+      i = te[8]
+
+    return a * e * i - a * f * h - b * d * i + b * f * g + c * d * h - c * e * g
+  }
+
+  invert() {
+    const te = this.elements,
+      n11 = te[0],
+      n21 = te[1],
+      n31 = te[2],
+      n12 = te[3],
+      n22 = te[4],
+      n32 = te[5],
+      n13 = te[6],
+      n23 = te[7],
+      n33 = te[8],
+      t11 = n33 * n22 - n32 * n23,
+      t12 = n32 * n13 - n33 * n12,
+      t13 = n23 * n12 - n22 * n13,
+      det = n11 * t11 + n21 * t12 + n31 * t13
+
+    if (det === 0) return this.set(0, 0, 0, 0, 0, 0, 0, 0, 0)
+
+    const detInv = 1 / det
+
+    te[0] = t11 * detInv
+    te[1] = (n31 * n23 - n33 * n21) * detInv
+    te[2] = (n32 * n21 - n31 * n22) * detInv
+
+    te[3] = t12 * detInv
+    te[4] = (n33 * n11 - n31 * n13) * detInv
+    te[5] = (n31 * n12 - n32 * n11) * detInv
+
+    te[6] = t13 * detInv
+    te[7] = (n21 * n13 - n23 * n11) * detInv
+    te[8] = (n22 * n11 - n21 * n12) * detInv
+
+    return this
+  }
+
+  transpose() {
+    let tmp
+    const m = this.elements
+
+    tmp = m[1]
+    m[1] = m[3]
+    m[3] = tmp
+    tmp = m[2]
+    m[2] = m[6]
+    m[6] = tmp
+    tmp = m[5]
+    m[5] = m[7]
+    m[7] = tmp
+
+    return this
+  }
+
+  transposeIntoArray(r: number[]) {
+    const m = this.elements
+
+    r[0] = m[0]
+    r[1] = m[3]
+    r[2] = m[6]
+    r[3] = m[1]
+    r[4] = m[4]
+    r[5] = m[7]
+    r[6] = m[2]
+    r[7] = m[5]
+    r[8] = m[8]
+
+    return this
+  }
+
+  setUvTransform(
+    tx: number,
+    ty: number,
+    sx: number,
+    sy: number,
+    rotation: number,
+    cx: number,
+    cy: number
+  ) {
+    const c = Math.cos(rotation)
+    const s = Math.sin(rotation)
+
+    this.set(
+      sx * c,
+      sx * s,
+      -sx * (c * cx + s * cy) + cx + tx,
+      -sy * s,
+      sy * c,
+      -sy * (-s * cx + c * cy) + cy + ty,
+      0,
+      0,
+      1
+    )
+
+    return this
+  }
+
+  scale(sx: number, sy: number = sx) {
+    this.premultiply(_m3.makeScale(sx, sy))
+
+    return this
+  }
+
+  rotate(theta: number) {
+    this.premultiply(_m3.makeRotation(theta))
+
+    return this
+  }
+
+  translate(tx: number, ty: number) {
+    this.premultiply(_m3.makeTranslation(tx, ty))
+
+    return this
+  }
+
+  // for 2D Transforms
 
-	makeTranslation(x: number, y: number) {
-		this.set(1, 0, x, 0, 1, y, 0, 0, 1)
+  makeTranslation(x: number, y: number) {
+    this.set(1, 0, x, 0, 1, y, 0, 0, 1)
 
-		return this
-	}
-
-	makeRotation(theta: number) {
-		// counterclockwise
-
-		const c = Math.cos(theta)
-		const s = Math.sin(theta)
-
-		this.set(c, -s, 0, s, c, 0, 0, 0, 1)
-
-		return this
-	}
-
-	makeScale(x: number, y: number) {
-		this.set(x, 0, 0, 0, y, 0, 0, 0, 1)
-
-		return this
-	}
-
-	setTranslation(x: number, y: number) {
-		const { elements } = this
-		elements[6] = x
-		elements[7] = y
-
-		return this
-	}
+    return this
+  }
+
+  makeRotation(theta: number) {
+    // counterclockwise
+
+    const c = Math.cos(theta)
+    const s = Math.sin(theta)
+
+    this.set(c, -s, 0, s, c, 0, 0, 0, 1)
+
+    return this
+  }
+
+  makeScale(x: number, y: number) {
+    this.set(x, 0, 0, 0, y, 0, 0, 0, 1)
+
+    return this
+  }
+
+  setTranslation(x: number, y: number) {
+    const { elements } = this
+    elements[6] = x
+    elements[7] = y
+
+    return this
+  }
 
-	equals(matrix: Matrix3) {
-		const te = this.elements
-		const me = matrix.elements
+  equals(matrix: Matrix3) {
+    const te = this.elements
+    const me = matrix.elements
 
-		for (let i = 0; i < 9; i++) {
-			if (te[i] !== me[i]) return false
-		}
+    for (let i = 0; i < 9; i++) {
+      if (te[i] !== me[i]) return false
+    }
 
-		return true
-	}
-
-	fromArray(array: number[], offset = 0) {
-		for (let i = 0; i < 9; i++) {
-			this.elements[i] = array[i + offset]
-		}
+    return true
+  }
+
+  fromArray(array: number[], offset = 0) {
+    for (let i = 0; i < 9; i++) {
+      this.elements[i] = array[i + offset]
+    }
 
-		return this
-	}
+    return this
+  }
 
-	clone() {
-		return new Matrix3().fromArray(this.elements)
-	}
+  clone() {
+    return new Matrix3().fromArray(this.elements)
+  }
 }
 const _m3 = new Matrix3()
 export { Matrix3 }

+ 297 - 297
train-web/src/pages/demo/lmm/math/Vector2.ts

@@ -1,375 +1,375 @@
 import { Matrix3 } from './Matrix3'
 
 class Vector2 {
-	x = 0
-	y = 0
-	readonly isVector2 = true
-	constructor(x = 0, y = x) {
-		this.x = x
-		this.y = y
-	}
-
-	get width() {
-		return this.x
-	}
-
-	set width(value) {
-		this.x = value
-	}
-
-	get height() {
-		return this.y
-	}
-
-	set height(value) {
-		this.y = value
-	}
-
-	set(x: number, y = x) {
-		this.x = x
-		this.y = y
-
-		return this
-	}
-
-	setScalar(scalar: number) {
-		this.x = scalar
-		this.y = scalar
-
-		return this
-	}
-
-	setX(x: number) {
-		this.x = x
-
-		return this
-	}
-
-	setY(y: number) {
-		this.y = y
+  x = 0
+  y = 0
+  readonly isVector2 = true
+  constructor(x = 0, y = x) {
+    this.x = x
+    this.y = y
+  }
+
+  get width() {
+    return this.x
+  }
+
+  set width(value) {
+    this.x = value
+  }
+
+  get height() {
+    return this.y
+  }
+
+  set height(value) {
+    this.y = value
+  }
+
+  set(x: number, y = x) {
+    this.x = x
+    this.y = y
+
+    return this
+  }
+
+  setScalar(scalar: number) {
+    this.x = scalar
+    this.y = scalar
+
+    return this
+  }
+
+  setX(x: number) {
+    this.x = x
+
+    return this
+  }
+
+  setY(y: number) {
+    this.y = y
 
-		return this
-	}
+    return this
+  }
 
-	setComponent(index: number, value: number) {
-		switch (index) {
-			case 0:
-				this.x = value
-				break
-			case 1:
-				this.y = value
-				break
-			default:
-				throw new Error('index is out of range: ' + index)
-		}
+  setComponent(index: number, value: number) {
+    switch (index) {
+      case 0:
+        this.x = value
+        break
+      case 1:
+        this.y = value
+        break
+      default:
+        throw new Error('index is out of range: ' + index)
+    }
 
-		return this
-	}
+    return this
+  }
 
-	getComponent(index: number) {
-		switch (index) {
-			case 0:
-				return this.x
-			case 1:
-				return this.y
-			default:
-				throw new Error('index is out of range: ' + index)
-		}
-	}
+  getComponent(index: number) {
+    switch (index) {
+      case 0:
+        return this.x
+      case 1:
+        return this.y
+      default:
+        throw new Error('index is out of range: ' + index)
+    }
+  }
 
-	clone() {
-		return new Vector2(this.x, this.y)
-	}
+  clone() {
+    return new Vector2(this.x, this.y)
+  }
 
-	copy(v: Vector2) {
-		this.x = v.x
-		this.y = v.y
+  copy(v: Vector2) {
+    this.x = v.x
+    this.y = v.y
 
-		return this
-	}
+    return this
+  }
 
-	add(v: Vector2) {
-		this.x += v.x
-		this.y += v.y
+  add(v: Vector2) {
+    this.x += v.x
+    this.y += v.y
 
-		return this
-	}
+    return this
+  }
 
-	addScalar(s: number) {
-		this.x += s
-		this.y += s
+  addScalar(s: number) {
+    this.x += s
+    this.y += s
 
-		return this
-	}
+    return this
+  }
 
-	addVectors(a: Vector2, b: Vector2) {
-		this.x = a.x + b.x
-		this.y = a.y + b.y
+  addVectors(a: Vector2, b: Vector2) {
+    this.x = a.x + b.x
+    this.y = a.y + b.y
 
-		return this
-	}
+    return this
+  }
 
-	addScaledVector(v: Vector2, s: number) {
-		this.x += v.x * s
-		this.y += v.y * s
+  addScaledVector(v: Vector2, s: number) {
+    this.x += v.x * s
+    this.y += v.y * s
 
-		return this
-	}
+    return this
+  }
 
-	sub(v: Vector2) {
-		this.x -= v.x
-		this.y -= v.y
+  sub(v: Vector2) {
+    this.x -= v.x
+    this.y -= v.y
 
-		return this
-	}
+    return this
+  }
 
-	subScalar(s: number) {
-		this.x -= s
-		this.y -= s
+  subScalar(s: number) {
+    this.x -= s
+    this.y -= s
 
-		return this
-	}
+    return this
+  }
 
-	subVectors(a: Vector2, b: Vector2) {
-		this.x = a.x - b.x
-		this.y = a.y - b.y
+  subVectors(a: Vector2, b: Vector2) {
+    this.x = a.x - b.x
+    this.y = a.y - b.y
 
-		return this
-	}
+    return this
+  }
 
-	multiply(v: Vector2) {
-		this.x *= v.x
-		this.y *= v.y
+  multiply(v: Vector2) {
+    this.x *= v.x
+    this.y *= v.y
 
-		return this
-	}
+    return this
+  }
 
-	multiplyScalar(scalar: number) {
-		this.x *= scalar
-		this.y *= scalar
+  multiplyScalar(scalar: number) {
+    this.x *= scalar
+    this.y *= scalar
 
-		return this
-	}
+    return this
+  }
 
-	divide(v: Vector2) {
-		this.x /= v.x
-		this.y /= v.y
+  divide(v: Vector2) {
+    this.x /= v.x
+    this.y /= v.y
 
-		return this
-	}
+    return this
+  }
 
-	divideScalar(scalar: number) {
-		return this.multiplyScalar(1 / scalar)
-	}
+  divideScalar(scalar: number) {
+    return this.multiplyScalar(1 / scalar)
+  }
 
-	min(v: Vector2) {
-		this.x = Math.min(this.x, v.x)
-		this.y = Math.min(this.y, v.y)
+  min(v: Vector2) {
+    this.x = Math.min(this.x, v.x)
+    this.y = Math.min(this.y, v.y)
 
-		return this
-	}
+    return this
+  }
 
-	max(v: Vector2) {
-		this.x = Math.max(this.x, v.x)
-		this.y = Math.max(this.y, v.y)
+  max(v: Vector2) {
+    this.x = Math.max(this.x, v.x)
+    this.y = Math.max(this.y, v.y)
 
-		return this
-	}
+    return this
+  }
 
-	clamp(min: Vector2, max: Vector2) {
-		// assumes min < max, componentwise
+  clamp(min: Vector2, max: Vector2) {
+    // assumes min < max, componentwise
 
-		this.x = Math.max(min.x, Math.min(max.x, this.x))
-		this.y = Math.max(min.y, Math.min(max.y, this.y))
+    this.x = Math.max(min.x, Math.min(max.x, this.x))
+    this.y = Math.max(min.y, Math.min(max.y, this.y))
 
-		return this
-	}
+    return this
+  }
 
-	clampScalar(minVal: number, maxVal: number) {
-		this.x = Math.max(minVal, Math.min(maxVal, this.x))
-		this.y = Math.max(minVal, Math.min(maxVal, this.y))
+  clampScalar(minVal: number, maxVal: number) {
+    this.x = Math.max(minVal, Math.min(maxVal, this.x))
+    this.y = Math.max(minVal, Math.min(maxVal, this.y))
 
-		return this
-	}
+    return this
+  }
 
-	clampLength(min: number, max: number) {
-		const length = this.length()
+  clampLength(min: number, max: number) {
+    const length = this.length()
 
-		return this.divideScalar(length || 1).multiplyScalar(
-			Math.max(min, Math.min(max, length))
-		)
-	}
+    return this.divideScalar(length || 1).multiplyScalar(
+      Math.max(min, Math.min(max, length))
+    )
+  }
 
-	floor() {
-		this.x = Math.floor(this.x)
-		this.y = Math.floor(this.y)
+  floor() {
+    this.x = Math.floor(this.x)
+    this.y = Math.floor(this.y)
 
-		return this
-	}
+    return this
+  }
 
-	ceil() {
-		this.x = Math.ceil(this.x)
-		this.y = Math.ceil(this.y)
+  ceil() {
+    this.x = Math.ceil(this.x)
+    this.y = Math.ceil(this.y)
 
-		return this
-	}
+    return this
+  }
 
-	round() {
-		this.x = Math.round(this.x)
-		this.y = Math.round(this.y)
+  round() {
+    this.x = Math.round(this.x)
+    this.y = Math.round(this.y)
 
-		return this
-	}
+    return this
+  }
 
-	roundToZero() {
-		this.x = this.x < 0 ? Math.ceil(this.x) : Math.floor(this.x)
-		this.y = this.y < 0 ? Math.ceil(this.y) : Math.floor(this.y)
+  roundToZero() {
+    this.x = this.x < 0 ? Math.ceil(this.x) : Math.floor(this.x)
+    this.y = this.y < 0 ? Math.ceil(this.y) : Math.floor(this.y)
 
-		return this
-	}
+    return this
+  }
 
-	negate() {
-		this.x = -this.x
-		this.y = -this.y
+  negate() {
+    this.x = -this.x
+    this.y = -this.y
 
-		return this
-	}
+    return this
+  }
 
-	dot(v: Vector2) {
-		return this.x * v.x + this.y * v.y
-	}
+  dot(v: Vector2) {
+    return this.x * v.x + this.y * v.y
+  }
 
-	cross(v: Vector2) {
-		return this.x * v.y - this.y * v.x
-	}
+  cross(v: Vector2) {
+    return this.x * v.y - this.y * v.x
+  }
 
-	lengthSq() {
-		return this.x * this.x + this.y * this.y
-	}
+  lengthSq() {
+    return this.x * this.x + this.y * this.y
+  }
 
-	length() {
-		return Math.sqrt(this.x * this.x + this.y * this.y)
-	}
+  length() {
+    return Math.sqrt(this.x * this.x + this.y * this.y)
+  }
 
-	manhattanLength() {
-		return Math.abs(this.x) + Math.abs(this.y)
-	}
+  manhattanLength() {
+    return Math.abs(this.x) + Math.abs(this.y)
+  }
 
-	normalize() {
-		return this.divideScalar(this.length() || 1)
-	}
+  normalize() {
+    return this.divideScalar(this.length() || 1)
+  }
 
-	angle() {
-		// computes the angle in radians with respect to the positive x-axis
+  angle() {
+    // computes the angle in radians with respect to the positive x-axis
 
-		const angle = Math.atan2(-this.y, -this.x) + Math.PI
+    const angle = Math.atan2(-this.y, -this.x) + Math.PI
 
-		return angle
-	}
+    return angle
+  }
 
-	distanceTo(v: Vector2) {
-		return Math.sqrt(this.distanceToSquared(v))
-	}
+  distanceTo(v: Vector2) {
+    return Math.sqrt(this.distanceToSquared(v))
+  }
 
-	distanceToSquared(v: Vector2) {
-		const dx = this.x - v.x,
-			dy = this.y - v.y
-		return dx * dx + dy * dy
-	}
+  distanceToSquared(v: Vector2) {
+    const dx = this.x - v.x,
+      dy = this.y - v.y
+    return dx * dx + dy * dy
+  }
 
-	manhattanDistanceTo(v: Vector2) {
-		return Math.abs(this.x - v.x) + Math.abs(this.y - v.y)
-	}
+  manhattanDistanceTo(v: Vector2) {
+    return Math.abs(this.x - v.x) + Math.abs(this.y - v.y)
+  }
 
-	setLength(length: number) {
-		return this.normalize().multiplyScalar(length)
-	}
+  setLength(length: number) {
+    return this.normalize().multiplyScalar(length)
+  }
 
-	lerp(v: Vector2, alpha: number) {
-		this.x += (v.x - this.x) * alpha
-		this.y += (v.y - this.y) * alpha
+  lerp(v: Vector2, alpha: number) {
+    this.x += (v.x - this.x) * alpha
+    this.y += (v.y - this.y) * alpha
 
-		return this
-	}
+    return this
+  }
 
-	lerpVectors(v1: Vector2, v2: Vector2, alpha: number) {
-		this.x = v1.x + (v2.x - v1.x) * alpha
-		this.y = v1.y + (v2.y - v1.y) * alpha
+  lerpVectors(v1: Vector2, v2: Vector2, alpha: number) {
+    this.x = v1.x + (v2.x - v1.x) * alpha
+    this.y = v1.y + (v2.y - v1.y) * alpha
 
-		return this
-	}
+    return this
+  }
 
-	equals(v: Vector2) {
-		return v.x === this.x && v.y === this.y
-	}
+  equals(v: Vector2) {
+    return v.x === this.x && v.y === this.y
+  }
 
-	fromArray(array: number[], offset = 0) {
-		this.x = array[offset]
-		this.y = array[offset + 1]
+  fromArray(array: number[], offset = 0) {
+    this.x = array[offset]
+    this.y = array[offset + 1]
 
-		return this
-	}
+    return this
+  }
 
-	toArray(array: number[], offset = 0) {
-		array[offset] = this.x
-		array[offset + 1] = this.y
+  toArray(array: number[], offset = 0) {
+    array[offset] = this.x
+    array[offset + 1] = this.y
 
-		return array
-	}
+    return array
+  }
 
-	rotate(angle: number) {
-		const { x, y } = this
-		const c = Math.cos(angle)
-		const s = Math.sin(angle)
-		this.x = x * c - y * s
-		this.y = x * s + y * c
-		return this
-	}
-
-	rotateAround(center: Vector2, angle: number) {
-		const c = Math.cos(angle),
-			s = Math.sin(angle)
-
-		const x = this.x - center.x
-		const y = this.y - center.y
-
-		this.x = x * c - y * s + center.x
-		this.y = x * s + y * c + center.y
-
-		return this
-	}
-
-	random() {
-		this.x = Math.random()
-		this.y = Math.random()
-		return this
-	}
-
-	lookAt(v: Vector2) {
-		const p = new Vector2().subVectors(v, this)
-		const ang = Math.atan2(p.y, p.x)
-		if (ang < 0) {
-			return ang + Math.PI * 2
-		} else {
-			return ang
-		}
-	}
-
-	applyMatrix3(m: Matrix3) {
-		const x = this.x,
-			y = this.y
-		const e = m.elements
-
-		this.x = e[0] * x + e[3] * y + e[6]
-		this.y = e[1] * x + e[4] * y + e[7]
-
-		return this
-	}
-
-	*[Symbol.iterator]() {
-		yield this.x
-		yield this.y
-	}
+  rotate(angle: number) {
+    const { x, y } = this
+    const c = Math.cos(angle)
+    const s = Math.sin(angle)
+    this.x = x * c - y * s
+    this.y = x * s + y * c
+    return this
+  }
+
+  rotateAround(center: Vector2, angle: number) {
+    const c = Math.cos(angle),
+      s = Math.sin(angle)
+
+    const x = this.x - center.x
+    const y = this.y - center.y
+
+    this.x = x * c - y * s + center.x
+    this.y = x * s + y * c + center.y
+
+    return this
+  }
+
+  random() {
+    this.x = Math.random()
+    this.y = Math.random()
+    return this
+  }
+
+  lookAt(v: Vector2) {
+    const p = new Vector2().subVectors(v, this)
+    const ang = Math.atan2(p.y, p.x)
+    if (ang < 0) {
+      return ang + Math.PI * 2
+    } else {
+      return ang
+    }
+  }
+
+  applyMatrix3(m: Matrix3) {
+    const x = this.x,
+      y = this.y
+    const e = m.elements
+
+    this.x = e[0] * x + e[3] * y + e[6]
+    this.y = e[1] * x + e[4] * y + e[7]
+
+    return this
+  }
+
+  *[Symbol.iterator]() {
+    yield this.x
+    yield this.y
+  }
 }
 
 export { Vector2 }

+ 123 - 123
train-web/src/pages/demo/lmm/objects/Group.ts

@@ -1,140 +1,140 @@
 import { Object2D, Object2DType } from './Object2D'
 
 class Group extends Object2D {
-	// 子集
-	children: Object2D[] = []
-	// 类型
-	readonly isGroup = true
+  // 子集
+  children: Object2D[] = []
+  // 类型
+  readonly isGroup = true
 
-	constructor(attr: Object2DType = {}) {
-		super()
-		this.setOption(attr)
-	}
+  constructor(attr: Object2DType = {}) {
+    super()
+    this.setOption(attr)
+  }
 
-	/* 设置属性 */
-	setOption(attr: Object2DType) {
-		Object.assign(this, attr)
-	}
+  /* 设置属性 */
+  setOption(attr: Object2DType) {
+    Object.assign(this, attr)
+  }
 
-	/* 添加元素 */
-	add(...objs: Object2D[]) {
-		for (let obj of objs) {
-			obj.parent && obj.remove()
-			obj.parent = this
-			this.children.push(obj)
-			this.dispatchEvent({ type: 'add', obj })
-			if (obj === this) {
-				console.error("obj can't be added as a child of itself.", obj)
-				return this
-			}
-		}
-		this.sort()
-		return this
-	}
+  /* 添加元素 */
+  add(...objs: Object2D[]) {
+    for (let obj of objs) {
+      obj.parent && obj.remove()
+      obj.parent = this
+      this.children.push(obj)
+      this.dispatchEvent({ type: 'add', obj })
+      if (obj === this) {
+        console.error("obj can't be added as a child of itself.", obj)
+        return this
+      }
+    }
+    this.sort()
+    return this
+  }
 
-	/* 删除元素 */
-	remove(...objs: Object2D[]) {
-		const { children } = this
-		for (let obj of objs) {
-			const index = children.indexOf(obj)
-			if (index !== -1) {
-				obj.parent = undefined
-				this.children.splice(index, 1)
-				this.dispatchEvent({ type: 'remove', obj })
-			} else {
-				for (let child of children) {
-					if (child instanceof Group) {
-						child.remove(obj)
-					}
-				}
-			}
-		}
-		return this
-	}
+  /* 删除元素 */
+  remove(...objs: Object2D[]) {
+    const { children } = this
+    for (let obj of objs) {
+      const index = children.indexOf(obj)
+      if (index !== -1) {
+        obj.parent = undefined
+        this.children.splice(index, 1)
+        this.dispatchEvent({ type: 'remove', obj })
+      } else {
+        for (let child of children) {
+          if (child instanceof Group) {
+            child.remove(obj)
+          }
+        }
+      }
+    }
+    return this
+  }
 
-	/* 清空children */
-	clear() {
-		for (let obj of this.children) {
-			obj.parent = undefined
-			this.dispatchEvent({ type: 'removed', obj })
-		}
-		this.children = []
-		return this
-	}
+  /* 清空children */
+  clear() {
+    for (let obj of this.children) {
+      obj.parent = undefined
+      this.dispatchEvent({ type: 'removed', obj })
+    }
+    this.children = []
+    return this
+  }
 
-	/* 排序 */
-	sort() {
-		const { children } = this
-		children.sort((a, b) => {
-			return a.index - b.index
-		})
-		for (let child of children) {
-			child instanceof Group && child.sort()
-		}
-	}
+  /* 排序 */
+  sort() {
+    const { children } = this
+    children.sort((a, b) => {
+      return a.index - b.index
+    })
+    for (let child of children) {
+      child instanceof Group && child.sort()
+    }
+  }
 
-	/* 根据名称获取元素 */
-	getObjectByName(name: string) {
-		return this.getObjectByProperty('name', name)
-	}
+  /* 根据名称获取元素 */
+  getObjectByName(name: string) {
+    return this.getObjectByProperty('name', name)
+  }
 
-	/* 根据某个属性的值获取子对象 */
-	getObjectByProperty<T>(name: string, value: T): Object2D | undefined {
-		const { children } = this
-		for (let i = 0, l = children.length; i < l; i++) {
-			const child = children[i]
-			if (child[name] === value) {
-				return child
-			} else if (child instanceof Group) {
-				const obj = child.getObjectByProperty<T>(name, value)
-				if (obj) {
-					return obj
-				}
-			}
-		}
-		return undefined
-	}
+  /* 根据某个属性的值获取子对象 */
+  getObjectByProperty<T>(name: string, value: T): Object2D | undefined {
+    const { children } = this
+    for (let i = 0, l = children.length; i < l; i++) {
+      const child = children[i]
+      if (child[name] === value) {
+        return child
+      } else if (child instanceof Group) {
+        const obj = child.getObjectByProperty<T>(name, value)
+        if (obj) {
+          return obj
+        }
+      }
+    }
+    return undefined
+  }
 
-	/* 遍历元素 */
-	traverse(callback: (obj: Object2D) => void) {
-		callback(this)
-		const { children } = this
-		for (let child of children) {
-			if (child instanceof Group) {
-				child.traverse(callback)
-			} else {
-				callback(child)
-			}
-		}
-	}
+  /* 遍历元素 */
+  traverse(callback: (obj: Object2D) => void) {
+    callback(this)
+    const { children } = this
+    for (let child of children) {
+      if (child instanceof Group) {
+        child.traverse(callback)
+      } else {
+        callback(child)
+      }
+    }
+  }
 
-	/* 遍历可见元素 */
-	traverseVisible(callback: (obj: Object2D) => void) {
-		if (!this.visible) {
-			return
-		}
-		callback(this)
-		const { children } = this
-		for (let child of children) {
-			if (!child.visible) {
-				continue
-			}
-			if (child instanceof Group) {
-				child.traverse(callback)
-			} else {
-				callback(child)
-			}
-		}
-	}
+  /* 遍历可见元素 */
+  traverseVisible(callback: (obj: Object2D) => void) {
+    if (!this.visible) {
+      return
+    }
+    callback(this)
+    const { children } = this
+    for (let child of children) {
+      if (!child.visible) {
+        continue
+      }
+      if (child instanceof Group) {
+        child.traverse(callback)
+      } else {
+        callback(child)
+      }
+    }
+  }
 
-	/* 绘图 */
-	drawShape(ctx: CanvasRenderingContext2D) {
-		const { children } = this
-		/* 绘制子对象 */
-		for (let obj of children) {
-			obj.draw(ctx)
-		}
-	}
+  /* 绘图 */
+  drawShape(ctx: CanvasRenderingContext2D) {
+    const { children } = this
+    /* 绘制子对象 */
+    for (let obj of children) {
+      obj.draw(ctx)
+    }
+  }
 }
 
 export { Group }

+ 36 - 36
train-web/src/pages/demo/lmm/objects/ObjectUtils.ts

@@ -5,61 +5,61 @@ import { Object2D } from './Object2D'
 
 /* 基于矩阵创建路径 */
 function crtPathByMatrix(
-	ctx: CanvasRenderingContext2D,
-	vertices: number[],
-	matrix: Matrix3,
-	closePath = false
+  ctx: CanvasRenderingContext2D,
+  vertices: number[],
+  matrix: Matrix3,
+  closePath = false
 ) {
-	const p0 = new Vector2(vertices[0], vertices[1]).applyMatrix3(matrix)
-	ctx.moveTo(p0.x, p0.y)
-	// console.log(p0)
+  const p0 = new Vector2(vertices[0], vertices[1]).applyMatrix3(matrix)
+  ctx.moveTo(p0.x, p0.y)
+  // console.log(p0)
 
-	for (let i = 2, len = vertices.length; i < len; i += 2) {
-		const pn = new Vector2(vertices[i], vertices[i + 1]).applyMatrix3(matrix)
-		ctx.lineTo(pn.x, pn.y)
-	}
-	closePath && ctx.closePath()
+  for (let i = 2, len = vertices.length; i < len; i += 2) {
+    const pn = new Vector2(vertices[i], vertices[i + 1]).applyMatrix3(matrix)
+    ctx.lineTo(pn.x, pn.y)
+  }
+  closePath && ctx.closePath()
 }
 
 /* 创建路径 */
 function crtPath(
-	ctx: CanvasRenderingContext2D,
-	vertices: number[],
-	closePath = false
+  ctx: CanvasRenderingContext2D,
+  vertices: number[],
+  closePath = false
 ) {
-	const p0 = new Vector2(vertices[0], vertices[1])
-	ctx.moveTo(p0.x, p0.y)
-	for (let i = 2, len = vertices.length; i < len; i += 2) {
-		const pn = new Vector2(vertices[i], vertices[i + 1])
-		ctx.lineTo(pn.x, pn.y)
-	}
-	closePath && ctx.closePath()
+  const p0 = new Vector2(vertices[0], vertices[1])
+  ctx.moveTo(p0.x, p0.y)
+  for (let i = 2, len = vertices.length; i < len; i += 2) {
+    const pn = new Vector2(vertices[i], vertices[i + 1])
+    ctx.lineTo(pn.x, pn.y)
+  }
+  closePath && ctx.closePath()
 }
 
 /* 加载图像的Promise对象 */
 function ImagePromise(image: HTMLImageElement) {
-	return new Promise<HTMLImageElement>((resolve) => {
-		image.onload = () => {
-			resolve(image)
-		}
-	})
+  return new Promise<HTMLImageElement>((resolve) => {
+    image.onload = () => {
+      resolve(image)
+    }
+  })
 }
 
 /* 图像的批量加载 */
 function ImagePromises(images: HTMLImageElement[]) {
-	return images.map((image) => ImagePromise(image))
+  return images.map((image) => ImagePromise(image))
 }
 
 /* 选择图案 */
 function SelectObj(scene: Scene) {
-	return function (objGroup: Object2D[], mp: Vector2): Object2D | null {
-		for (let obj of [...objGroup].reverse()) {
-			if (scene.isPointInObj(obj, mp, obj.pvmMatrix)) {
-				return obj
-			}
-		}
-		return null
-	}
+  return function (objGroup: Object2D[], mp: Vector2): Object2D | null {
+    for (let obj of [...objGroup].reverse()) {
+      if (scene.isPointInObj(obj, mp, obj.pvmMatrix)) {
+        return obj
+      }
+    }
+    return null
+  }
 }
 
 export { crtPath, crtPathByMatrix, ImagePromise, ImagePromises, SelectObj }

+ 1 - 1
train-web/src/router/index.ts

@@ -27,7 +27,7 @@ const routes = [
     ],
   }, {
     path: '/demo',
-    redirect: "/home/hello",
+    redirect: "/demo/hello",
     children: [
       {
         path: '/demo/clothes',

+ 15 - 6
train-web/tsconfig.json

@@ -2,14 +2,19 @@
   "compilerOptions": {
     "baseUrl": ".",
     "paths": {
-      "@/*": ["./src/*"]
+      "@/*": [
+        "./src/*"
+      ]
     },
     "target": "ES2020",
     "useDefineForClassFields": true,
     "module": "ESNext",
-    "lib": ["ES2020", "DOM", "DOM.Iterable"],
+    "lib": [
+      "ES2020",
+      "DOM",
+      "DOM.Iterable"
+    ],
     "skipLibCheck": true,
-
     /* Bundler mode */
     "moduleResolution": "bundler",
     "allowImportingTsExtensions": true,
@@ -17,7 +22,6 @@
     "isolatedModules": true,
     "noEmit": true,
     "jsx": "preserve",
-
     /* Linting */
     "strict": true,
     "noUnusedLocals": true,
@@ -29,7 +33,12 @@
     "src/**/*.d.ts",
     "src/**/*.tsx",
     "src/**/*.vue",
-    "types/**/*.d.ts"
+    "types/**/*.d.ts",
+    "types/**/*.ts"
   ],
-  "references": [{ "path": "./tsconfig.node.json" }]
+  "references": [
+    {
+      "path": "./tsconfig.node.json"
+    }
+  ]
 }

+ 7 - 0
train-web/types/Track.ts

@@ -0,0 +1,7 @@
+class track {
+
+  drawTrack(ctx: CanvasRenderingContext2D) {
+    ctx.save();
+    // TODO 绘画轨道
+  }
+}

+ 1 - 1
train-web/types/auto-imports.d.ts

@@ -297,7 +297,7 @@ declare global {
 // for type re-export
 declare global {
   // @ts-ignore
-  export type { Component, ComponentPublicInstance, ComputedRef, InjectionKey, PropType, Ref, VNode } from 'vue'
+  export type { Component, ComponentPublicInstance, ComputedRef, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, VNode, WritableComputedRef } from 'vue'
 }
 // for vue template auto import
 import { UnwrapRef } from 'vue'

+ 1 - 0
train-web/types/components.d.ts

@@ -16,6 +16,7 @@ declare module 'vue' {
     HelloWorld: typeof import('./../src/components/HelloWorld.vue')['default']
     IconEpSuccessFilled: typeof import('~icons/ep/success-filled')['default']
     IconSvg: typeof import('./../src/components/icon-svg/index.vue')['default']
+    MCanvas: typeof import('./../src/components/MCanvas.vue')['default']
     RouterLink: typeof import('vue-router')['RouterLink']
     RouterView: typeof import('vue-router')['RouterView']
   }