Lesson06:Core 核心层:数学、几何、对象与实体
源码 https://github.com/CHMOSE023/MiniCAD
系列:MiniCAD第 8 / 17 篇
- MiniCAD 初级教程课程规划
- C++ 基础课:读懂 MiniCAD 所需的语言知识
- Lesson01:项目能跑起来
- Lesson02:整体架构地图
- Lesson03:程序入口与主循环
- Lesson04:输入系统与消息映射
- Lesson05:绘图工具如何工作
- Lesson06:Core 核心层:数学、几何、对象与实体
- Lesson07:命令模式与撤销重做
- Lesson08:选择、拾取与夹点编辑
- Lesson09:视口、相机与坐标转换
- Lesson10:渲染层入门
- Lesson11:文字与字体系统
- Lesson12:吸附与约束系统
- Lesson13:命令行系统
- Lesson14:多文档管理
- Lesson15:综合实战:扩展一个 MiniCAD 功能
目录
学习目标
读懂 Core 层的四个子模块(Math / GeomKernel / Object / Entity)的设计意图和关键接口,理解 Object 根类的 ID 体系和手写 RTTI,以及 Entity 的三个纯虚函数如何驱动整个系统的拾取、渲染和复制。
相关源码
src/Core/Object/Object.hpp/RuntimeType.hppsrc/Core/Entity/Entity.hpp/EntityAttr.hpp/LineEntity.hppsrc/Core/GeomKernel/Line.hpp(代表 GeomKernel 全家)src/Core/Draw/IDrawSink.hppsrc/Core/Math/Point3.hpp/Vec3.hpp/Mat4.hpp(见 Lesson00_Cpp.md 第 14 节)
为什么用 .hpp 扩展名
Core 层所有文件用 .hpp,而非 .h/.cpp。约定含义:这些文件是模板或内联实现,整个定义在一个文件里,不需要单独的翻译单元。上层模块包含 .hpp 即可,不需要在 CMakeLists.txt 里列举 .cpp。
子模块一览
Core/
├── Math/ 纯数值类型:Point2/3, Vec2/3, Mat4, Color4(见 Lesson00_Cpp.md)
├── GeomKernel/ 几何算法:Line, Circle, Arc, AABB, Polyline, Spline …
├── Object/ 根对象:ObjectID 体系 + 手写 RTTI
├── Entity/ CAD 实体:Entity 基类 + 11 个具体实体
└── Draw/ 绘制抽象:IDrawSink 接口
Object:根对象与 ID 体系
class Object {
public:
using ObjectID = uint64_t;
static constexpr ObjectID InvalidID = 0; // 0 保留为无效
ObjectID GetID() const { return m_id; }
virtual const RuntimeTypeInfo* GetTypeInfo() const = 0;
template<typename T>
bool IsKindOf() const noexcept {
return GetTypeInfo()->IsKindOf(&T::TypeInfo);
}
inline static const RuntimeTypeInfo TypeInfo{ "Object", nullptr };
protected:
explicit Object(ObjectID id) : m_id(id) {}
private:
ObjectID m_id;
};
ObjectID 分配:在 Scene 中用 std::atomic<ObjectID> 维护,Scene::NextObjectID() 每次 fetch_add(1) 无锁递增。ID 从 1 开始,0 永远是 InvalidID。
为什么 Object 不暴露 SetID(给外部调用):ID 一旦进入 Scene 就不应再变。Clone() 创建新对象时需要传入新 ID,由调用者负责分配。
手写 RTTI:RuntimeTypeInfo + DECLARE_RUNTIME_TYPE
C++ 标准 dynamic_cast 和 typeid 依赖 RTTI 编译选项,在某些嵌入式或 WASM 环境下可能被关闭。MiniCAD 用宏实现了等价功能:
struct RuntimeTypeInfo {
const char* Name;
const RuntimeTypeInfo* Parent;
bool IsKindOf(const RuntimeTypeInfo* other) const {
const RuntimeTypeInfo* cur = this;
while (cur) {
if (cur == other) return true;
cur = cur->Parent; // 沿继承链向上走
}
return false;
}
};
#define DECLARE_RUNTIME_TYPE(ThisClass, ParentClass) \
public: \
using Super = ParentClass; \
inline static const RuntimeTypeInfo TypeInfo { #ThisClass, \
&ParentClass::TypeInfo };\
virtual const RuntimeTypeInfo* GetTypeInfo() const override { \
return &TypeInfo; \
}
宏展开后在类中生成两样东西:
static const RuntimeTypeInfo TypeInfo:类型信息静态对象(含类名字符串和父类型指针)- 虚函数
GetTypeInfo():运行时返回本类的类型信息
使用示例:
Object* obj = scene.GetEntity(id);
// 判断类型
if (obj->IsKindOf<LineEntity>()) // 走 TypeInfo 链,相当于 dynamic_cast 检查
auto* line = static_cast<LineEntity*>(obj); // 已确认类型,可直接 static_cast
// GripEditor 按类型查找 Handler
m_handlers[&LineEntity::TypeInfo]; // RuntimeTypeInfo* 作为 map 键
继承链举例:LineEntity::TypeInfo.Parent → Entity::TypeInfo → Object::TypeInfo → nullptr。
EntityAttr:CAD 属性
struct EntityAttr {
Math::Color4 Color = Color4::White();
LayerID LayerId = 0;
LineType LineType = LineType::SOLID;
double LineWidth = 1.0;
bool Visible = true;
};
Entity 基类持有一个 EntityAttr m_attr,任何具体实体都继承了这 5 个 CAD 属性。Draw() 实现里用 GetAttr().Color 取颜色,但选中/悬停时覆盖为特殊颜色:
// LineEntity::Draw 中
const Color4& color = isSelected ? IDrawSink::kSelectionColor
: isHovered ? IDrawSink::kHoverColor
: attr.Color;
Entity:三个纯虚函数
class Entity : public Object {
virtual AABB GetBoundingBox() const = 0;
virtual void Draw(IDrawSink&, bool isSelected, bool isHovered) const = 0;
virtual unique_ptr<Entity> Clone(ObjectID newId) const = 0;
DECLARE_RUNTIME_TYPE(Entity, Object)
protected:
explicit Entity(ObjectID id) : Object(id) {}
private:
EntityAttr m_attr;
};
| 方法 | 服务的系统 | 说明 |
|---|---|---|
GetBoundingBox() | Picking 的 HitTest / BoxSelect | 返回轴对齐包围盒(AABB),用于快速相交测试 |
Draw(IDrawSink&, ...) | Editor::UpdateSceneVertices() | 向 sink 发射几何(线段、纹理四边形),不接触 GPU |
Clone(newId) | CopyCommand、MirrorCopyCommand 等 | 深拷贝实体,赋予新 ID,保留属性 |
LineEntity 全解
class LineEntity : public Entity {
LineEntity(ObjectID id, const Point3& start, const Point3& end)
: Entity(id), m_line(start, end) {}
AABB GetBoundingBox() const override { return m_line.GetBounds(); }
void Draw(IDrawSink& sink, bool isSelected, bool isHovered) const override {
const Color4& color = isSelected ? kSelectionColor
: isHovered ? kHoverColor
: GetAttr().Color;
sink.DrawLine(m_line.Start, m_line.End, color, false);
// false = 不是 overlay(放入正式场景顶点缓冲)
}
unique_ptr<Entity> Clone(ObjectID newId) const override {
auto e = make_unique<LineEntity>(newId, m_line.Start, m_line.End);
e->SetAttr(GetAttr()); // 复制属性(颜色、图层等)
return e;
}
DECLARE_RUNTIME_TYPE(LineEntity, Entity)
private:
Line m_line;
};
LineEntity 只持有一个 Line m_line,几何计算完全委托给 GeomKernel::Line。
GeomKernel:几何算法集
以 Line 为例,GeomKernel 的职责是提供算法,不带任何 CAD 属性:
struct Line {
Point3 Start, End;
Vec3 Vector() const; // End - Start
Vec3 Direction() const; // 单位方向向量
double Length() const;
Point3 Midpoint() const;
Point3 PointAt(double t) const; // P(t) = Start + t*(End-Start)
double ProjectParam(const Point3& p); // p 在直线上的参数 t
Point3 ClosestPoint(const Point3& p); // p 到直线的最近点
double DistanceToPoint(const Point3& p);
AABB GetBounds() const;
bool IsValid() const; // 长度 > LengthEPS
};
DistanceToPoint 是拾取的核心:Picking::HitTest 把鼠标世界坐标传给 line.DistanceToPoint(),如果距离小于阈值(换算到屏幕空间)则命中。
ProjectParam 的实际用途:
double t = line.ProjectParam(p);
// t < 0:p 在 Start 延长线上
// 0 ≤ t ≤ 1:p 在线段上
// t > 1:p 在 End 延长线上
吸附"最近点"时用此判断是否在线段范围内。
IDrawSink:绘制接口
class IDrawSink {
static const Color4 kSelectionColor; // 蓝色
static const Color4 kHoverColor; // 青色
virtual void DrawLine(const Point3& a, const Point3& b,
const Color4& color, bool isOverlay) = 0;
virtual void EmitText(const Point3& pos, const string& utf8, float height,
float rotation, const Color4& color) = 0;
virtual void EmitMText(/* 多行文字参数 */) = 0;
};
DrawContext 是它的唯一实现(见 Lesson02)。实体通过 IDrawSink 发射几何,与底层渲染 API 完全解耦。
继承层次总结
Object ← ObjectID + RTTI 根
└── Entity ← EntityAttr + 三纯虚函数
├── LineEntity ← Line
├── CircleEntity ← Circle
├── ArcEntity ← Arc
├── EllipseEntity ← Ellipse
├── RectangleEntity ← Rectangle
├── PolylineEntity ← Polyline
├── SplineEntity ← Spline
├── PointEntity ← Point3
├── TextEntity ← 单行文字
└── MTextEntity ← 多行文字(带 StyleId)
拓展练习
DECLARE_RUNTIME_TYPE宏展开后TypeInfo的Parent指针指向哪里?如果父类也有TypeInfo,两个静态对象各自存在哪里?Object的构造函数是protected,意味着什么?能直接new Object()吗?- 找到
CopyCommand.h,确认它在哪里调用了Clone(),新 ID 如何获取。 - 为一个新实体"十字标记(CrossEntity)"设计
GetBoundingBox()、Draw()和Clone():几何是两条垂直线段,中心点由构造函数给定。