Skip to content
On this page

数学库

在 CAD 二次开发中, 正确的使用数学库是十分重要的, 我们并不需要很多数学, 只需要会普通的四则运算和调用 mxcad 提供的的 api 即可。

我们通过快速入门了解了如果打开图纸后,希望对图形进行各种处理,就需要各种计算, mxcad 提供了一些类参与计算或者表示一些数据结构。

几何图形信息的数学体系

在 cad 中,我们要描述图形的顶点、边、线、面、体和其他各种信息有很多不同的方法,如果我们使用不同的绘图系统,每个绘图系统又可能有独特的方式或者特定的 API,去解决某个或某类具体的问题

正因为有了太多可以选择的工具,我们也就很难找到最恰当的那一个。

且如果我们手中只有解决具体问题的工具,没有统一的方法论,那我们也无法一劳永逸地解决问题的根本。

因此在图形学中, 建立了一套描述几何图形信息与各个图形系统无关联的、简单的基于向量和矩阵运算的数学体系,以及如何用这个体系来解决我们的可视化图形呈现的问题。

坐标系与坐标映射

首先我们来看看 mxcad 可能会用到的一些坐标系:

  1. HTML 采用的是窗口坐标系 左上角为坐标原点,x 轴向右,y 轴向下,坐标值对应像素值, 在 CAD 中我们一般称为屏幕坐标。

  2. webgl 坐标系, mxcad 依赖 mxdraw, mxdraw 内部使用了特定版本修改后的 three.js 所以, 一般这种我们指的就是 Three.js 的坐标系

  3. 绘图坐标系,就是 cad 中的绘图坐标系,再 mxcad 和 mxdraw 基于它的坐标系的坐标, 一般成为文档坐标。

  4. CAD 坐标系, 是指 CAD 图纸的坐标系, 在 mxcad 中 McGePoint3d 的坐标就是 CAD 图纸坐标

mxcad 的坐标体系相互转换的方法是在mxdraw 坐标转换中全部列举出来了 我们可以直接使用 mxdraw 提供的 API 对 mxcad 相关坐标进行转换。

比如我们把屏幕坐标转成 CAD 图纸坐标:

ts
import { MxFun } from "mxdraw"
import { McGePoint3d, MxCpp } from "mxcad"
 const mxcad = MxCpp.getCurrentMxCAD()
if (!mxcad) return

