← 返回

DeepDraw引擎开发 快速入门

更新于 2026-10-20#DeepDraw

目录

DeepDraw引擎开发 快速入门(一):SketchUp 替代
DeepDraw引擎开发 快速入门(二):下载DeepDraw引擎
DeepDraw引擎开发 快速入门(三):用代码绘制线段
DeepDraw引擎开发 快速入门(四):用代码绘制面
DeepDraw引擎开发 快速入门(五):创建选择工具
DeepDraw引擎开发 快速入门(六):读取模型文件
DeepDraw引擎开发 快速入门(七):InputPoint输入捕获器
DeepDraw引擎开发 快速入门(八):画线工具
DeepDraw引擎开发 快速入门(九):矩形工具
DeepDraw引擎开发 快速入门(十):推挤工具
DeepDraw引擎开发 快速入门(十一):偏移工具
DeepDraw引擎开发 快速入门(十二):参考线工具
DeepDraw引擎开发 快速入门(十三):删除命令

(一)SketchUp替代

SketchUp的优势

建筑方案往往从简单的问题开始:场地上能放下什么,体量如何组合,空间关系是否合理?这个阶段需要快速画线、生成面、推拉体块,再根据讨论结果反复修改。SketchUp 凭借直接的建模方式,成为许多建筑师、室内设计师和学生表达早期想法的工具。它的价值不只在于生成三维模型,更在于让设计者把注意力放在方案上。

为什么关注 SketchUp 替代

中国有大量建筑设计、城市更新、室内设计和工程项目,方案推敲与三维表达的需求贯穿日常工作。国家统计局的第五次全国经济普查公报记录了建筑业的庞大体量

住房和城乡建设部等部门《关于推动智能建造与建筑工业化协同发展的指导意见》提出,要突破专用软件等关键技术,积极应用自主可控的 BIM 技术,并建设数字设计基础平台和集成系统。

本系列文章,希望以最简单的方式入手,一步一步的讲解如何动手开发一款建筑设计工具。

目前有哪些路径

目前已知的替代产品有2个:

  • 1、DeepDraw几何引擎,深度智绘,中国
  • 2、ingetrazo,github,秘鲁

DeepDraw

DeepDraw引擎是 深度智绘科技(上海)有限公司 开发的3D建模引擎,完美替代SketchUp,官网https://www.archieast.com

DeepDraw专业版 本已经非常完善,面向企业开发者,对于个人开发者,有专门的基础版引擎可以使用,开放社区支持二次开发,建模功能覆盖 SketchUp 标准版,开发者可以把重点放在应用层的组织、插件、文件格式、UI交互等。
引擎的数据存储 和Sketchup保持一致,不改变用户操作习惯,历史文件无损导入
甚至可以通过在Sketchup中Ctrl-C,DeepDraw中Ctrl-V粘贴模型,反向粘贴回Sketchup也同样适用
可以导入非常大的场景,例如香港九龙场景,300M+

IngeTrazo

IngeTrazo 是一个来自秘鲁、受 SketchUp 启发的开源三维建模项目,主要面向建筑与土木工程场景。项目目前使用 Python 和 PySide6,优先支持 Linux,也提供 Windows 安装包。它的仓库展示了画线、推拉、编辑以及 .skp 文件读写等能力,同时明确说明项目仍处于 0.x 阶段,文件格式和插件接口可能变化。它为我们提供了一个观察对象:从基础几何操作,到真正可用的设计工具,中间需要补上哪些细节。以后有机会,我会单独介绍这个项目。

这个系列准备做什么

本系列采用 DeepDraw 几何引擎作为底层,逐步搭建一款建筑设计 App。引擎已经具备Sketchup核心功能,文章则会从 App 框架、代码绘图和交互工具入手,让读者看清这些能力怎样接入应用、怎样形成熟悉的建模体验。

(二)下载DeepDraw引擎

DeepDraw引擎基础版 windows 64位 (184M)
https://static.archieast.com/DeepDrawBase_v1.zip
这个是完整版本,包含预编译程序,下载下来解压就可以使用

DeepDraw引擎Mini版 windows 64位 (59M)
https://static.archieast.com/DeepDrawHMI_win64.zip
这是给开发者用的入门版本,包含源代码和预编译程序,启动程序需要的最少功能

解压缩后的文件列表

在这里插入图片描述

引擎的核心代码,可以在python中直接调用 editor2.cp38-win_amd64.pyd render_pipeline.cp38-win_amd64.pyd core.py

python代码,app_demo包含了最少的代码,尽可能的降低上手难度 main.py - app主窗口 view3d.py - OpenGL显示窗口 tool_camera.py - 相机控制

引擎对二次开发非常友好,可以任意修改除了OpenGL窗口外的所有UI界面。这意味着你可以完全按照自己的产品需求,重新设计工具栏、菜单栏、属性面板等各个界面元素,打造出具有自己品牌风格的建筑设计软件。引擎内部将三维渲染窗口与UI层做了彻底解耦,你只需要专注于业务逻辑和界面布局,无需关心底层OpenGL渲染细节,大幅降低了二次开发的难度和门槛。

运行环境

python3.8 64位 (安装对应版本python)

安装依赖

pip install pyside2
pip install pyopengl
pip install numpy
python main.py

运行成功

操作方式和Sketchup相同:
旋转视角:鼠标中键
平移视角:Shift+鼠标中键

在这里插入图片描述

(三)用代码绘制线段

在上一节,已经下载并成功运行了DeepDraw引擎的demo框架,启动后显示一个带坐标系的空场景,这节主要演示,如何用api调用,向场景中添加连段,以及线段如何合并成面

创建线段的方法

第一步:获取当前模型,活动组件

        model = core.active_model()
        comp = model.GetActiveComponent()

