0%

源代码及环境

url:https://github.com/qcad/qcad branch:master tag:v3.27.8.4 sha:13c1383e83828fc7120f1acc697e76bd201bdeba

Win10 MSVC15 Qt5.15.2 Qt6.4.1

配置编译

文档几乎为零,网络上讨论的内容也很少。当然,商业化的项目多少会在开源代码上些障碍以保证商业利益,开源代码开源部分基本处于放羊状态。Qt 从源代码编译也是十分困难的事情,需要对 Qt 有长时间的研究才能做到。

Qt5

Qt5 的配置只有 pro 文件,没有编写对应的 CMake 文件

QCAD 官网给的是用 qt5.13.2, 然而由于 qt 强推新版本,没有办法直接获取到旧版本,而手动编译旧版本不太可能,qt 的编译相当困难。使用目前还能获取到的 5.15.2 版本,官网记录的使用 qmake 的方法无法正常输出。qtcreator 可以正常配置,但是只能构建 src 下的目录,无法在 qtcreator 中运行或调试,会陷入无限循环的 pre-build 事件。

目测该问题的原因在于 pro 文件没有使用比较标准的,qt 官方的环境配置方式,而是简单的设置了手动拷贝文件的命令。而且各个项目之间没有正确的依赖。

构建成功后,手动配置环境后,可以成功的运行初始化。但是没有后续,由于无法调试,无法分析具体问题

Qt6

Qt6 提供了 CMake 文件,但是有很多问题,项目依赖,代码,环境设置,宏定义均有问题。这个CMake 看起来像是使用某种工具将 pro 文件自动转化得来的。

代码制定的是 C++17,然而使用的第三方库里却有 binary_function 这种早就被弃用的内容。

耗了不少功夫才成功的将代码编译运行调试起来。然而存在一个致命的问题——QCAD 使用了 qt 旧版的 jsEngine,这部分的内容还没在 Qt6 上解决,导致 QCAD 核心的脚本引擎没有办法运行起来,程序无法初始化该部分内容。

代码分析

公共静态成员变量

在 RVector 类中发现了个没见过的写法

1
2
3
4
5
6
// in class
public:
static const RVector invalid;
static const RVector nullVector;
static const RVector nanVector;
// in class

代码的作用基本就是提供了一些用于判断的变量。好处是省去了一次构造函数。

但是个人不推荐这种写法,同样的作用不如

1
2
3
4
5
6
// in class
public:
static const RVector& get_invalid();
static const RVector& get_nullVector();
static const RVector& get_nanVector();
// in class

同样的作用,而且可以将 invalid 等变量内置,避免在输出成动态链接库时一些链接的问题。实际上由于原来的 CMake 写得有些问题,导致这部分的代码在动态库链接时就产生了问题。

js脚本系统

QCAD 搭载了一个 js 脚本系统,可以方便二次开发。然而目前看来有代码中有很多功能都使用了这个脚本系统来运行特定的脚本进行替代,例如 gui 的初始化等等。

虽然一个脚本系统确实是有助于二次开发,但是个人不是很推荐脚本系统深入到各个核心系统的运行中。效率是一方面,而开发效率又是另一方面。可以理解这样的初衷是想通过修改脚本来达到快速修改程序运行逻辑。

但是笔者实际的工作经验表明,这种做法,随着开发时间的不断推移,最后就会变成既需要维护脚本代码的烂摊子,又要维护C++代码的烂摊子,一项任务,双倍工作,四倍难度。

当然不是说脚本构思这种设计不好。只是从实际的角度来看,只有超大型的公司开发的超大型软件,例如 AutoDesk CAD,3ds Max 等软件才能稍微的把这些脚本系统利用起来,用户自行开发真正需求的自定义插件。小公司、小软件,不如完善开发文档开发流程,优化基础功能来满足大多数用户的需求来得实际。

而且说实话,没有类型提示的编程语言真的是让人讨厌。到处都是 any,到处找各种变量的实际类型真的很浪费时间。

绘制系统

代码基本集中在 RGraphicsViewImage.cpp

看看有没有比较出彩的地方。

总体来说渲染的流程和渲染对象系统都是比较经典的设计,各种东西比较完整。使用 Qt 的渲染系统,没看到有什么特殊的优化的地方。

代码中看样子是有做多线程渲染,但是并没有真正的实现,从残余的代码来看是通过排序后,按序划分到各个 QPainter

能够满足各种线型、不同缩放下的细分。但是感觉效率不会太高,内容少时还凑合,东西多点就会很卡。

