目录
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()