第二步:创建2个顶点对象,新创建的顶点,会自动合并距离非常近的顶点(距离小于0.001)的顶点

        point1 = core.CVector3D(10, 10, 0)
        point2 = core.CVector3D(200, 10, 0)
        
        v1 = comp.PopulateVertex(point1)
        v2 = comp.PopulateVertex(point2)

第三步:创建线段对象,并且加入到当前组件中,正常情况线段也要自动合并,这里先演示简单的方法,下一节再介绍线段合并

        new_edge = core.CEdge(v1, v2, True).AddToUndoManager()
        comp.AddEntity(new_edge)

工具栏按钮

在工具栏上有一个默认的图标,actiontest,这个按钮用来做测试代码非常方便

    @Slot()
    def on_actiontest_triggered(self):
        print("test1")

运行后,点工具条上的唯一按钮,就会在场景里创建出一条线段

完整源代码

main.py

import sys
from PySide2 import QtWidgets
from PySide2.QtCore import Slot
from Ui_main_window import Ui_MainWindow

import core


class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
    def __init__(self):
        super().__init__()
        self.setupUi(self)
    
    def init_system(self):
        model = core.CModel()
        model.Activate()
        core.set_active_model(model)
        
        # for view
        self.view.BindModel(model)
    
    @Slot()
    def on_actiontest_triggered(self):
        print("test1")
        
        model = core.active_model()
        comp = model.GetActiveComponent()
        
        point1 = core.CVector3D(10, 10, 0)
        point2 = core.CVector3D(200, 10, 0)
        
        v1 = comp.PopulateVertex(point1)
        v2 = comp.PopulateVertex(point2)
        
        new_edge = core.CEdge(v1, v2, True).AddToUndoManager()
        comp.AddEntity(new_edge)
        
        self.view.MarkDirty()
        self.view.repaint()


if __name__ == "__main__":
    app = QtWidgets.QApplication(sys.argv)
    
    MainWindow = MainWindow()
    MainWindow.show()
    MainWindow.init_system()
    sys.exit(app.exec_())

(四)用代码绘制面

在上一节,介绍了如何绘制单独的线,这次介绍绘制面

在Sketchup的建模方式中,一般的理解,绘制矩形是线绘制4个线段,然后再把面创建出来,并且建立面和线的拓扑关系

实际上,Sketchup可以自己判断,哪些线段已经围成了在同一个平面上的多边形面

也就是说,用代码创建面,只要创建在同一个面上的4个首位连接的线,面会自动生成

DeepDraw也用了相同的逻辑来绘制面

创建面的方法

        model = core.active_model()
        comp = model.GetActiveComponent()
        
        point1 = core.CVector3D(10, 10, 0)
        point2 = core.CVector3D(200, 10, 0)
        point3 = core.CVector3D(200, 200, 0)
        point4 = core.CVector3D(10, 200, 0)
        
        v1 = comp.PopulateVertex(point1)
        v2 = comp.PopulateVertex(point2)
        v3 = comp.PopulateVertex(point3)
        v4 = comp.PopulateVertex(point4)
        
        edge1 = core.CEdge(v1, v2, True).AddToUndoManager()
        edge2 = core.CEdge(v2, v3, True).AddToUndoManager()
        edge3 = core.CEdge(v3, v4, True).AddToUndoManager()
        edge4 = core.CEdge(v4, v1, True).AddToUndoManager()
        
        comp.AddEntity(edge1)
        comp.AddEntity(edge2)
        comp.AddEntity(edge3)
        comp.AddEntity(edge4)
        
        self.view.MarkDirty()
        self.view.repaint()

这段代码放到按钮事件里,运行后可以看到创建了4条线段,形成一个矩形,但是只有线没有面

在这里插入图片描述

自动生成面

这时候在代码最后再加入下面2行:

        edge_ls = [edge1, edge2, edge3, edge4]
        core.IBaseTool.check_model(comp, edge_ls, False)

再运行测试,这次就自动生成了面,跟前面说到的一样,魔法主要在check_model中完成,包括自动创建面,交叉点检测,切割线段,切割面,自动开孔,都是在这个函数调用里完成

也就是说,不管是用画线工具,还是矩形,圆形,弧形,每次创建了线段后,都要放到这个函数里计算对场景的修改 在这里插入图片描述

完整源代码

main.py

import sys
from PySide2 import QtWidgets
from PySide2.QtCore import Slot
from Ui_main_window import Ui_MainWindow
import core

class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
    def __init__(self):
        super().__init__()
        self.setupUi(self)
    
    def init_system(self):
        model = core.CModel()
        model.Activate()
        core.set_active_model(model)
        
        # for view
        self.view.BindModel(model)

    @Slot()
    def on_actiontest_triggered(self):
        model = core.active_model()
        comp = model.GetActiveComponent()
        
        point1 = core.CVector3D(10, 10, 0)
        point2 = core.CVector3D(200, 10, 0)
        point3 = core.CVector3D(200, 200, 0)
        point4 = core.CVector3D(10, 200, 0)
        
        v1 = comp.PopulateVertex(point1)
        v2 = comp.PopulateVertex(point2)
        v3 = comp.PopulateVertex(point3)
        v4 = comp.PopulateVertex(point4)
        
        edge1 = core.CEdge(v1, v2, True).AddToUndoManager()
        edge2 = core.CEdge(v2, v3, True).AddToUndoManager()
        edge3 = core.CEdge(v3, v4, True).AddToUndoManager()
        edge4 = core.CEdge(v4, v1, True).AddToUndoManager()
        
        comp.AddEntity(edge1)
        comp.AddEntity(edge2)
        comp.AddEntity(edge3)
        comp.AddEntity(edge4)
        
        edge_ls = [edge1, edge2, edge3, edge4]
        core.IBaseTool.check_model(comp, edge_ls, False)
        
        self.view.MarkDirty()
        self.view.repaint()