绘制流程

  • paintEntitiesMulti -> 绘制制定区域内的实体
    • 获取一些颜色有关的配置
    • 查询区域内的实体,并且按 Z 轴排序
    • 遍历排序后的实体,获取所有 drawable 且遍历
      • 检查是否需要重新生成 drawable 绘制的内容,判定的属性有 AlwaysRegen、AutoRegen 和 PixelSizeHint 和当前不符。包括一些射线,细分过大过小的曲线等
      • 只要有一个 drawable 需要重新生成绘制内容则该 entity 重新生成全部内容
        • 经过一系列的判断
        • sceneQt->exportEntity(entityId:id, allBlocks:bool)
        • 这里 sceneQt 算是一个 RExporter,可以看出无论是显示,还是打印或者导出文件,都算 RExporter 子类的动作
          • entity->exportEntity()
          • RExporter 基类定义了各种设置数据的接口,以 RArcEntity::exportEntity 为例就可以看到使用了 RExporter::setBrush 和 RExporter::exportArc, 将圆弧的数据写入到了 sceneQt 中
          • 注意这里只是设置了数据,并没有真正的绘制
    • 然后开始通过多线程绘制 entity list, 但是这里把多线程的代码注释了,实际还是单线程
    • paintEntitiesThread(threadId, list, start, end)
    • 这里值得注意的是,drawable 是和 entity 对象分开的。也就是说 entity 只拥有抽象的数据,drawable 由 RGraphicsScene 管理,感觉没什么必要,基本每个 entity 都有自己的 drawable,当 entity 的数量多起来时,通过 id 在 drawable map 中查找还是挺耗时间和内存的。
    • 然后就针对各种不同 RGraphicsSceneDrawable::Type 进行绘制,粗略过了一下各种类型绘制的代码,都是使用 Qt 的绘制系统。感觉性能可能不会很高。感觉有点不一样的地方是,绘制变换内容需要使用 Transform 和 EndTransform 的 drawable 向 transform stack 中压入和弹出变换矩阵。

RGraphicsSceneDrawable 划分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// in class
enum Type {
Invalid,
PainterPath,
PainterPathRay,
PainterPathXLine,
Image,
Text,
Transform,
EndTransform
};

enum Mode {
NoMode = 0x0000,
NoPlot = 0x0001, //!< not plotted (from not plottable layers)
PixelUnit = 0x0002, //!< unit interpreted as pixels
WorkingSet = 0x0004 //!< in current working set
};
// in class

Type 是用于表示绘制的内容——路径、图像、文字之类的。Mode 用于表示属于范围,比如说是辅助对象还是绘制对象等。感觉划分得比较粗,有挺多内容其实不太好实现——例如填充的内容该属于哪类,归属于 PainterPath ?

RPainterPath

继承自 QPainterPath,添加了一些 CAD 系统有关的内容。一部分设置直接使用了 QPen、QBrush,这部分内容确实是挺繁琐的,东西又多又杂,反正最后也是直接使用 QPainterEngine,直接用 Qt 的内容无缝对接也挺省事的。

数据位于一个 originalShapes:QList<QSharedPointer<RShape>> 关注下 RShape 就好

RShape 拥有的类型如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// in class
public:
enum Type {
Unknown = -1,
Point = 0,
Line = 1,
Arc = 2,
Circle = 3,
Ellipse = 4,
Polyline = 5,
Spline = 6,
Triangle = 7,
XLine = 8,
Ray = 9
};
// in class

比较常见的设计,只是类型上多了一些 XLine、Ray 等一般 CAD 会有,而平面绘制引擎没有的东西。

对象系统

基类是 RObject,使用 Id:int objectId作为索引。这里的 id 并不是全局唯一的。两个 entity 可以拥有同一个 id,具体会访问到哪个对象,要看 RDocument 中记录的是哪个。

属性储存使用的是一个 customProperties:QMap<QString, QVariantMap> 低属性时有一定的好出,占用空间小。极限情况可能会比较吃力。另外就是使用 QString 作为索引,可能会产生冲突。

另外对象系统管理得比较散乱,虽然有指定 document 的函数,但是没有和 RDocument 中联系起来,两边数据能否对应起来全看编码有没有出 bug。

RTTI 使用 enum 作为识别和区分。简单的设置。

GUI 交互

交互系统,当然这里指的是底层的框架,而不是产品经理设计操作逻辑。这个系统其实挺难设计的,特别是要能支撑复杂的操作逻辑的时候。

RAction

定义了一个 RAction 的基类,用于表示所有的指令。其中有一些鼠标事件,键盘事件的纯虚函数,事件的类型有不少是在 Qt 事件的基础上改造的。例如

class RMouseEvent: public QMouseEvent, public RInputEvent

但是这个基本只是个架子,实际没多少代码写在 cpp 里面,只有 RNavigationAction,是关于用鼠标移动页面的操作的。

大部分操作写在了 js,通过写一个转发事件到 js 的 REcmaAction 子类(REcmaAction 有关的代码是自动生成的)。例如一个三点画圆的功能,写在了 scripts\Draw\Arc\Arc3P.js 里,具体的逻辑看Arc3P.prototype.setStateArc3P.prototype.pickCoordinate就知道了。挺简单的一个switch分支。

简单交互功能的代码没什么好看的。本来想看点复杂的,但是好像没有太过复杂的逻辑。倒是有一个 RAction 的栈,一层套一层的 js Action 组成了挺复杂的操作逻辑。这个分层实现倒是有点意思。而且对应的 gui 状态全由 js 脚本里定义

但是其实灵活度不高。有一些操作,例如编辑对象,通过的是 RRefPoint 这个类去实现的。而相关的管理是写在 RStorage 子类中

默认的 RAction 是 DefaultAction.js,从中可以分析出许多功能具体的实现方式。

ROperation