const mxdraw = mxcad.mxdraw
const canvas = mxdraw.getCanvas()
canvas.addEventListener("click", (e) => {
  const pt = mxdraw.viewCoord2Cad(e.offsetX, e.offsetY, 0)
  console.log(pt)
}

尽管这四个坐标系在原点位置、坐标轴方向、坐标范围上有所区别,但都是直角坐标系,所以它们都满足直角坐标系的特性:不管原点和轴的方向怎么变,用同样的方法绘制几何图形,它们的形状和相对位置都不变。

向量 McGeVector3d

注意

引入 mxcad 后,在全局会自动在全局挂载 THREE 变量, 表示 three.js 如果发现 McGeVector3d 可以调用toVector3 方法得到 THREE.Vector3 使用 three.js 提供的 API 进行向量运算 而要将 THREE.Vector3 变成 McGeVector3d 只需要将 THREE.Vector3 当成 new McGeVector3d 的参数就可以了

那么在直角坐标系如何表示一个点 一段线呢?

前面的例子因为包含 x、y 、z 三个轴,所以它们构成了一个绘图的三维空间, 但通常我们只需要考虑 x、y。因此,我们可以用二维向量来表示这个平面上的点和线段。二维向量其实就是一个包含了两个数值的数组,一个是 x 坐标值,一个是 y 坐标值。

假设,现在这个平面直角坐标系上有一个向量 v。

向量 v 有两个含义:一是可以表示该坐标系下位于 (x, y) 处的一个点;二是可以表示从原点 (0,0) 到坐标 (x,y) 的一根线段

同样的两个向量一样可以进行数学运算:

举个例子,现在有两个向量,v1 和 v2,如果让它们相加,其结果相当于将 v1 向量的终点(x1, y1),沿着 v2 向量的方向移动一段距离,这段距离等于 v2 向量的长度。

这样,我们就可以在平面上得到一个新的点(x1 + x2, y1 + y2),一条新的线段[(0, 0), (x1 + x2, y1 + y2)],以及一段折线:[(0, 0), (x1, y1) , (x1 + x2, y1 + y2)]。

描述向量数学运算

其次,一个向量包含有长度和方向信息。它的长度可以用向量的 x、y 的平方和的平方根来表示

js
v.length = function () {
  return Math.hypot(this.x, this.y);
};

它的方向可以用与 x 轴的夹角来表示,即:

js
v.dir = function () {
  return Math.atan2(this.y, this.x);
};

在上面的代码里,Math.atan2 的取值范围是 -π 到 π,负数表示在 x 轴下方,正数表示在 x 轴上方

最后,根据长度和方向的定义,我们还能推导出一组关系式:

js
v.x = v.length * Math.cos(v.dir);
v.y = v.length * Math.sin(v.dir);

这个推论意味着一个重要的事实:我们可以很简单地构造出一个绘图向量。也就是说,如果我们希望以点 (x0, y0) 为起点,沿着某个方向画一段长度为 length 的线段,我们只需要构造出如下的一个向量就可以了。

js
// dir 是一个向量的方向(与x轴的夹角), length 是向量的长度
function createV1(dir, length) {
  return {
    x: length * Math.cos(dir),
    y: length * Math.sin(dir),
  };
}
var v0 = { x: 0, y: 0 };
var v1 = createV1(Math.PI / 5, 30);
// 然后我们把向量 (x0, y0) 与这个向量 v1相加,得到的就是这条线段的终点

而在 mxcad 中也提供了对应的方法McGeVector3d.lengthMcGeVector3d.angleTo1 来求向量长度和方向角度,

还有两个向量相加的McGeVector3d.add 你可以选择使用我们提供的这些 API 来简化上述运算。

在 three.js 中有向量THREE.Vector3 而对应的在 mxcad 也存在McGeVector3d表示 3D 空间中的矢量(向量)

在该类中存在四个轴kXAxiskYAxiskZAxiskNegateZAxis 分别都是固定的向量。

THREE.Vector3McGeVector3d 是完全等价的,只是在 mxcad 中与其他数据参与运算的是McGeVector3d

下面简单说明了一下 mxcad 提供的一些向量的运算方法:

ts
import { McGeVector3d } from "mxcad";
const vet = new McGeVector3d(1, 0, 0);
// 得到THREE.Vector3
const tVet = vet.toVector3();
const newVet = new McGeVector3d(tVet);
// 旋转
tVet.rotateBy(Math.PI.McGeVector3d.kXAxis);
// 取反
vet.negate();
// 垂直90度
vet.perpVector();
// 计算两个向量之间的角度
vet.angleTo1(newVet);
vet.angleTo1(newVet, McGeVector3d.kZAxis);
// 归一化
vet.normalize();
// 点积
vet.dotProduct(newVet);
// 交叉积
vet.crossProduct(newVet);
// 是否相等
vet.isEqualTo(newVet);
// 向量与某个值相乘
vet.mult(10);

可以参考数学库演示效果中点击获取向量长度和角度和根据方向和距离通过一个向量得到一条线,查看如果正确使用对应方法。

如上代码我们使用了很多向量的运算方法,可能你并不理解他们代表什么意思,我们大致讲解一下:

向量的加减法

他们的运算比较容易计算:

js
//  比如向量v + 向量v1:
(v.x + v1.x, v.y + v1.y)(
  // 比如向量v - 向量v1:
  v.x - v1.x,
  v.y - v1.y
);

那么他们加减得到向量,如何理解呢?

向量相加 将各个向量依次首尾顺次相接,结果为第一个向量的起点指向最后一个向量的终点向量 a 加向量 b,将 a 和 b 首尾相连后,由 a 的起点指向 b 的终点,就是 a + b

向量相减 将两个向量平移至公共起点 O,从减数向量的终点 B 指向被减向量的终点 A 为相减结果将向量 a 和向量 b 的起点移到左下角的公共起点 O,从点 B 指向点 A 的向量就是 a - b

可能理解起来比较抽象,下面我们通过查看数学库演示效果中点击向量的加减 查看具体的效果和源码可以更容易理解

向量的乘法

向量乘法有两种,一种是点乘,一种是叉乘,它们有着不同的几何和物理含义

如果你阅读后不是很理解可以在数学库演示效果中点击向量乘法 查看它的实际应用, 阅读源码更容易理解其概念。

点乘

假设,现在有两个 N 维向量 a 和 b,a = [a1, a2, ...an],b = [b1, b2, ...bn],那向量的点积代码如下:

js
a*b = a1*b1 + a2*b2 + ... + an*bn

在 N 维线性空间中,a、b 向量点积的几何含义是 a 向量乘以 b 向量在 a 向量上的投影分量。 它的物理含义相当于 a 力作用于物体,产生 b 位移所做的功。点积公式如下图所示: 向量点乘

当然点乘还有两种特殊情况:

a、b 向量平行,那么它们的夹角是 0°, 那么 a·b=|a|*|b| 用 JavaScript 代码表示就是:

js
a.x * b.x + a.y * b.y === a.length * b.length;

a、b 向量垂直,那么它们的夹角是 90°,那么 a·b=0 用 JavaScript 代码表示就是:

js
a.x * b.x + a.y * b.y === 0;

叉乘

叉乘和点乘有两点不同:

首先,向量叉乘运算的结果不是标量,而是一个向量;其次,两个向量的叉积与两个向量组成的坐标平面垂直

以二维空间为例,向量 a 和 b 的叉积,就相当于向量 a(蓝色带箭头线段)与向量 b 沿垂直方向的投影(红色带箭头线段)的乘积 那如下图所示,二维向量叉积的几何意义就是向量 a、b 组成的平行四边形的面积向量叉乘

那叉乘在数学上该怎么计算呢?假设,现在有两个三维向量 a(x1, y1, z1) 和 b(x2, y2, z2),那么,a 与 b 的叉积可以表示为一个如下图的行列式: 叉乘行列式表示

其中 i、j、k 分别是 x、y、z 轴的单位向量。我们把这个行列式展开,就能得到如下公式:

js
a * b = [y1 * z2 - y2 * z1, - (x1 * z2 - x2 * z1), x1 * y2 - x2 * y1]

二维空间中向量叉乘的物理意义就是 a 和 b 的力矩(力矩你可以理解为一个物体在力的作用下,绕着一个轴转动的趋向。

3D 点 McGePoint3d

这是最常使用的一个类 McGePoint3d 代表 3D 空间中的一个点 的构造类

xyz 三个双精度数值组成的结构, 当然 McGePoint3d 本质上也是一个向量, 上面我们讲了向量也可以表示一个点。

之所以区分,是因为它的含义就是表示坐标系上的一个点, 而不是表示一个量,

如果要改变这个点的位置,就需要仿射变换,可以通过对向量进行运算得到新的点位置。

下面是该类常用的一些方法,主要是与向量进行数学运算得到新的点的位置。

ts
import { McGePoint3d } from "mxcad"
const pt1 = new McGePoint3d(0, 0, 0)
// 或者
const pt2 new McGePoint3d({ x: 0, y: 0, z: 0})

// 提供了一些实用的方法
// 判断两个点是否相等
pt1.isEqualTo(pt2)
// 计算两点距离
pt1.distanceTo(pt2)
// 将three.js 的向量设置成点
pt1.setFromVector3(new THREE.Vector3())
// 得到点对应的three.js 向量
pt1.toVector3()

// 两点相减得到新向量
const vet = pt1.sub(pt2)
// 加上向量的新位置
pt1.addvec(vet)
// 简写
pt1.av(vet)

// 减去向量的新位置
pt1.subvec(vet)
// 简写
pt1.sv(vet)

前面我们提到了仿射变换, 那么什么是放射变换呢?

仿射变换简单来说就是“线性变换 + 平移”。

比如,对元素设置 CSS 的 transform 属性就是对元素应用仿射变换。

而几何图形的仿射变换具有以下 2 个性质: 1、仿射变换前是直线段的,仿射变换后依然是直线段 2、对两条直线段 a 和 b 应用同样的仿射变换,变换前后线段长度比例保持不变

常见的仿射变换形式包括平移、旋转、缩放以及它们的组合

最简单的就是平移,在 mxcad 中你可以直接理解为 McGePoint3d 点通过 addvec 方法加上一个向量 McGeVector3d, 就是朝该向量所代表的方向上平移向量的距离。

矩阵 McGeMatrix3d

如上我们知道如何平移一个点,我们同样可以通过线性变换对一个点进行旋转和缩放。

那么什么是线性变换呢? , 我们一样可以通过向量运算的方式, 得到如何旋转和缩放的方式。

只是旋转和缩放, 我们选择用矩阵的形式表示,通过矩阵与向量相乘形式的变换就叫做线性变换。

线性变换除了可以满足仿射变换的 2 个性质之外,还有 2 个额外的性质: 1、线性变换不改变坐标原点(因为如果 x0、y0 等于零,那么 x、y 肯定等于 0); 2、线性变换可以叠加,多个线性变换的叠加结果就是将线性变换的矩阵依次相乘,再与原始向量相乘。

那根据线性变换的第 2 条性质,我们就能总结出一个通用的线性变换公式,即一个原始向量 P0 经过 M1、M2、…Mn 次的线性变换之后得到最终的坐标 P

在 mxcad 在中 McGeMatrix3d 类表示 3D 空间的仿射变换

通常情况下我们需要将平移、旋转、缩放等组合形成的各种复杂的放射变换都通过线性变换来表示,

我们只需要将原本 n 维的坐标转换为了 n+1 维的坐标。 这种 n+1 维坐标被称为齐次坐标,对应的矩阵就被称为齐次矩阵

我们的 McGeMatrix3d 也是齐次矩阵, 所以,我们可以直接通过 McGeMatrix3d 进行各种线性变换, 最终再通过向量的transformBy方法应用这个仿射变换。

同样的矩阵也可以应用在 mxcad 中所有的几何实体McDbEntity.transformBy进行仿射变换, 因为所有的几何图形都是基于点和线构成的。

而我们可以将点或者线看作一个向量,对实体进行放射变换就相当于对组成实体的所有点进行放射变换。

矩阵的乘法

矩阵的乘法其实对应的就是上面讲述的线性变换可以叠加的这一特性,

我们希望通过一个一个的矩阵组合形成一个复杂的仿射变换, 就是通过矩阵一个一个的相乘得到的最终的矩阵,就是组合形成的复杂的仿射变换

其中两个 A、B 矩阵相乘, 以 A 为例, 这个时候 A 可以选择左乘或者右乘矩阵 B,

左乘就是B * A 右乘就是A * B

这里我们通过下图可以理解左乘和右乘的区别:

首先假设矩阵 A: 矩阵A

设列向量: 列向量

用列向量去右乘矩阵 A 列向量去右乘矩阵A

相当于对矩阵 A 中的列向量做线性组合

用列向量左乘矩阵 A 列向量左乘矩阵A

相当于对矩阵 A 中的行向量左线性组合

根据上述概念扩展到矩阵乘法中的左乘与右乘, 思路是一样的:

设置一个矩阵 B: 矩阵B

用矩阵 B 来左乘矩阵 A: 矩阵B来左乘矩阵A

因此矩阵 B 左乘矩阵 A 得到的新矩阵的每一行都是矩阵 A 的行向量的线性组合,同理,矩阵 B 右乘矩阵 A 得到的新矩阵的每一列都是矩阵 A 的列向量的线性组合

下面列举一些 McGeMatrix3d 提供的方法:

ts
import { McGeMatrix3d, McGePoint3d, McGeVector3d } from "mxcad";
// 乘法的单位矩阵
McGeMatrix3d.kIdentity;

const m = new McGeMatrix3d();

const m1 = new McGeMatrix3d();

// 设置为单位矩阵。
m.setToIdentity();

// 左乘指定的矩阵。
const m3 = m.preMultBy(m1);

// 右乘指定的矩阵。
m3.postMultBy(m1);

// 矩阵设置为两个矩阵的乘积。
new McGeMatrix3d().setToProduct(m1, m2);

// 逆矩阵。
m1.invert();

// 是否为奇异矩阵。
m1.isSingular();

// 转置
m1.transposeIt();

// 是否相等
m1.isEqualTo(m2);

// 矩阵的行列式。
m1.det();

// 将矩阵设置为指定的坐标系。参数分别是原点、xyz轴
m1.setCoordSystem(
  new McGePoint3d(),
  new McGeVector3d(),
  new McGeVector3d(),
  new McGeVector3d()
);

// 平移
m1.setToTranslation(new McGeVector3d(0, 1, 0));

// 旋转 参数:角度、轴、旋转中心点
m1.setToRotation(Math.PI, McGeVector3d.kXAxis, new McGePoint3d());

// 缩放 参数: 缩放因子、缩放中心点
m1.setToScaling(0.5, new McGePoint3d());

// 设置为镜向矩阵
m1.setMirror(new McGePoint3d(), new McGePoint3d());

// 获取缩放因子
m1.scale();

// 获取矩阵中指定位置的元素值 参数 行索引、列索引
m1.getData(0, 0);

如何在 mxcad 中使用矩阵,可以查看数学库演示效果中对点的旋转平移和缩放|对实体进行仿射变换|查看具体的效果和源码可以更容易理解使用。

MxCADResbuf

MxCADResbuf是 CAD 二次开发中resbuf(即 "result buffer") 用来传递数据的一种数据结构。

一般用于对象属性查询、自定义对象的定义和存储、XDATA(扩展数据)处理、绘图实体的编辑 等

例如在MxCADSelectionSet选择集中表示过滤对象:

ts
import { MxCADSelectionSet, MxCADResbuf } from "mxcad";
let ss = new MxCADSelectionSet();
let filter = new MxCADResbuf();
// 这里添加查询字符"0" 第二个参数是数据类型8 在CAD二次开发中 表示这是一个空指针(RTNUL)即该 resbuf 结构不包含任何有效数据,通常在链表的末尾作为终止符使用
filter.AddString("0", 8);
// 选择图层0的所有实体
ss.allSelect(filter);
ss.forEach((objId) => console.log(objId));

calcBulge 计算圆弧凸度

MxCADUtility 是MxCADUtilityClass 提供的实例, 提供了很多实用的方式。

MxCADUtility.calcBulge 计算圆弧凸度

在多段线实体中添加点有一个参数是凸度值, mxcad 提供了 calcBulge 方法来计算得到凸度

需要三个参数依次为: 圆弧的开始点(起点)、圆弧的中点、圆弧的结束点(端点)

ts
import { MxCADUtility, McGePoint3d, McDbPolyline } from "mxcad";
// 圆弧开始点
const startPoint = new McGePoint3d(0, 0, 0);
// 圆弧中点
const midPoint = new McGePoint3d(0, 0, 0);
// 圆弧结束点
const endPoint = new McGePoint3d(0, 0, 0);
const bulge = MxCADUtility.calcBulge(startPoint, midPoint, endPoint).val;
const pl = new McDbPolyline();
pl.addVertexAt(startPoint, bulge);
pl.addVertexAt(endPoint);

如果你并不知道以上三个参数中的任意一个,参考下图自行计算凸度:

圆弧凸度计算公式

凸度值就是圆弧的开始点与结束点距离的一半去除以 圆弧的开始点与结束点相连这条线段的中点到圆弧圆心的距离

数学库演示效果

该 demo 基于 vue3, 采用 tsx 的方式, 请自行参考vue 官网 Tsx 说明编写代码(展开代码可进行编辑)