if __name__ == "__main__":
    app = QtWidgets.QApplication(sys.argv)
    
    MainWindow = MainWindow()
    MainWindow.show()
    MainWindow.init_system()
    sys.exit(app.exec_())

(五):创建选择工具

这一节开始介绍选择工具,引擎里提供了拾取对象的工具类CPickHelper,通过鼠标点击屏幕上的坐标,这个类遍历场景中的全部对象,包括组件中,返回一个列表,包含了所有鼠标点周围(9x9)像素内所有碰到的物体,并且通过面遮挡,排序等方式,得到最佳的拾取对象(BestPick)

创建SelectTool

直接上源代码,这是最简单的一个工具类,目的是演示,如何使用CPickHelper

import core

class SelectTool(core.IBaseTool):
    def __init__(self, view):
        super().__init__(view.view_proxy)
        self.view = view
        self.activate()
    
    def activate(self):
        self.pick_helper = core.CPickHelper(self.view.view_proxy)
    
    def draw(self, gl):
        pass
        
    def draw_2d(self, painter):
        pass

    def on_left_button_down(self, pos, mod):
        self.pos_x = pos.x()
        self.pos_y = pos.y()
        
        picker = self.pick_helper
        picker.Reset()

        picker.SinglePick(self.pos_x, self.pos_y, 9, None)
        
        best = picker.BestPick()
        
        print("select:")
        if best:
            print(best.e)

    
    def on_left_button_up(self, pos):
        pass

    def on_mouse_move(self, pos, modifiers):
        pass

运行结果

运行后,创建一个矩形,这相当于给场景添加了5个对象(4个线段和一个面),然后用鼠标选择面,可以看到,点选面,终端显示的select是CFace,点选线,显示CEdge,当同时选中时,CEdge的优先级要高于CFace

[python3.8]
>>> select:
<editor2.CEdge object at 0x0000028B018491F0>
select:
<editor2.CFace object at 0x0000028B01892130>
select:
<editor2.CEdge object at 0x0000028B0181C030>

高亮选中效果

现在已经可以知道CPickHelper返回的是什么了,那么把选中的高亮显示,基本的选择工具就有,其实只要使用SetSelection和ClearSelection,引擎的渲染模块,就会自动显示高亮效果

    def on_left_button_down(self, pos, mod):
        self.pos_x = pos.x()
        self.pos_y = pos.y()
        
        model = core.active_model()
        
        picker = self.pick_helper
        picker.Reset()

        picker.SinglePick(self.pos_x, self.pos_y, 9, None)
        
        best = picker.BestPick()
        
        print("select:")
        if best:
            print(best.e)
            model.SetSelection([best.e])
        else:
            model.ClearSelection()

运行起来,交互效果非常好
在这里插入图片描述

再测试细节更丰富的模型

这里导入个复杂点的模型,要用到下一节讲到的读取dim文件,其实只要在程序初始化的时候加入一小段:

        model = core.CModel()
        binfile.load_sketchup_model(model, "test3_triangle.dim")
        model.Activate()
        core.set_active_model(model)

在这里插入图片描述

(六):读取模型文件

从文件中读取模型,需要用到binary_file_v2.mpy,需要单独下载并安装,类似于插件模块

导入模块

import binary_file_v2 as binfile
DEFAULT_MODEL = "ren.dim"

这个 ren.dim 是系统默认的纸片人模型,与 SketchUp 新建场景时自带的参考人角色类似,官网原版中也内置了该模型。此前为了简化代码、方便读者快速上手,我们刻意省略了这部分内容。

    def load_default_model(self):
        model = core.CModel()
        binfile.load_sketchup_model(model, DEFAULT_MODEL)
        model.Activate()
        core.set_active_model(model)
        # for view
        self.view.BindModel(model)

在这里插入图片描述

(七):InputPoint输入捕获器

接下来可以开始最重要的工具-画线工具

在做画线工具前,还有个很重的功能,InputPointFinder,这个类负责获取用户输入,主要是捕获鼠标操作可能吸附的顶点,也包括捕获线,中点,交叉点,地平面

测试InputPointFinder

直接上源代码,这个例子非常简单,在OnMouseMove时间里,运行Inference并获取吸附的Point,然后在draw里显示出来

import core

class DrawLineTool(core.IBaseTool):
    def __init__(self, view):
        super().__init__(view.view_proxy)
        self.view = view
        self.activate()
    
    def activate(self):
        self.input = core.CInputPointFinder(self.view.view_proxy)
        #启动鼠标追踪
        self.view.setMouseTracking(True)
    
    def deactivate(self):
        #关闭鼠标追踪
        self.view.setMouseTracking(False)
    
    def draw(self, gl):
        self.input.Draw()
   
    def on_left_button_down(self, pos, mod):
        pass
    
    def on_left_button_up(self, pos):
        pass

    def on_mouse_move(self, pos, modifiers):
        px = pos.x()
        py = pos.y()
        
        self.input.Reset()
        
        ok = self.input.Inference(px, py, None, None )
        assert ok
        
        self.cur_point = self.input.Position()
        

普通Vertex,线段,显示黑色 线段中点青色 面蓝色 交叉点橙色