ROperation 则是定义了操作对象动作的基类,例如添加或移除一个 entity。但是这部分的代码比较少,都是些常见的操作,没看到有操作具体对象的代码。可能全放在了它的 ECMA 脚本系统上了。

事件系统

代码从 RMainWindowQt::event() 开始入手

使用了 Qt 作为 GUI 框架的程序,基本在代码里找下 QMainWindow 相关的 event 函数就好。可以看到事件运作流程是是

  • 注册监听器
  • 触发某一类型的事件
  • event 中识别,再 notifyXXXXListeners)()
  • 监听器运行绑定的函数

说实话,这用那么多的 dynamic_cast 感觉有点粗糙,还是在主event循环里

接着看主要的显示窗口类 RGraphicsViewQt 里的事件处理函数,传递的过程大致如下

RMainWindowQt -> RGraphicsViewQt -> RGriphicsScene -> RDocumentInterface -> CurrentAction

不过这里还不是太关键的内容,没显露出是怎么和 js 交互的。这部分绑定是由 qt-labs-qtscriptgenerator 生成的,比较复杂。

杂项

  • 使用 id 查询返回到具体的对象是智能指针类型
  • 同样使用了空间索引进行包围框查询
  • entity 属性的设置直接使用了 RPropertyTypeId 和 QVariant。感觉编码上挺省事的,但是使用起来不太方便,没有类型提示的接口用起来很麻烦
  • 注释里写了一些和没写没什么区别的 doxygen 格式的文档

2022年9月24号星期六 10时至12时 14时至15时30分

与会:全体软件平台组10人

培训内容

C/C++代码培训:实战演练——使用性能剖析器优化效率

这一次花费了不少时间来做准备,准备了两个实际生产环境中的问题。通过现场现场演练,使用VS自带的新能分析器来一步一步展示如何配置环境,通过CPU剖析找到热点代码,分析问题优化改进,并验证结果。最后剩下一些时间,额外的展示了内存使用的分析。

课件的是在提前预演问题时准备的,将过程和记录下来。

效果和心得

这一次与会人员的反馈比较好,清楚地展示教导了如何使用剖析器优化代码的过程和思路。

经过上一次的教训,这次使用了自己的高性能笔记本(插电),大大减少的编译和剖析器的耗时。

改进

附文最后带有,当场进行的培训质量打分,获得了比较高的评价。

有反馈到,讲解的例子有点多,耗时过长。在准备时,有点担心没能达到很好的效果,又刚好遇上的不同的案例,就想着多一点内容,哪怕没能很好的掌握这次培训的技能,也能够很好的加深映像。这个反馈给我的实际映像是,有很多同事实际的水平可能比工作中的水平要高不少,往往因为各种因素消极怠工。

也有反馈到,希望拓展的内容更多。在现场的讲解中也确实遇到,许多拓展的东西更吸引注意力。不过这一块需要讲解的需要相当深厚的水平,这块我准备的很多拓展内容都是事先准备的,想要信手沾来还有点难度,需要更加努力。

还有一块是将一些专业的词汇写下来,可供有兴趣的同学自己去额外查找学习

附文

附当场的主讲记录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
代码培训20220924 10:00-12:000
主题:实战演练——通过性能分析器优化程序效率
参加人员:软件平台组全体10人

程序优化
基数方法:手动log时间

性能分析器,Profile,
VS自带的剖析器

实际案例
CMakeLists.txt -> sln
cmake -Bvs_out -G"Visual Studio 16"

窗口-》重置窗口布局
诊断工具窗口
勾选《记录CPU配置文件》选项
两个断点之间生成具体的分析-》打开详细信息

带调试信息的release模式,保证相似的速度和可以使用断点
问题:
读取文件 《打标测试3(1).dxf》 debug下全部显示很卡顿, release 速度尚可
环境:
主工程库版本 "77e2203 - 更新子目录至最新"
Win10 64bit i5-10400 2.90GHz RAM:16G GPU: UHD 630
编译类型 Debug RelWithDebug
分析
View::Render()中断点显示一帧需要237/162ms
CPU Profile 记录出一帧中最耗时的函数是QueryTransMatrix(), 占用每帧的72.88/68.52%消耗

在Release下速度尚可
使用CMake输出程 sln 工程,以RelWithDebug 模式编译
同样的情况View::Render()中断点显示一帧需要22/21ms左右
CPU Profile 记录 QueryTransMatrix() 中消耗是 31.82/4.55%
解决
编写了QueryTransMatrix()新算法
主工程库版本 "79da267 - 更新新的QueryTransMatrix()算法"
cadc 版本 "06016e4 - 优化QueryTransMatrix()的效率"
同样条件在 Debug 下一帧需要 79/11ms左右消耗为 32.91/18.3%
QueryTransMatrix()优化了85/54.95%,整体优化了67/62%
同样条件在 RelWithDebug 下一帧需要18/17ms左右消耗为 11.12/19.07%
QueryTransMatrix()优化了70/86%,整体优化了20/19%
思考
20%的性能优化是长期可靠的吗,在某些条件下会不会劣化


