feat: 初始化Easy Patch插件及依赖文件

- 添加Blender插件核心文件:__init__.py、ui.py、property.py、preference.py
- 添加插件工具模块:g.py、loop.py、generate_loop.py、const.py、op.py
- 添加翻译工具:utils/trans.py
- 添加PuLP线性规划库及其依赖文件,包括CBC求解器二进制文件
- 添加.gitignore和VSCode配置文件
This commit is contained in:
2026-03-03 19:24:57 +08:00
commit ab91b120e6
44 changed files with 17551 additions and 0 deletions

535
boundary.py Normal file
View File

@@ -0,0 +1,535 @@
from __future__ import annotations
import mathutils
import bmesh
from bpy_extras import view3d_utils
import bpy
import numpy as np
from . import const
from .utils import functions
from . import g
class Boundary:
def __init__(self, start_vertex, end_vertex, path):
self.index = -1
self.verts = []
self.path = path
self.start_vertex = start_vertex
self.end_vertex = end_vertex
self.temp_length = 3 # for uncreated boundary
self.mid_co = mathutils.Vector((0, 0, 0))
self.solid_path = []
self.is_editable = True
self.is_hover = False
self.is_solid_start = (
not not start_vertex
and start_vertex not in self.get_boundaries_verts_except_self()
)
self.is_solid_end = None
@classmethod
def create_temp(cls, start_vertex):
instance = cls(
start_vertex,
None,
[],
)
return instance
@property
def length(self):
if self.edges:
return len(self.edges)
else:
return self.temp_length
@length.setter
def length(self, length):
# 固化边
if not self.is_editable:
return
if length < 1:
return
# TODO: 下面两个暂不用考虑两个boundary为同一个loop并且boundary都为1了则冲突了
# 建成和非建成改的长度不一样
if self.edges:
# TODO:特殊情况两个boundary且都为1的时候冲突了
# if (
# length == 1
# and len(self.op_instance.patch.boundaries) == 2
# and any(
# [
# boundary.length == 1
# for boundary in self.op_instance.patch.boundaries
# ]
# )
# ):
# return
# 特殊情况已建成的边如果头尾都吸附到了某个edge的两个顶点上并且不是吸附路径如果这个时候改变分段为1则会报错创建已存在的边
if length == 1:
start_vertex_related_edges = self.start_vertex.link_edges
for start_vertex_related_edge in start_vertex_related_edges:
if self.end_vertex in start_vertex_related_edge.verts:
return
self.change_length(length)
else:
# # TODO:特殊情况两个边且都为1的时候
# if (
# length == 1
# and len(self.op_instance.patch.boundaries) == 1
# and self.op_instance.patch.boundaries[0].length == 1
# ):
# return
# 特殊情况未建成的边如果头尾都吸附到了某个edge的两个顶点上并且不是吸附路径如果这个时候改变分段为1则会报错创建已存在的边强制约束不能小于1
if length <= 1:
return
self.temp_length = length
def get_actual_verts_co_and_path(self):
# 第一个是考虑了temp_path第二个是真实的path
if self.solid_path:
path = self.solid_path
vertex_co_along_path = path
else:
path = self.path
vertex_co_along_path, _ = self.vertices_co_along_path()
return vertex_co_along_path, path
def change_length(self, length):
self.remove_mesh()
self.create_mesh(length)
# self.op_instance.patch.update_after_boundary_change()
functions.on_change_boundary_length(self)
def append_path_from_2d(
self,
context,
co_2d,
is_snap_surface=True,
is_snap_opposite=False,
):
# 獲取base_co
if len(self.path) != 0:
base_co = self.path[-1]
else:
if self.start_vertex:
# vertex.co获取的是局部坐标
start_vertex = g.obj.matrix_world @ self.start_vertex.co
base_co = start_vertex
self.path.append(start_vertex)
else:
base_co = (0, 0, 0)
region = context.region
rv3d = context.region_data
# 阈值
if self.path:
path_last_co_2d = view3d_utils.location_3d_to_region_2d(
region, rv3d, self.path[-1]
) # 视图外会返回None
if not path_last_co_2d:
return
if (
path_last_co_2d - mathutils.Vector(co_2d)
).length < const.draw_path_threshold:
# print("太小不记录")
return
co_3d = []
last_co = mathutils.Vector(base_co)
# Get the ray origin and direction
ray_origin = view3d_utils.region_2d_to_origin_3d(region, rv3d, co_2d)
ray_direction = view3d_utils.region_2d_to_vector_3d(region, rv3d, co_2d)
hit = None
if is_snap_surface:
snap_bvh_trees = g.snap_bvh_trees
for snap_bvh_tree in snap_bvh_trees:
location, normal, index, distance = snap_bvh_tree.ray_cast(
ray_origin, ray_direction
)
if location:
hit = True
break
if hit:
if (
is_snap_opposite or normal.dot(ray_direction.normalized()) < 0
): # 点乘大于0反向
co_3d = location
if not co_3d:
# Project base point
plane_center = last_co
plane_normal = ray_direction
intersection_point = mathutils.geometry.intersect_line_plane(
ray_origin, ray_origin + ray_direction, plane_center, plane_normal
)
if intersection_point:
co_3d = intersection_point
self.path.append(co_3d)
return
def create_mesh(self, length):
if self.solid_path:
self.is_editable = False
# self.edges 和self.verts 已在update_solid中定义并且不用创建实体
else:
vertices_co_along_path_3d, self.verts_correspond_path_idxs = (
self.vertices_co_along_path(length)
)
# create vert
vertices_along_path = []
for i in range(len(vertices_co_along_path_3d)):
coord = vertices_co_along_path_3d[i]
if i == 0:
# 开头
if self.start_vertex:
vertices_along_path.append(self.start_vertex)
else:
vertex = functions.create_temp_vertex(coord)
vertices_along_path.append(vertex)
self.start_vertex = vertex
elif i == len(vertices_co_along_path_3d) - 1:
# 中间
if self.end_vertex:
vertices_along_path.append(self.end_vertex)
else:
vertex = functions.create_temp_vertex(coord)
vertices_along_path.append(vertex)
self.end_vertex = vertex
else:
# 结尾
vertex = functions.create_temp_vertex(coord)
vertices_along_path.append(vertex)
# create edge
boundary_edges = []
for i in range(len(vertices_along_path) - 1):
edge = functions.create_temp_edge(
vertices_along_path[i], vertices_along_path[i + 1]
)
boundary_edges.append(edge)
bmesh.update_edit_mesh(g.obj_me)
self.edges = boundary_edges # ordered edges from start vert
self.verts = vertices_along_path
self.update_mid_co()
@functions.set_block_before_done
def done_from_temp(self, end_vertex):
if (
len(self.path) < 3
): # path代表鼠标路径即使吸附上了solid边这个也是鼠标路径
functions.log("too short, cancel")
return
self.end_vertex = end_vertex
self.is_solid_end = (
not not end_vertex
and end_vertex not in self.get_boundaries_verts_except_self()
)
if (
self.start_vertex
and self.end_vertex
and self.start_vertex == self.end_vertex
):
functions.log("start and end same, cancel")
return
# 开始处理切分↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
# 处理切分
# boundary被切分被切分的boundary的相关的loop要先删pattern及其网格每切分一个boundaryboundary会被删除1条新建3条,然后重建loop然后相关的loop即新建的boundary被切分成2个的boundary要重建网格
# 截断了另外的boundary需要把那些boundary拆分并重建两个可能1.开始或结束点在boundary上 2.开启了自动截断模式(笔记自动截断的情况获取截断点的坐标分割path注意要先创建截断点vert(这里单独写个函数)把他赋予到对应boundary的start和end再执行create_mesh)
mid_verts_to_boundary = (
{}
) # 建立中间点(即除了开始和结束)到boundary的映射。考虑不是每次都建立不然滚轮的时候会频繁创建开销很小不考虑
for b in g.boundaries:
if b == self: # 排除自身
continue
for v in b.verts:
if v == b.start_vertex or v == b.end_vertex:
continue
# # 理论上来说中间点和boundary是一一对应的不可能有重复的情况
# if v in mid_verts_to_boundary:
# raise "mid vert duplicated"
mid_verts_to_boundary[v] = b
# TODO: 暂时无法处理两个切分点都在同一个boundary的中间点上返回None
# TODO: 暂时无法处理两个boundary的情况
# 下面这个约束同时过滤了上面这两个实际上这两个issue要同时解决才能应用
for b in g.boundaries:
if b == self: # 跳过自身(没必要,因为这个都没创建)
continue
if self.start_vertex in b.verts and self.end_vertex in b.verts:
functions.log("2 boundary case, cancel")
return
# 被切割的boundary要先删pattern的mesh,等切割完了再全部recreate
to_be_seperate_boundary_by_end = None
to_be_seperate_boundary_by_end_related_loops = []
if self.end_vertex in mid_verts_to_boundary:
to_be_seperate_boundary_by_end = mid_verts_to_boundary[self.end_vertex]
for loop in functions.get_loops_from_boundary(
to_be_seperate_boundary_by_end
):
# 删pattern
loop.remove_pattern()
to_be_seperate_boundary_by_end_related_loops.append(loop)
to_be_seperate_boundary_by_start = None
to_be_seperate_boundary_by_start_related_loops = []
if self.start_vertex in mid_verts_to_boundary:
to_be_seperate_boundary_by_start = mid_verts_to_boundary[self.start_vertex]
for loop in functions.get_loops_from_boundary(
to_be_seperate_boundary_by_start
):
# 删pattern
loop.remove_pattern()
to_be_seperate_boundary_by_start_related_loops.append(loop)
# 从全局变量删loop
to_be_seperate_boundary_related_loops = set(
(
to_be_seperate_boundary_by_end_related_loops
+ to_be_seperate_boundary_by_start_related_loops
)
) # 取并集因为有可能会有同一个loop的情况
for (
to_be_seperate_boundary_related_loop
) in to_be_seperate_boundary_related_loops:
g.loops.remove(to_be_seperate_boundary_related_loop)
# 从全局变量删boundary
if to_be_seperate_boundary_by_end:
g.boundaries.remove(to_be_seperate_boundary_by_end)
if to_be_seperate_boundary_by_start:
g.boundaries.remove(to_be_seperate_boundary_by_start)
# 切分
if to_be_seperate_boundary_by_end:
need_to_be_cut_boundary = mid_verts_to_boundary[self.end_vertex]
seperate_boundary_1, seperate_boundary_2 = seperate_boundary_from_vert(
need_to_be_cut_boundary, self.end_vertex
)
# 切分为两个的boundary加进全局变量
for i in [seperate_boundary_1, seperate_boundary_2]:
# 重建boundary网格
i.create_mesh(3)
# 附加到全局变量
g.boundaries.add(i)
if to_be_seperate_boundary_by_start:
need_to_be_cut_boundary = mid_verts_to_boundary[self.start_vertex]
seperate_boundary_1, seperate_boundary_2 = seperate_boundary_from_vert(
need_to_be_cut_boundary, self.start_vertex
)
# 切分为两个的boundary加进全局变量
for i in [seperate_boundary_1, seperate_boundary_2]:
# 重建boundary网格
i.create_mesh(3)
# 附加到全局变量
g.boundaries.add(i)
# 重建loop和pattern在外部进行了因为是删loop而不是改loop所以可以自动识别到
# 处理切分完毕↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑
# 如果开始结束都在某个loop的控制点中则要把那个loop删掉正方形对角切
for loop in g.loops:
controll_verts = loop.controll_verts
if (
self.start_vertex in controll_verts
and self.end_vertex in controll_verts
):
g.loops.remove(loop)
loop.remove_pattern()
# 创建网格前,处理吸附到中轴线
if self.is_editable and g.is_drawing_along_mirror:
functions.process_path_to_mirror_axis(self.path)
self.create_mesh(self.length)
g.boundaries.add(self)
g.is_redraw_boundary = True
return self
def remove_mesh(
self, exclude_verts=[], is_force_remove_edges=False, is_remove_start_end=False
):
if is_force_remove_edges:
functions.remove_bm_edges(
g.obj_bm, self.edges
) # using bmesh.ops.delete edges will also delete the island vert
if len(self.edges) == 1:
# 特殊情况,
functions.remove_bm_edges(
g.obj_bm, [self.edges[0]]
) # using bmesh.ops.delete edges will also delete the island vert
if len(self.edges) > 1:
if is_remove_start_end:
# 删除网格,包括开始和结束顶点用于用户在modal的删除
boundaries_verts_except_self = self.get_boundaries_verts_except_self()
to_be_delete_verts = []
remaining_verts = []
for vert in self.verts_generator():
if vert in exclude_verts:
continue # 排除 exclude_verts 中的顶点
if self.is_solid_start and vert == self.start_vertex:
remaining_verts.append(
vert
) # 如果是 solid_start, 跳过start_vertex
continue
if self.is_solid_end and vert == self.end_vertex:
remaining_verts.append(vert) # 如果是 solid_end, 跳过end_vertex
continue
# 并且这个vert不能出现在其他boundary中
if vert in boundaries_verts_except_self:
remaining_verts.append(vert)
continue
to_be_delete_verts.append(vert)
functions.delete_bm_verts(g.obj_bm, to_be_delete_verts)
# 即使经过上面的条件还是可能漏三角形外部画一根截断一个边先删外部的那一根然后删被截断的下面那根然后删上面那根中间会留一个顶点因为一开始就把他当成solid_vert看了但是随着boundary被删除他就不是了
to_be_delete_verts = []
for vert in remaining_verts:
if len(vert.link_edges) == 0:
to_be_delete_verts.append(vert)
functions.delete_bm_verts(g.obj_bm, to_be_delete_verts)
else:
# 删除网格,除了开始和结束顶点,用于改变长度时的删除网格
verts = [
vert
for vert in self.verts_generator()
if vert != self.start_vertex
and vert != self.end_vertex
and vert not in exclude_verts
]
functions.delete_bm_verts(g.obj_bm, verts)
self.edges = []
bmesh.update_edit_mesh(g.obj_me)
def get_boundaries_verts_except_self(self):
boundaries_verts_except_self = set()
for b in g.boundaries:
if b == self:
continue
boundaries_verts_except_self.update(b.verts)
return boundaries_verts_except_self
def verts_generator(self):
# # find first edge
# start_vertex = self.start_vertex
# yield start_vertex
# for edge in self.edges:
# end_vertex = edge.other_vert(start_vertex)
# yield end_vertex
# start_vertex = end_vertex
for i in self.verts: # 搞不懂为什么一开始写成上面这样
yield i
@property
def vertices_co(self):
return [temp.co for temp in list(self.verts_generator())]
def vertices_co_along_path(self, edge_length=None):
if edge_length is None:
edge_length = self.temp_length
return functions.uniform_vertices_co_along_path(self.path, edge_length - 1)
def update_mid_co(self):
midpoint_of_path = functions.get_midpoint_of_path(g.obj_bm, self.edges)
self.mid_co = g.obj.matrix_world @ midpoint_of_path
return
@functions.set_block_before_done
def delete(self):
# boundary删除loop可能会被删除loop被删pattern也会被删
related_loops = functions.get_loops_from_boundary(self)
# 删除网格
for related_loop in related_loops:
related_loop.remove_pattern()
if not self.solid_path:
self.remove_mesh(is_remove_start_end=True)
# 从全局变量中删除loop和boundary
g.boundaries.remove(self)
for related_loop in related_loops:
g.loops.remove(related_loop)
g.is_redraw_boundary = True
g.is_redraw_pattern = True
return
def seperate_from_path_idxs(path_idxs):
# 从多个
pass
def seperate_boundary_from_vert(boundary, seperate_vert):
# 从boundary已有的顶点拆分boundary
# 如果path不够长则只有头尾直线
# # 首先要删除boundaries列表外部删了
# g.boundaries.remove(boundary)
seperate_vert_co_world = g.obj.matrix_world @ seperate_vert.co
# 删除boundary网格不包括开始和结束点和vert
boundary.remove_mesh(exclude_verts=[seperate_vert], is_force_remove_edges=True)
# 拆分
ori_start_vert = boundary.start_vertex
ori_end_vert = boundary.end_vertex
# 通过verts_correspond_path_idxs来获取已有顶点到path索引的映射因为path是不均匀的所以要在计算均匀顶点的时候顺便计算
vert_idx_in_verts = boundary.verts.index(seperate_vert)
vert_idx_in_path = boundary.verts_correspond_path_idxs[
vert_idx_in_verts
] # 如果path 10个则范围是0~9,tou是0尾是9,中间是0~8
if len(boundary.path) < vert_idx_in_path + 2: # 理论不会发生
raise f"vert_idx_in_path exceed path length: {len(boundary.path)} {vert_idx_in_path}"
path_1 = boundary.path[0 : vert_idx_in_path + 1] + [seperate_vert_co_world]
path_2 = [seperate_vert_co_world] + boundary.path[
vert_idx_in_path + 1 : len(boundary.path)
]
boundary_1 = Boundary(ori_start_vert, seperate_vert, path_1)
boundary_2 = Boundary(seperate_vert, ori_end_vert, path_2)
return boundary_1, boundary_2