具体的绘制代码在引擎里

	glPointSize(5);
	switch (this->snap_type) {
	case SnapType_vertex:
		glColor3f(0, 0, 0);
		break;
	case SnapType_intersection:
		glColor3f(1.0, 0.5, 0.0);
		break;
	case SnapType_edge_center:
		glColor3f(0.03f, 0.73f, 0.81f);
		break;
	case SnapType_edge:
		glColor3f(0, 0, 0);
		break;
	case SnapType_guild_line:
		glColor3f(0, 0, 0);
		break;
	case SnapType_axis:
		glColor3f(0, 1.0, 0);
		break;
	case SnapType_other_point:
		glColor3f(0, 1.0, 0);
		break;
	case SnapType_ref_point_intersect:
		glColor3f(1.0, 0, 1.0);
		break;
	case SnapType_face:
		glColor3f(0, 0.0, 1.0);
		break;
	default:
		return;
	}

(八):画线工具

画线工具,是Sketchup的核心功能,作为DeepDraw引擎研发团队的一员,基本上所有的工作,都是围绕这个功能展开的。画线的功能看起来简单,其实背后有非常复杂的工作,比如求交,切分线段,分割面,闭合检测,自动开孔,错误检查等等。

在工具侧也是一样,最常用的矩形工具,本质上就是画线工具的延生,画了4条线段,创建面的功能交给引擎处理,偏移工具,路劲跟随,本质上也是计算点和线段,所以只要把画线的工具研究明白了,整个软件的功能,都是顺理成章了。画线工具属于2点工具,有很多工具都是2点类工具(矩形工具,移动工具…),本质上用户在屏幕点选确定第一个点,移动一段距离确定第二个点,这个过程中也包括了吸附到顶点/中点/面/交差点的能力

前面一节,我们已经把InputPoint弄清楚了,这个工具类,返回当前鼠标位置可能的吸附点,最终如果没有吸附到任何对象,就是计算和地平面的交点。有了这个基础类,画线工具其实很容易,捕获鼠标点击事件,获取第一个3维坐标点cur_point,再次点击事件把cur_point放到prev_point中,如此反复,除了第一个输入点,后续每个都会创建一个新的线段,同时Draw函数绘制当前的预览线段

创建DrawLineTool

直接上源代码,原本的画线工具远比这个复杂的多,本文尽可能用易于理解的方式讲解,把复杂的功能,拆成step-by-step的实现过程。工具类本质上分2部分,交互部分,执行部分,大部分工具类,都用这种方式拆分。引擎的开发过程中,也是这样先验证交互,预览计算生成的顶点,最后再把顶点合理的连接起来。

class DrawLineTool(core.IBaseTool):
    def __init__(self, view):
        super().__init__(view.view_proxy)
        self.view = view
        self.activate()
        
        self.is_first_point = True
        self.first_point = None
        self.prev_point = None
        self.cur_point = None
    
    def activate(self):
        self.input = core.CInputPointFinder(self.view.view_proxy)
        #启动鼠标追踪
        self.view.setMouseTracking(True)
    
    def deactivate(self):
        #关闭鼠标追踪
        self.view.setMouseTracking(False)
    
    def draw(self, gl):
        self.input.Draw()
        
        if self.is_first_point == False:
            gl.glColor3f(0.0, 0.0, 0.0)
            
            p1 = self.prev_point
            p2 = self.cur_point
            
            gl.glBegin(gl.GL_LINES)
            gl.glVertex3d(p1.x, p1.y, p1.z )
            gl.glVertex3d(p2.x, p2.y, p2.z )
            gl.glEnd()
   
    def on_left_button_down(self, pos, mod):
        px = pos.x()
        py = pos.y()
        
        self.input.Reset()
        ok = self.input.Inference(px, py, None, None )
        assert ok
        
        new_point = self.input.Position()
        if self.is_first_point:
            self.first_point = new_point
            self.is_first_point = False
        else:
            # create line
            pass
        
        self.prev_point = self.cur_point = new_point
    
    def on_left_button_up(self, pos):
        pass

    def on_mouse_move(self, pos, modifiers):
        px = pos.x()
        py = pos.y()
        
        self.input.Reset()
        
        ok = self.input.Inference(px, py, None, None )
        assert ok
        
        self.cur_point = self.input.Position()

运行效果

在这里插入图片描述 从上面的视频可以看到,画线工具的交互已经完全可以了,就差把确认的线段添加到场景里去

这下可以直接用到第三节(用代码绘制线段)中的代码了

找到之前那段代码,把硬编码的point1改成self.prev_point,point2改成self.cur_point,在鼠标点击事件哪里留的#create line注释处调用这个函数就完成

    def add_new_line(self):        
        model = core.active_model()
        comp = model.GetActiveComponent()
        
        point1 = self.prev_point
        point2 = self.cur_point
        
        v1 = comp.PopulateVertex(point1)
        v2 = comp.PopulateVertex(point2)
        
        new_edge = core.CEdge(v1, v2, True).AddToUndoManager()
        comp.AddEntity(new_edge)
        core.IBaseTool.check_model(comp, [new_edge], False)
        
        new_edge.Adopted()
        
        self.view.MarkDirty()
        self.view.repaint()

运行效果

这次已经可以完整的画出线段了,而且如果加入了check_model调用,更神器的效果出现了

当完成一个闭合线段,图形引擎能够自动创建出面,如果画的线跨越了一个面,还能实现对面的切割,对线段的切割,找到封闭区域创建出新的面,都一气呵成了,这跟在Sketchup里的操作完全一样

还能绘制凹多边形,甚至在多边形内部画一个新的多边形完成开孔效果,只要Sketchup里有的,这里都有 在这里插入图片描述

(九):矩形工具

矩形工具也是Sketchup中应用最广的工具之一,往往很多时候,只需要用矩形工具,几乎很少需要铅笔工具

有了InputPoint和画线工具的基础,再增加一个矩形工具相对来说就非常简单

只需要用InputPoint获取对角线,用2个点推算出矩形的边,添加到场景中,调用引擎的合并算法就可以了。

创建RectTool

import core