主题
分析文件 《打标测试3(1).dxf》 的打开性能
环境
主工程库版本 "f6c154a - 修正非win下条件编译的错误"
FileParser版本 "04e83b9 - Merge branch 'master' into 'master'"
Win10 64bit i5-10400 2.90GHz RAM:16G
编译类型 RelWithDebug
分析
总耗时 1010/723 ms
主要性能开销
1. 47.03/42.6% FileParser\FileParser\src\dxf\dxf_reader.cpp ln 142 dxfReader::readDouble()
8.91/7.19% ln 146 " if (readString(&text))"
std::getline 就使用了 8.81/7.19%
23.96/22.82% ln 149 "sd >> doubleData;//将字符串转为double"
这里的开销主要是将字符串转为double值, 为了完成字符串转double
结合上下文看, 实际的开销应该是 37.82/34.58%, 要把对应变量的构造和析构也算进去
这里可以尝试优一下

解决方案
FileParser 版本 "5e124b1 - 降低字符串转double的消耗"
总耗时 663/510ms 降低 34.4/29.5%
最终字符串转为double值的开销为总 7.54/6.08%, 性能优化 87/87.6%

这里面引申出一个问题, 对于文件解析来说, std 的字符串库的性能是否满足告诉解析

2. src\Parser\DxfParser.cpp
12.87/12.72% ln 689 "it->Transform(ViewOffset);"
6.24/6.84% ln 692 "it->Transform(USCmove);"

这里就是个很简单的问题了, 这里进行了2次变换, 总消耗 19.11/19.56%
但其实稍微观察下这个函数就知道这里的变换可以合并成一次变换

解决方案
主工程版本 "d3a7ebc - 合并DxfParser中的变换"
总耗时 936/676ms 降低 6.4/6.67%
Transform 总开销为 14.69/14.64%, 性能优化 28.8/31%

合并所有优化
主工程库版本 "2bf8975 - 更新FileParser版本"
总开销 620/461ms, 优化 38.7/38.3%

思考
这个文件有什么特点, 这些优化在什么情况下会发挥到比较好的作用
主要是读double数据,切分换行
在占总消耗 19.11% 的 Transform() 中有, 其中的 ReDraw() 函数占总消耗的 12.67%
可以进行怎样的优化, 又会带来什么样的后果

往往内存操作的耗时是最高的

总体需要注意的事项
+ 二八原则, 80%的消耗在20%的代码上. 在代码编写之初不要瞎优化, 写一些难以阅读的代码
+ 一切优化建立在剖析数据的上, 要根据实际的情况下的剖析结果去改进
+ 当运行消耗都比较分散时, 往往难以再做性能优化. 因为这时候往往是设计结构不足以应对实际需求
这时候就需要根据经验, 实际情况, 算法改进等重新进行设计
+ 做好性能分析的记录, 保存环境和关键数据, 空口无凭, 有数据才能有对比

容易理解的程度:5, 4.5,4,4.5,4,5,5,5,5
讲解的形式:5,5,5,5,5,4(内容冗余),4.5,4.5,4.5
授课风格:5,5,5,4.5(再扩散一点),4.5(增加比喻),4(扩展内容),4.5(专业的词语),5,4.5

记录一下解一元三次方程的详细推导。一元三次方程有通解,网络上有许多相关的内容,这里不再展开历史科普。

有三次方程

a3x3+a2x2+a1x1+a0=0a_3x^3 + a_2x^2 + a_1x^1 + a_0 = 0

需要转成 x3x^3 的系数为 11 的形式

a=a2/a3b=a1/a3c=a0/a3\begin{aligned} a &= {a_2} / {a_3} \\ b &= {a_1} / {a_3} \\ c &= {a_0} / {a_3} \end{aligned}

x3+ax2+bx1+c=0(1) x^3 + ax^2 + bx^1 + c = 0 \tag{1}

x=ya3(1.1)x = y - \frac{a}{3} \tag{1.1}

带入 (1)(1)

(ya3)3+a(ya3)2+b(ya3)+c=0(1.2)(y - \frac{a}{3})^3 + a·(y - \frac{a}{3})^2 + b·(y - \frac{a}{3}) + c = 0 \tag{1.2}

进一步化简 (1.2)(1.2)

y3+(ba23)y+(2a327ab3+c)=0(1.3)y^3 + (b - \frac{a^2}{3})y + (\frac{2a^3}{27} - \frac{ab}{3} +c) = 0 \tag{1.3}

p=ba23q=2a327ab3+c\begin{aligned} p &= b - \frac{a^2}{3}\\ q &= \frac{2a^3}{27} - \frac{ab}{3} + c \end{aligned}

p,qp,q 带入 (1.3)(1.3)

y3+py+q=0(1.4)y^3 + py + q = 0 \tag{1.4}

y=v+wy = v + w

带入 (1.4)(1.4)

(v+w)3+p(v+w)+q=0(1.5)(v + w)^3 + p(v + w) + q = 0 \tag{1.5}

化简 (1.5)(1.5)

(3vw+p)(v+w)+(v3+w3+q)=0(1.6)(3vw + p)(v + w) + (v^3 + w^3 + q) = 0 \tag{1.6}

选择 (1.6)(1.6) 中的一个解

3vw+p=0(1.7)3vw + p = 0 \tag{1.7}

作为附加条件, 带入 (1.6)(1.6), 得方程组

v3+w3=qvw=p3(1.8)\begin{aligned} v^3 + w^3 &= -q\\ vw &= -\frac{p}{3} \tag{1.8} \end{aligned}

方程组 (1.8)(1.8) 可以进一步写成

v3+w3=qv3w3=p333(1.9)\begin{aligned} v^3 + w^3 &= -q\\ v^3w^3 &= -\frac{p^3}{3^3} \tag{1.9} \end{aligned}

方程组 (1.9)(1.9) 的解是方程组 (1.10)(1.10) 的解,但是方程组 (1.10)(1.10) 的解不一定是方程组 (1.9)(1.9) 的解,因此选择满足方程组的解:

vw=p3vw = -\frac{p}{3}

根据韦达定理, 方程组(1.9)的解式下述二次方程的根
韦达定理中, 一般二次方程 ax2+bx+c=0ax^2 + bx + c = 0 中有

x1+x2=ba,x1x2=cax_1+x_2 = -\frac{b}{a},\quad x_1x_2 = \frac{c}{a}

v3,w3v^3,w^3 分别为韦达定理中的 x1,x2x_1,x_2, 且令 a=1a = 1, 带入(1.9)(1.9)可以得以下方程

ba=qca=p333\begin{aligned} -\frac{b}{a} &= -q \\ \frac{c}{a} &= -\frac{p^3}{3^3} \end{aligned}

此时令 a=1a = 1, 则可以得一个新的关于 v3,w3v^3,w^3 的二次方程

m2+qm(p3)3=0(2)m^2 + qm - (\frac{p}{3})^3 = 0 \tag{2}

方程 (2)(2) 的解为

m1,2=v3,w3=q±q2+4(p3)32=q2±(q2)2+(p3)3(3)\begin{aligned} m_{1,2} &= v^3,w^3 \\ &= \frac{-q \pm \sqrt{q^2 + 4(\frac{p}{3})^3}}{2} \\ &= -\frac{q}{2} \pm \sqrt{(\frac{q}{2})^2 + (\frac{p}{3})^3} \tag{3} \end{aligned}

因此有

v=q2+(q2)2+(p3)33w=q2(q2)2+(p3)33(4)\begin{aligned} v &= \sqrt[3]{ -\frac{q}{2} + \sqrt{(\frac{q}{2})^2 + (\frac{p}{3})^3}} \\ w &= \sqrt[3]{ -\frac{q}{2} - \sqrt{(\frac{q}{2})^2 + (\frac{p}{3})^3}} \tag{4} \end{aligned}

所以对于三次方程 y3+py+q=0y^3 + py + q = 0 的解可以写成如下形式

y=v+w=q2+(q2)2+(p3)33+q2(q2)2+(p3)33\begin{aligned} y &= v + w \\ &= \sqrt[3]{ -\frac{q}{2} + \sqrt{(\frac{q}{2})^2 + (\frac{p}{3})^3}} + \sqrt[3]{ -\frac{q}{2} - \sqrt{(\frac{q}{2})^2 + (\frac{p}{3})^3}} \end{aligned}

对于三次方程 y3+py+q=0y^3 + py + q = 0,记判别式

k=(q2)2+(p3)3k = (\frac{q}{2})^2 + (\frac{p}{3})^3

有三种情况

  1. 当 k > 0 时,有一个实数解和两个虚数解
  2. 当 k = 0 时,有三个实数解,但至少有两个解是相同的
  3. 当 k < 0 时,有三个不同的实数解

在虚数范围内,已知有 x3=1x^3 = 1 的三个根

ϵ1=1232iϵ2=12+32iϵ3=1(5)\begin{aligned} \epsilon_1 &= -\frac{1}{2} - \frac{\sqrt{3}}{2}i \\ \epsilon_2 &= -\frac{1}{2} + \frac{\sqrt{3}}{2}i \\ \epsilon_3 &= 1 \tag{5} \end{aligned}

ϵ1=ϵ2ϵ2=ϵ1ϵ3=ϵ0(5.1)\begin{aligned} \epsilon_1 &= \epsilon^2 \\ \epsilon_2 &= \epsilon^1 \\ \epsilon_3 &= \epsilon^0 \tag{5.1} \end{aligned}

(5)(5)(5.1)(5.1) 可得,在三次方程 x3=rx^3 = r 中,设 r1r_1 为其中一个解,则所有解可以统一表示为 r1ϵ0,r1ϵ1,r1ϵ2r_1\epsilon^0,r_1\epsilon^1,r_1\epsilon^2

v1=q2+(q2)2+(p3)33v_1 = \sqrt[3]{ -\frac{q}{2} + \sqrt{(\frac{q}{2})^2 + (\frac{p}{3})^3}}

作为其中一个解,和 (1.8)(1.8)vw=p/3vw = -p/3

w1=p3v1w_1 = -\frac{p}{3v_1}

由于pp为实数,则有结论 v1,w1v_1,w_1 同时具有虚部或同时不具有虚部
因此三次方程 y3+py+q=0y^3 + py + q = 0 的九个解只有以下三个解满足条件