class DrawRectTool(core.IBaseTool):
    def __init__(self, view):
        super().__init__(view.view_proxy)
        self.view = view
        self.activate()
        
    def reset(self):
        self.state = 1
        self.start_point = None
        self.end_point = None
    
    def activate(self):
        self.input = core.CInputPointFinder(self.view.view_proxy)
        #启动鼠标追踪
        self.view.setMouseTracking(True)
        self.reset()
    
    def deactivate(self):
        #关闭鼠标追踪
        self.view.setMouseTracking(False)
    
    def draw(self, gl):
        self.input.Draw()
        if self.state == 2:
            self.draw_temp_rect(self.start_point, self.end_point, gl)

    def draw_temp_rect(self, p1, p3, gl):
        diff = p3 - p1
        
        d = diff.Dot(self.dir_s)
        p2 = p1.MoveForward(self.dir_s, d)
        
        d = diff.Dot(self.dir_t)
        p4 = p1.MoveForward(self.dir_t, d)

        gl.glColor3f(0.0, 0.0, 1.0);
        self.draw_line(p1, p2, gl)
        self.draw_line(p2, p3, gl)
        self.draw_line(p3, p4, gl)
        self.draw_line(p4, p1, gl)
    
    def draw_line(self, p1, p2, gl):
        gl.glBegin(gl.GL_LINES)
        gl.glVertex3d(*p1.ToTuple())
        gl.glVertex3d(*p2.ToTuple())
        gl.glEnd()
    
    def init_face_space(self, plane):
        s = core.CVector3D()
        t = core.CVector3D()
        plane.ComputeST(s, t)
        self.dir_s = s
        self.dir_t = t

    def get_point(self, px, py):
        input = self.input
        input.Reset()
        ok = input.Inference(px, py, None, None)
        assert ok
        return input.Position()

    def on_left_button_down(self, pos, mod):
        new_point = self.get_point(pos.x(), pos.y())

        if self.state == 1:
            self.start_point = new_point
            
            self.on_face_plane = core.CPlane(0, 0, 1, 0)
            self.init_face_space(self.on_face_plane)
            
            self.state = 2
        else:
            # create rect
            self.state = 1
        
        self.end_point = new_point
    
    def on_left_button_up(self, pos):
        pass

    def on_mouse_move(self, pos, modifiers):
        self.end_point = self.get_point(pos.x(), pos.y())
    
    def on_cancel(self):
        self.reset()

(十):推挤工具

多数人认为推挤工具是Sketchup的核心,从图形学引擎的角度看,我觉得只能算是常用的功能吧,因为大多数建模软件都有挤压(Extrude)工具,Sketchup最本质的区别是多边形建模中点线面的自动合并算法,还有辅助线功能也是独一份的存在

挤压工具算法其实不难,DeepDraw引擎开发的最初,挤压工具也是Python编写的,总共500行左右,不用参考任何代码文章,自己想想,1天时间也能跑起来,实现过程也没有遇到什么障碍。现在DeepDraw引擎集成好了PushPullController,可以直接调用,工具需要的代码就很少,主要就是交互,执行交给引擎来做了

如果大家对挤压算法感兴趣的话,可以自己用API写一下试试,这样非常有帮助,到系列文章完成的时候,我会把答案公布出来供参考

推挤工具+偏移工具的组合,已经基本实现了Sketchup建模范式 在这里插入图片描述

墙体自动开孔,也是Sketchup非常有特色的功能,这才是核心,才是我们需要学习和研究的

在这里插入图片描述

创建PushTool

import core
from push_pull_controller import PushPullController

class PushTool(core.IBaseTool):
    def __init__(self, view):
        super().__init__(view.view_proxy)
        self.view = view
        self.ctrl = None

    def activate(self):
        self.push_dir = None
        self.push_orig = None
        self.push_dist = 0.0
        self.input = core.CInputPointFinder(self.view.view_proxy)
        self.is_begin = False
        self.view.setMouseTracking(True)
    
    def deactivate(self):
        self.view.setMouseTracking(False)

    def draw(self, gl):
        if self.is_begin:
            self.input.Draw()
            
            push_target_point = self.push_orig.MoveForward(self.push_dir, self.push_dist)
            self.draw_line(self.push_orig, push_target_point, gl)
    
    def draw_line(self, p1, p2, gl):
        gl.glColor3f(1.0, 0.0, 0.0);
        gl.glBegin(gl.GL_LINES)
        gl.glVertex3d(p1.x, p1.y, p1.z )
        gl.glVertex3d(p2.x, p2.y, p2.z )
        gl.glEnd()
    
    def on_left_button_down(self, pos, mod):
        self.last_pos = pos
        
        if self.is_begin == False:
            
            pick = core.CPickHelper(self.view.view_proxy)
            pick.SinglePick(pos.x(), pos.y(), 9, None)
            
            push_face = None
            model = self.view.model
            best_face = pick.PickedFace()
            if best_face:
                push_face = best_face.e
            else:
                model.ClearSelection()
                return
                
            self.view.model.UndoManager().StartOperation("Push")

            self.is_begin = True

            self.ctrl = PushPullController()
            self.ctrl.begin(push_face, model)
            
            select_ls = [self.ctrl.push_face]
            model.SetSelection(select_ls)
            
            plane = push_face.GetPlane()
            self.push_dir = core.CVector3D(plane.x, plane.y, plane.z)
            
            pick_ray = pick.GetPickRay()
            
            s = plane.IntersectWithLine(pick_ray)
            self.push_orig = pick_ray.PointAt(s)
            self.push_dist = 0.0

        else:
            self.ctrl.finish()
            self.view.model.UndoManager().CommitOperation()
            self.is_begin = False
            self.view.model.ClearSelection()
        
        self.view.MarkDirty()

    def on_left_button_up(self, pos):
        pass

    def do_find_point(self, px, py):
        input = self.input
        input.Reset()
        
        self.snap_axis = 8
        
        #=======================
        # Pick
        def filter1(e):
            return e not in self.ctrl.filter_set
        
        filter_proxy = core.EntityFilter(filter1)
        
        ok = input.Inference(px, py, filter_proxy, None)
        assert ok
        
        #print( self.input.snap_type)
        
        if self.input.snap_type == core.SnapType.axis_plane:
            #print("SnapType.axis_plane")
            return False, None
        elif self.input.snap_type == core.SnapType.axis:
            #print("SnapType.axis")
            return False, None
        elif self.input.snap_type == core.SnapType.face:
            # check plane
            face = self.input.snap_to
            if face.GetPlane().IsSameTo2(self.ctrl.begin_plane):
                #print("same to begin_plane")
                return False, None
            
            push_face = self.ctrl.push_face
            if not face.GetPlane().GetNormal().InParallelWith(push_face.GetPlane().GetNormal()):
                #print("not paralle")
                return False, None
        
        return True, input.Position()
 
    def on_mouse_move(self, pos, modifiers):
        if self.is_begin:
            ok, point = self.do_find_point(pos.x(), pos.y())
            if ok:
                s1 = (point-self.push_orig).Dot(self.push_dir)
            else:
                pick_ray = self.input.pick_helper.GetPickRay()
                push_ray = core.CStraightLine(self.push_orig, self.push_dir)
                ok, s1, s2 = core.CStraightLine.ComputeNearestPoint(push_ray, pick_ray)
                if not ok:
                    print("Compute push_dist Error")
                    return
            
            value = s1
            
            diff = value - self.push_dist
            
            diff_vec = self.push_dir*diff

            self.ctrl.push(diff_vec)
            
            self.push_dist = value

            # update model
            self.view.MarkDirty()
        
        self.last_pos = pos