y1=v1+w1y2=v1ϵ+w1ϵ2y3=v1ϵ2+w1ϵ\begin{aligned} y_1 &= v_1 + w_1 \\ y_2 &= v_1\epsilon + w_1\epsilon^2 \\ y_3 &= v_1\epsilon^2 + w_1\epsilon \end{aligned}

k>0k > 0

此时 v1,w1v_1,w_1 均为实数,只有一个实数解且只可能是

y=v1+w1x=ya3\begin{aligned} y &= v_1 + w_1 \\ x &= y - \frac{a}{3} \end{aligned}

k=0k = 0

此时 v1,w1v_1,w_1 均为实数,ϵ,ϵ2\epsilon, \epsilon^2 为共轭复数,有两个实数解,已有一个实数解为

y1=v1+w1=q2+(q2)2+(p3)33+p3v1x1=y1a3\begin{aligned} y_1 &= v_1 + w_1 \\ &= \sqrt[3]{ -\frac{q}{2} + \sqrt{(\frac{q}{2})^2 + (\frac{p}{3})^3}} + -\frac{p}{3v_1} \\ x_1 &= y_1 - \frac{a}{3} \end{aligned}

剩下的解为

y2=v1ϵ+w1ϵ2y3=v1ϵ2+w1ϵ\begin{aligned} y_2 &= v_1\epsilon + w_1\epsilon^2 \\ y_3 &= v_1\epsilon^2 + w_1\epsilon \end{aligned}

在虚数部分定有

0=v1w1v1=w1\begin{aligned} 0 &= v_1 - w_1 \\ v_1 &= w_1 \end{aligned}

则解为

x1=y1=v1+w1a3x2=y2=12(v1+w1)a3=v1a3\begin{aligned} x_1 = y_1 &= v_1 + w_1 - \frac{a}{3} \\ x_2 = y_2 &= -\frac{1}{2}(v_1 + w_1) - \frac{a}{3} = -v_1 - \frac{a}{3} \end{aligned}

k<0k < 0

有三个实数解,有虚数的开方公式可得

x1=y1a3=z1+w3a3=2r3cos(ϕ3)a3x2=y2a3=z2+w2a3=2r3cos(ϕ+2π3)a3x3=y3a3=z3+w1a3=2r3cos(ϕ+4π3)a3\begin{aligned} x_1 = y_1 - \frac{a}{3}&= z_1 + w_3 - \frac{a}{3} = 2 \sqrt[3]{r}·cos(\frac{\phi}{3}) - \frac{a}{3} \\ x_2 = y_2 - \frac{a}{3} &= z_2 + w_2 - \frac{a}{3} = 2 \sqrt[3]{r}·cos(\frac{\phi + 2\pi}{3}) - \frac{a}{3} \\ x_3 = y_3 - \frac{a}{3} &= z_3 + w_1 - \frac{a}{3} = 2 \sqrt[3]{r}·cos(\frac{\phi + 4\pi}{3}) - \frac{a}{3} \end{aligned}

其中

z=q2+Δiϕ=arctan[tan(ϕ)]tan(ϕ)=2kqr=(q22)k=(q22)(q22)(p33)=(p33)\begin{aligned} z&=-\frac{q}{2}+\sqrt{\Delta}i \\ \phi &= arctan[tan(\phi)] \\ tan(\phi) &= -2\frac{\sqrt{-k}}{q} \\ r &= \sqrt{(\frac{q}{2}^2) - k} = \sqrt{(\frac{q}{2}^2) - (\frac{q}{2}^2) - (\frac{p}{3}^3)} = \sqrt{- (\frac{p}{3}^3)} \end{aligned}

其中如果 tan(ϕ)<0tan(\phi) < 0 ,最终计算出来的 y1,y2,y3y_1,y_2,y_3 要改变正负号

线段(有限长)和圆弧(非整圆)的求交点可以分为以下3部分进行:

设线段 ll 两端分别为 AABB,设圆 OO 的圆心为 CC,半径为 rr

一、求直线是否和正圆相交

这一部方法有两种,一种是通过三角形面积来计算,有:

AB×AC=nAB|AB \times AC| = n|AB|

nn 为直线 ABAB 到圆心的最短距离(亦是垂直距离)。

另一种是求出圆心在直线 ABAB 上的投影点,再算出距离。设点 OO 为圆心在直线 ABAB 上的投影点,有:

ABACABAB=AOAB\frac{AB \cdot AC}{AB \cdot AB} = \frac{|AO|}{|AB|}

简化一下,有:

f=ABACABABf = \frac{AB \cdot AC}{AB \cdot AB}

O=A+fABO = A + fAB

求出点 OO 后,可以直接通过距离的平方进行比较,无需开方。

这里通过第二种方法进行求解,因为接下来比较方便

二、 求直线与正圆的交点

承接上面第二种方法,设 PPQQ 分别为直线 ll 与圆 OO 的交点(仅有有一个交点时计算方法相同,不做特别讨论)。则有:

OP2=OQ2=r2OC2|OP|^2 = |OQ|^2 = r^2 - |OC|^2

k=OPABk = \frac{|OP|}{|AB|}

Q,P=A+(f±k)ABQ,P = A + (f \pm k)AB