(十一):偏移工具

偏移工具的使用也是非常多的,和挤压工具组合起来更是能发挥出神奇的效果,偏移工具代码稍微有点多,要拆开来讲解

源代码

import core

class OffsetTool(core.IBaseTool):
    def __init__(self, view):
        super().__init__(view.view_proxy)
        self.view = view

    def activate(self):
        self.is_begin = False
        
        self.target_face = None
        self.start_point = None
        self.start_point_edge = None
        self.offset_points = None
        
        self.input = core.CInputPointFinder(self.view.view_proxy)
        
        self.view.setMouseTracking(True)

    def deactivate(self):
        self.view.setMouseTracking(False)

    def draw(self, gl):
        if self.is_begin:
            if self.start_point:
                size = self.view.camera_zoom * 0.01
                core.draw_target_with_size(self.start_point, size, gl)
                
                self.draw_offset_loop(gl)
    
    def draw_offset_loop(self, gl):
        if self.offset_points:
            gl.glColor3f(1.0, 0.0, 0.0);
            count = len(self.offset_points)
            for i in range(count):
                p1 = self.offset_points[i-1]
                p2 = self.offset_points[i]
                self.draw_line(p1, p2, gl)
                
    def draw_points(self, gl):
        if self.offset_points:
            size = self.view.camera_zoom * 0.01
            for p in self.offset_points:
                core.draw_target_with_size(p, size, gl)
    
    def create_offset(self):
        if not self.offset_points:
            return
            
        adopt_ls = []
            
        self.view.model.UndoManager().StartOperation("Offset")
        
        comp = self.view.model.GetActiveComponent()
        entity_ls = []
        
        vert_ls = []
        for p, edge in self.offset_edges:
            vert = core.CVertex(p).AddToUndoManager()
            adopt_ls.append(vert)
            vert_ls.append(vert)
            entity_ls.append(vert)

        count = len(vert_ls)
        for i in range(count):
            v1 = vert_ls[i-1]
            v2 = vert_ls[i]
            new_edge = core.CEdge(v1, v2).AddToUndoManager()
            adopt_ls.append(new_edge)
            comp.AddEntity(new_edge)
            entity_ls.append(new_edge)

        comp.CheckModel(entity_ls, None)
        
        self.view.model.UndoManager().CommitOperation()
        
        for e in adopt_ls:
            e.Adopted()
        
        self.view.MarkDirty()
    
    def draw_line(self, p1, p2, gl):
        gl.glBegin(gl.GL_LINES)
        gl.glVertex3d(*p1.ToTuple())
        gl.glVertex3d(*p2.ToTuple())
        gl.glEnd()

    def init_face_space(self, plane):
        s = core.CVector3D()
        t = core.CVector3D()
        plane.ComputeST(s, t)
        self.dir_s = s
        self.dir_t = t
    
    def static_input_face(self):
        face_set = set()
        
        snap_type = self.input.snap_type
        if snap_type == core.SnapType.face:
            face_set.add(self.input.snap_to)
        elif snap_type == core.SnapType.edge or snap_type == core.SnapType.edge_center:
            edge = self.input.snap_to
            for eu in edge.EnumEdgeUse():
                face_set.add(eu.GetParentFace())
        elif snap_type == core.SnapType.vertex:
            vertex = self.input.snap_to
            for edge in vertex.EnumEdges():
                for eu in edge.EnumEdgeUse():
                    face_set.add(eu.GetParentFace())
        
        return face_set
    
    def compute_offset_start(self, x, y):
        pick_ray = self.view.ComputePickRay(x, y)
        plane = self.target_face.GetPlane()
        
        s = plane.IntersectWithLine(pick_ray)
        self.point = pick_ray.PointAt(s)
        
        min_len = 1e10
        near_point = self.point
        snap_edge = None
        
        outer_loop = self.target_face.GetOuterLoop()
        for eu in outer_loop.EnumEdgeUse():
            edge = eu.GetEdge()
            p1 = edge.GetStartVertex().Pos()
            p2 = edge.GetEndVertex().Pos()
            line1 = core.CStraightLine.FromEdge(p1, p2 )
            orig, dir, llen = core.edge_to_ray(eu.GetEdge())
            ok, s1, s2 = core.CStraightLine.ComputeNearestPoint(line1, pick_ray)
            if ok:
                if s1 < 0: s1 = 0
                if s1 > llen: s1 = llen
                point = orig.MoveForward(dir, s1)
                length = (point - self.point).Length()
                if length < min_len:
                    min_len = length
                    near_point = point
                    snap_edge = eu
        
        self.offset_length = min_len
        self.start_point = near_point
        self.start_point_edge = snap_edge
        
    def compute_offset_value(self, x, y):
        camray_orig, camray_dir, camray_end = self.view.ComputeCameraRay(x, y)
        plane = self.target_face.GetPlane()
        s = plane.IntersectWithRay(camray_orig, camray_dir)
        self.point = camray_orig.MoveForward(camray_dir, s)
        
        line_orig, line_dir = self.compute_line_by_edgeuse(self.start_point_edge)
        
        inner_dir = plane.GetNormal().Cross(line_dir)
        clip_plane = core.CPlane(inner_dir, line_orig)
        dist = clip_plane.ProjPoint(self.point)
        self.offset_length = dist
    
    def compute_line_by_edgeuse(self, edgeuse):
        p1 = edgeuse.GetStartVertex().Pos()
        p2 = edgeuse.GetEndVertex().Pos()
        dir = (p2 - p1).Normalized()
        return p1, dir
    
    def compute_offset_line(self, edgeuse, offset, plane):
        p1 = edgeuse.GetStartVertex().Pos()
        p2 = edgeuse.GetEndVertex().Pos()

        diff = p2 - p1
        dir = diff.Normalized()
        offset_dir = plane.GetNormal().Cross(dir)
        orig = p1.MoveForward(offset_dir, offset)
        return core.CStraightLine(orig, dir)
        
    def compute_offset(self, offset_length):
        plane = self.target_face.GetPlane()
        
        points = []
        offset_edges = []
        
        outer_loop = self.target_face.GetOuterLoop()
        for eu in outer_loop.EnumEdgeUse():
            this_line = self.compute_offset_line(eu, offset_length, plane)
            next_line = self.compute_offset_line(eu.Next(), offset_length, plane)
            
            ok, s1, s2 = core.CStraightLine.ComputeNearestPoint(this_line, next_line)
            if ok:
                #assert math.fabs(s1-s2) < EPS
                point = this_line.PointAt(s1)
            else:
                point = this_line.orig
            
            points.append(point)
            item = point, eu.GetEdge()
            offset_edges.append(item)
        
        self.offset_points = points
        self.offset_edges = offset_edges
    
    def get_point(self, px, py):
        input = self.input
        input.Reset()
        ok = input.Inference(px, py, None, None)
        assert ok
        return input.Position()

    def on_left_button_down(self, pos, mod):
        if self.is_begin == False:
            pick = self.input.pick_helper
            pick.Reset()
            pick.SinglePick(pos.x(), pos.y(), 9, None)
            
            self.target_face = None
            model = self.view.model
            best_face = pick.PickedFace()
            if best_face:
                self.target_face = best_face.e
                selected = [self.target_face]
                model.SetSelection(selected)
            else:
                model.ClearSelection()
                return
            
            self.compute_offset_start(pos.x(), pos.y())
            self.is_begin = True
            
        else:
            # 执行
            self.create_offset()
            
            print("ok")
            self.is_begin = False
            self.target_face = None
            self.start_point = None
            self.start_point_edge = None
            self.offset_points = None

    def on_left_button_up(self, pos):
        pass

    def on_mouse_move(self, pos, modifiers):
        if self.is_begin:
            self.compute_offset_value(pos.x(), pos.y())
            self.compute_offset(self.offset_length)

(十二):参考线工具

参考线工具是Sketchup独一份的存在,也是实际画图种非常常用的功能,所以放在核心功能系列讲了

在这里插入图片描述

创建GuideLineTool

import core

class GuideLineTool(core.IBaseTool):
    def __init__(self, view):
        super().__init__(view.view_proxy)
        self.view = view

    def activate(self):
        self.state = core.PressState.press_first
        self.is_begin = False
        self.snap_line = False
        self.start_point = None
        self.prev_point = None
        self.end_point = core.CVector3D()
        
        self.input = core.CInputPointFinder(self.view.view_proxy)
        
        self.view.setMouseTracking(True)
        
    def deactivate(self):
        self.view.setMouseTracking(False)

    def draw(self, gl):
        self.input.Draw()
        
        if self.state == core.PressState.press_second:
            size = self.view.camera_zoom * 0.01
            core.draw_target_with_size(self.start_point, size, gl)
            core.draw_target_with_size(self.end_point, size, gl)

            gl.glColor3f(0.0, 0.0, 0.0);
            core.draw_line(self.start_point, self.end_point, gl)
            
            if self.snap_line:
                self.draw_temp_guide_line(self.end_point, self.line_dir, gl)

    def draw_temp_guide_line(self, orig, dir, gl):
        inf = 100000
        p1 = orig + dir * inf
        p2 = orig + dir * -inf
        gl.glColor3f(0.24, 0.36, 0.53)
        core.draw_line(orig, p1, gl)
        core.draw_line(orig, p2, gl)

    def do_input_inference(self, px, py):
        input = self.input
        input.Reset()
        
        self.snap_axis = 8
        
        #=======================
        # Pick
        if self.state == core.PressState.press_first:
            ok = input.Inference(px, py, None, None)
        else:
            ok = input.Inference(px, py, None, self.start_point)
        assert ok
        new_point = input.Position()
        
        return new_point
    
    def on_left_button_down(self, pos, mod):
        #=======================
        # input_point
        new_point = self.do_input_inference(pos.x(), pos.y())
        
        if self.state == core.PressState.press_first:
            self.state = core.PressState.press_second
            
            snap_type = self.input.snap_type
            
            if snap_type == core.SnapType.edge or snap_type == core.SnapType.edge_center:
                e = self.input.snap_to
                line_start, line_dir, line_len = core.edge_to_ray(e)

                self.line_origin = new_point
                self.line_dir = line_dir
                
                self.snap_line = True

            elif snap_type == core.SnapType.axis:
                axis_id = self.input.snap_to_id
                if axis_id == 0:
                    axis_dir = core.CVector3D(1, 0, 0)
                elif axis_id == 1:
                    axis_dir = core.CVector3D(0, 1, 0)
                elif axis_id == 2:
                    axis_dir = core.CVector3D(0, 0, 1)

                self.line_origin = new_point
                self.line_dir = axis_dir
                
                self.snap_line = True
            
            elif snap_type == core.SnapType.guide_line:

                self.line_origin = new_point
                #self.snap_to = self.input.snap_to
                line = self.input.snap_to.GetStraightLine()
                # snap_to释放后,对其dir的引用就失效了
                # 因该用line.dir.copy()
                self.line_dir = line.dir
                
                self.snap_line = True

            else:
               
                self.snap_line = False
            
            self.end_point = self.start_point = new_point
        
        elif self.state == core.PressState.press_second:
            self.state = core.PressState.press_first
            
            if self.snap_line:
                
                self.create_guide_line(self.end_point, self.line_dir)
            else:
                self.end_point = new_point
            
            self.snap_line = False
    
  
    def on_mouse_move(self, pos, modifiers):
        #=======================
        # input_point
        new_point = self.do_input_inference(pos.x(), pos.y())
        
        if self.snap_line:
            dir_end = new_point - self.line_origin
            dir_up = dir_end.Normalized().Cross(self.line_dir)
            dir_ok = self.line_dir.Cross(dir_up).Normalized()
            
            dist = dir_end.Dot(dir_ok)
            self.end_point = self.line_origin + dir_ok * dist
        else:
            self.end_point = new_point
            self.line_origin = new_point
        
        # 计算长度
        line_len = (self.end_point - self.line_origin).Length()
        
        #print("snap_type:", su_algo.snap_type_name(self.input.snap_type), self.input.snap_type)
        
        do_snap_at = core.SnapType.axis_plane, core.SnapType.last_point_axis, core.SnapType.axis, core.SnapType.face
        if self.input.snap_type in do_snap_at and line_len > core.EPS:
            dir = (self.end_point - self.line_origin).Normalized()
            self.end_point = self.line_origin.MoveForward(dir, line_len)

    def on_left_button_up(self, pos):
        pass

    def create_guide_line(self, orig, _dir):
        self.view.model.UndoManager().StartOperation("Tape")
        guide = core.CGuideLine().AddToUndoManager()
        guide.orig = orig
        guide.dir = _dir
        
        comp = self.view.model.GetActiveComponent()
        comp.AddEntity(guide)
        
        guide.Adopted()

        self.view.model.UndoManager().CommitOperation()
        self.view.MarkDirty()