可以通过 0(f±k)10 \leq (f \pm k) \leq 1 来快速判断点 PPQQ 是否位于线段 ABAB 上。

三、 求位于线段的交点是否在圆弧的方位内

设圆弧 arcarc 以点 CC 为圆心,半径为 rr,起始角为 startstart,转动角度为 sweepsweep。则求出交点 PPQQ 是否在圆弧的范围内。这一部分更多的是程序上的判断。

起始角为 startstart,转动角度为 sweepsweep 可以分为四种情况

最终圆弧 arcarc02π0-2\pi 内可以用一部分或两部分来表示,例如当 start=60°,sweep=120°start = 60°, \quad sweep = -120° 时,圆弧 arcarc 所在的范围为 [0°,60°],[300°,360°][0°,60°],[300°,360°]

通过 arctan(PC),arctan(QC)arctan(PC),arctan(QC)就能判断出交点是否在圆弧 arcarc

2022年8月12号星期五15点—18点

与会:全体软件平台组10人

培训内容

最近都比较忙,来不及准备培训内容,沿着上次讲的内容,展示了下这些内容最终是如何设计和编写的.然后就上次提出的拓展问题,现场让一个与会人员进行尝试解决.主要是上次内容的进一步实战演练

效果和心得

要提前做好充分的准备,现场随便找点东西讲的效果感觉很差

近期同事问到了这个问题,顺便记录一下。首先代码文件的编码和两个地方有关,一是文件本身的编码,二是编译器设置接受的编码。这里两个地方只要对应起来就可以了。

文件本身编码好处理,在不同的 IDE 中新建的文件默认的编码格式可能各有不同,只要记得检查并改成需要的编码就可以了。另一是处理已有的代码,可以使用脚本自动处理成想要的编码。

TODO: 补上对应的 python 代码

另外一个是编译器的编码设置。不同编译器有不同设置,Linux 和 UNIX 基本都是使用 utf-8 作为系统默认编码,相应的 gcc, clang 等也是使用 utf-8 作为默认编码的。在不跨平台的情况下,这两个系统一般是不会遇到文件编码问题的。

需要注意的是 Windows 下。MSVC 使用的编码设置是系统设定的本地语言和 utf-8 bom。虽然Windows的机内码是 utf-16 le。顾及是为了兼容早期程序和全球化吧,表层的编码使用的是本地编码。例如中国地区用的是 gbk。这时候如果代码文件本身是 utf-8编码的,就有可能出现错误,因为 utf-8 的范围比 gbk 大,有识别不了的字符。而代码文件是 utf-8 bom 开头的就没问题,因为 utf-8 bom 编码的文件,文件开头有固定的标示符,可以直接识别出来,gbk 和 utf-8 就不能直接区分开来。

在跨平台的情况下,应该采取代码统一用 utf-8 编码,然后工程设置里指定编译器使用 utf-8 编码。例如 CMakeLists.txt 中输出目标为 MSVC 时应该增加 add_compile_options(/utf-8)

vscode配置clangd注意事项

  • 需要clang++和安装clangd,可以让vscode 拓展自动下载
  • 无法找到 qt 目录
    搭配 CMake 使用会省去很多麻烦,在 clangd.arguments 里面增加 "--compile-commands-dir=${workspaceFolder}/build"
    具体的目录是配置的compile_commands.json所在的目录,这样就可以自动的配置好qt目录
    CMake 有些输出目标不会生成 compile_commands.json,可以手动制定
  • 无法找到stl目录
    clangd.arguments 增加 "--query-driver=/usr/bin/clang++" 手动置顶编译器目录来解决
    官网相关链接
    有些bug,好像需要编译一次才能找到标准库目录

在项目中总会遇到使用很多第三方库库的时候,在CMake中,链接静态库会将依赖添加到使用的新项目中,结果导致输出的静态库需要附上依赖一起使用。

摘抄了一些搜索得到的记录,还没进行具体实验

Because a static lib is not much more than an archive of object files, you may be able to extract the object code from each lib and recombine them with ar

Combining static libraries
https://stackoverflow.com/questions/665752/combining-static-libraries

Combining several static libraries into one using CMake
https://stackoverflow.com/questions/37924383/combining-several-static-libraries-into-one-using-cmake

近一两年来在公司主讲代码培训差不多进行了二三十次,但是感觉效果都不是很好。很难调动起气氛,事后也看不到同事的代码水平有什么提升。深刻的体会到了大学时老师在台上声嘶力竭得讲,看着台下一片混混欲睡的学生是什么感觉

方法改进

为了提升培训的效果,本次培训采用了新的方式,以工作中的一个实际问题为例,逐步的讲解如何进行处理。本次的培训分为两部分,一是解决问题的流程以及实战演练,二是进一步深化,讲解设计代码的思路

培训内容

解决问题的流程

  • 首先是找到问题所在
  • 如何找到对应功能的代码
  • 如何分析运作的机理
  • 修改时应当注意的问题/影响范围
  • 从表面的原因分析出深层的原因

并从这些实际的例子中,抛砖引玉插入代码设计、编写中的思路技巧经验和注意事项