(十三):删除命令

删除属于立刻执行的功能,不需要交互,不需要状态,所以称之为命令,通过菜单调用 另外还有删除全部参考线的命令 还有很多类似的命令,都是这种类型

在这里插入图片描述 这里测试了多边形开孔的功能,虽然之前就有,但是直到delete完成,才能很直观的演示

在这里插入图片描述 这个墙体建模是不是很熟悉,也需要删除命令,才能完成

创建op_delete_select

import core
#import utils

#+++++++++++++++++++++++
# OP:
# 删除选择的对象
#+++++++++++++++++++++++
def op_delete_selected(model):
    model.UndoManager().StartOperation("Delete select")
    
    selection = model.GetSelection()
    
    face_ls = []
    other_ls = []
    for e in selection:
        if type(e) is core.CFace:
            face_ls.append(e)
        else:
            other_ls.append(e)

    for e in face_ls:
        comp = e.GetParentComponent()
        assert comp
        e.Erase(comp)
    
    for e in other_ls:
        # check already delte
        if e.HasDeleted():
            print(f"{e} has delete, skip")
        else:
            comp = e.GetParentComponent()
            assert comp
            e.Erase(comp)
    
    model.UndoManager().CommitOperation()
    
    model.ClearSelection()



#+++++++++++++++++++++++
# OP:
# delete_all_guide_lines
#+++++++++++++++++++++++
def op_delete_all_guide_lines(model):
    model.UndoManager().StartOperation("Clear guide lines")
    
    comp = model.GetActiveComponent()
    
    guide_ls = []
    for e in comp.EnumEntities():
        if type(e) is core.CGuideLine:
            guide_ls.append(e)
    
    for e in guide_ls:
        comp = e.GetParentComponent()
        assert comp
        e.Erase(comp)
    
    model.UndoManager().CommitOperation()
    
    model.ClearSelection()

调用命令

    #==================
    # Menu command
    #==================
    @Slot()
    def on_actionDelete_triggered(self):
        model = core.active_model()
        operations.op_delete_selected(model)
        # update view
        self.view.MarkDirty()
        self.view.repaint()


    @Slot()
    def on_actionDeleteAllGuide_triggered(self):
        model = core.active_model()
        operations.op_delete_all_guide_lines(model)
        # update view
        self.view.MarkDirty()
        self.view.repaint()
查看「DeepDraw开发文档」系列 →