然后是深化培训的内容,进一步讲解如何设计优化一项功能

  • 例举所有的需求
  • 提出基本的实现流程
  • 从多个角度去分析、归类需求
  • 按照分析和归类去整理出总的要求
  • 设计数据结构
  • 根据具体的实现去优化迭代设计

效果和心得

虽然培训会议持续了足足三个小时,但是总体的效果看起来比以前要好很多。通过现场演练,和加强提问的互动环节,听起来总算没那么干巴巴了的。而且根据现场反馈,这样一点点的讲,能够更好的掌握项目原有代码的思路,之前只是对着设计说明书一顿输出,听着云里雾里的,效果不好。

可能一两次培训难以立刻提升与会人员的代码设计能力和分析水平,毕竟演练中的实际问题作为主讲的我轻描淡写地就解决了,其实背后需要大量的积累。但是至少这个功能,都大概掌握了。

而且在过程中,像附文那样现场创建一个文档将内容边讲解边记录下来,起到了一个像板书一样的效果,也可以帮助现场理解和集中注意力。同时有一个现场的记录方便回忆和巩固。

总结上,这次的方式可以延续下来。

改进

  • 要准备一个更好的电脑。现场演练需要实时地编译代码,这次使用的 mbp 的低压 i5 实在是慢,过慢的编译速度浪费了不少时间和频繁打断了讲解。

  • 换上实际的开发环境。这次使用的 vs code,无法满足大型工程的语法智能分析,没有很好跳转代码,浪费了不少时间。

附文

附当场的主讲记录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
/*******************************************************************************
2022年8月12号星期五15点—18点
与会:全体软件平台组10人
主题:从节点捕捉功能为实例讲述问题解决和代码设计
*******************************************************************************/
节点捕捉到自身
节点捕捉的功能单一,比较简单

+ 找到代码在哪里
+ 直接问设计的人,远古代码,领导很忙
+ 设计文档,文档不全,很难看懂
+ 自己找
+ 分析功能的入口,mouseMove
+ 断点:循环事件每次都触发,无法筛选

+ 分析运作的机理
+ 合理的运用断点
+ 相关的文档
+ 代码有足够注释
+ 代码合理的名字和结构是更好的注释

qt mouseMoveEvent-》setMouseAutoCapture
-〉View::AutoCapture->View::CaptureQuery
-> 1.Scene::CaptureQuery 2.Mode::CaptureQuery
在空间索引里面查找,筛选的条件不够
-> Item::CaptureQuery 线索断了

根据 Item::CaptureQuery 的结果来绘制和设置数据
设置到 VMouseEvent

+ 开始修改
+ Scene::CaptureQuery简单的修改了(增加 IsSelected()的判断)
带来影响几乎是全局的

+ 有别的地方和这里有同样的问题(面向对象,基类子类继承)
+ 改动会不会影响到别的不需要的地方,带来新的问题

+ 应该在 ViewMode 里面改

Mode,Scene,管辖的范围重合,

需求:
+ 针对不同的情况,可以进行特化,且互不影响
问题
+ 处理范围重合(返回的情况过于单一,只用一个bool变量进行标示,只能标记有没有查询到
+ 已经处理,但是没找到需要捕捉到点


CaptureData 有效的利用起来-》修正流程兼容更多的情况。captureItem也要根据对应的属性重新绘制

+ 节点、圆心、中点的捕捉
+ 栅格的捕捉
+ 交点、垂点、切点、最近点的捕捉

+ 程序:
-》 输入:鼠标位置,捕捉范围

(最简单)—》捕捉的范围做空间查询-》对应图形-》(如果是路径对象-》对应线段

《- 输出:捕捉的点,捕捉的类型(扩展图像输出)

分类需求(可以从很多角度去区分):
输入需求的角度:
只需要鼠标点和捕捉范围的(隐藏的意义:剩下所需的属性只需要有对象本身提供)
节点、圆心、中点、栅格、已有交点的捕捉,只需要 Item::CaptureQuery 自己实现
需要只是额外的交互对象的:
交点、垂点、切点、最近点的捕捉(可以指示/储存额外对象)(any,type enum)

交互的需求:
需要提前指示的:圆心(不会一直显示的属性点)-》需要修改流程(多步合作)

实现难度:
交点(同一个图形之间的交点,不同图形之间的交点(不同的子类的))(只做单一对象的交点
运算速度,设计结构

数据结构
CaptureData

算法,需求,功能
——》分析好需求-》设计数据结构-》实现调整
分类。

课后思考:
1. 怎么插入栅格的捕捉
2. 找出这里栅格的实现过程,插入自动的捕捉

普通函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <utility>
#include <iostream>

template<typename Fn, Fn fn, typename... Args>
void func_wraper(Args... args)
{
/*do some thing*/
std::cout << "hello, ";

fn(std::forward<Args>(args)...);

/*do some thing*/
std::cout << " bye.";
}

#define WRAPER(FUNC) func_wraper<decltype(&FUNC), &FUNC>

void func_a(int i){
/*do some thing*/ std::cout<<"func_a("<<i<<")";
}
int main(){
WRAPER(func_a)(int(0));
return 0;
}

输出

1
hello, func_a(0) bye.
阅读全文 »