4 changed files with 902 additions and 68 deletions
@ -0,0 +1,198 @@ |
|||
import math |
|||
import numpy as np |
|||
import matplotlib.pyplot as plt |
|||
from mpl_toolkits.axes_grid1 import make_axes_locatable |
|||
|
|||
|
|||
def arc_gwn_batch(a, b, bulge, P): |
|||
""" |
|||
向量化批处理接口:对 (N, 2) 的点阵 P 同时计算单段圆弧的广义绕数 W。 |
|||
""" |
|||
a = np.asarray(a, dtype=float) |
|||
b = np.asarray(b, dtype=float) |
|||
P = np.asarray(P, dtype=float) |
|||
|
|||
half_ab = 0.5 * (a + b) |
|||
bisec_vec = 0.5 * np.array([a[1] - b[1], b[0] - a[0]]) |
|||
center = half_ab + bisec_vec / bulge |
|||
|
|||
ca = a - center |
|||
cb = b - center |
|||
cp = P - center |
|||
|
|||
D = np.linalg.norm(cp, axis=1) |
|||
r = np.linalg.norm(ca) |
|||
theta = 2.0 * math.atan(bulge) |
|||
|
|||
W = np.empty(len(P)) |
|||
|
|||
mask_center = D < 1e-12 |
|||
mask_circle = np.abs(D - r) < 1e-12 |
|||
mask_normal = ~(mask_circle) |
|||
|
|||
# W[mask_center] = theta / (2.0 * math.pi) |
|||
W[mask_circle] = theta / (4.0 * math.pi) |
|||
|
|||
cp_n = cp[mask_normal] |
|||
D_n = D[mask_normal] |
|||
|
|||
sigma = np.where(r > D_n, 1.0, -1.0) |
|||
Rd = r * D_n |
|||
r_p_D = r + D_n |
|||
abs_r_D = np.abs(r - D_n) |
|||
|
|||
dot_a = ca.dot(cp_n.T) |
|||
cross1 = cp_n[:, 0] * ca[1] - ca[0] * cp_n[:, 1] |
|||
Ga = np.arctan2(r_p_D * cross1, abs_r_D * (Rd + dot_a)) |
|||
|
|||
dot_b = cb.dot(cp_n.T) |
|||
cross2 = cp_n[:, 0] * cb[1] - cb[0] * cp_n[:, 1] |
|||
Gb = np.arctan2(r_p_D * cross2, abs_r_D * (Rd + dot_b)) |
|||
|
|||
delta = Gb - Ga |
|||
fix = delta * theta < 0.0 |
|||
delta[fix] += math.copysign(math.pi, theta) |
|||
|
|||
two_pi_W = 0.5 * theta + sigma * delta |
|||
W[mask_normal] = two_pi_W / (2.0 * math.pi) |
|||
|
|||
return W |
|||
|
|||
|
|||
def plot_multi_arc_gwn_field(arcs, margin=0.5, nx=400, ny=400): |
|||
""" |
|||
绘制多段圆弧的广义绕数之和。 |
|||
|
|||
参数 |
|||
---- |
|||
arcs : list of tuple |
|||
每段圆弧表示为 (a, b, bulge),其中 a, b 为长度为 2 的序列。 |
|||
margin : float |
|||
AABB 外扩边界。 |
|||
nx, ny : int |
|||
采样分辨率。 |
|||
""" |
|||
arcs = [(np.asarray(a, dtype=float), np.asarray(b, dtype=float), float(bulge)) |
|||
for a, b, bulge in arcs] |
|||
|
|||
# ---- 1. 计算所有圆弧的几何信息,确定全局 AABB ---- |
|||
all_arc_x = [] |
|||
all_arc_y = [] |
|||
all_centers = [] |
|||
all_rs = [] |
|||
|
|||
for a, b, bulge in arcs: |
|||
half_ab = 0.5 * (a + b) |
|||
bisec_vec = 0.5 * np.array([a[1] - b[1], b[0] - a[0]]) |
|||
center = half_ab + bisec_vec / bulge |
|||
r = np.linalg.norm(a - center) |
|||
theta = 2.0 * math.atan(bulge) |
|||
|
|||
start_angle = math.atan2(a[1] - center[1], a[0] - center[0]) |
|||
end_angle = start_angle + theta |
|||
t_arc = np.linspace(start_angle, end_angle, 200) |
|||
arc_x = center[0] + r * np.cos(t_arc) |
|||
arc_y = center[1] + r * np.sin(t_arc) |
|||
|
|||
all_arc_x.append(arc_x) |
|||
all_arc_y.append(arc_y) |
|||
all_centers.append(center) |
|||
all_rs.append(r) |
|||
|
|||
# 全局 AABB |
|||
x_min = min(ax.min() for ax in all_arc_x) - margin |
|||
x_max = max(ax.max() for ax in all_arc_x) + margin |
|||
y_min = min(ay.min() for ay in all_arc_y) - margin |
|||
y_max = max(ay.max() for ay in all_arc_y) + margin |
|||
|
|||
# ---- 2. 均匀采样并累加所有圆弧的 GWN ---- |
|||
xs = np.linspace(x_min, x_max, nx) |
|||
ys = np.linspace(y_min, y_max, ny) |
|||
X, Y = np.meshgrid(xs, ys) |
|||
|
|||
points = np.stack([X.ravel(), Y.ravel()], axis=1) |
|||
W = np.zeros(len(points)) |
|||
|
|||
for a, b, bulge in arcs: |
|||
W += arc_gwn_batch(a, b, bulge, points) |
|||
|
|||
W = W.reshape(X.shape) |
|||
|
|||
# ---- 3. 三分区独立归一化 ---- |
|||
eps = np.finfo(float).eps * 1e6 |
|||
bound_low = -eps |
|||
bound_high = eps |
|||
|
|||
# 负区 |
|||
W_neg = W.copy() |
|||
W_neg[W >= bound_low] = np.nan |
|||
W_min = np.nanmin(W_neg) |
|||
if not np.isnan(W_min): |
|||
W_neg = (bound_low - W_neg) / (bound_low - W_min) |
|||
|
|||
# 零区 |
|||
W_zero = W.copy() |
|||
W_zero[(W < bound_low) | (W > bound_high)] = np.nan |
|||
W_zero = (W_zero - bound_low) / (2.0 * eps) |
|||
|
|||
# 正区 |
|||
W_pos = W.copy() |
|||
W_pos[W <= bound_high] = np.nan |
|||
W_max = np.nanmax(W_pos) |
|||
if not np.isnan(W_max): |
|||
W_pos = (W_pos - bound_high) / (W_max - bound_high) |
|||
|
|||
# ---- 4. 绘图 ---- |
|||
fig, ax = plt.subplots(figsize=(10, 8)) |
|||
|
|||
im_neg = ax.pcolormesh(X, Y, W_neg, cmap='Blues', vmin=0, vmax=1, shading='auto') |
|||
im_zero = ax.pcolormesh(X, Y, W_zero, cmap='Greys', vmin=0, vmax=1, shading='auto') |
|||
im_pos = ax.pcolormesh(X, Y, W_pos, cmap='Reds', vmin=0, vmax=1, shading='auto') |
|||
|
|||
# 叠加所有圆弧 |
|||
for idx, (a, b, bulge) in enumerate(arcs): |
|||
ax.plot(all_arc_x[idx], all_arc_y[idx], 'k-', linewidth=2.5, zorder=4) |
|||
ax.plot(a[0], a[1], 'go', markersize=7, zorder=5) |
|||
ax.plot(b[0], b[1], 'mo', markersize=7, zorder=5) |
|||
ax.plot(all_centers[idx][0], all_centers[idx][1], 'k+', markersize=8, mew=1.5, zorder=5) |
|||
|
|||
ax.set_aspect('equal') |
|||
ax.set_xlim(x_min, x_max) |
|||
ax.set_ylim(y_min, y_max) |
|||
ax.set_title(f'Multi-Arc GWN Sum ({len(arcs)} arcs)') |
|||
|
|||
# ---- 5. 三个独立 colorbar ---- |
|||
divider = make_axes_locatable(ax) |
|||
|
|||
cax_neg = divider.append_axes("left", size="5%", pad=0.4) |
|||
cbar_neg = fig.colorbar(im_neg, cax=cax_neg, orientation='vertical') |
|||
cbar_neg.ax.yaxis.set_ticks_position('left') |
|||
cbar_neg.ax.yaxis.set_label_position('left') |
|||
cbar_neg.set_label('Negative $W$ (normalized)', rotation=90, labelpad=10) |
|||
cbar_neg.set_ticks([0, 1]) |
|||
cbar_neg.set_ticklabels([f'{bound_low:.2e}', f'{W_min:.3f}' if not np.isnan(W_min) else 'N/A']) |
|||
|
|||
cax_pos = divider.append_axes("right", size="5%", pad=0.4) |
|||
cbar_pos = fig.colorbar(im_pos, cax=cax_pos, orientation='vertical') |
|||
cbar_pos.set_label('Positive $W$ (normalized)', rotation=270, labelpad=15) |
|||
cbar_pos.set_ticks([0, 1]) |
|||
cbar_pos.set_ticklabels([f'{bound_high:.2e}', f'{W_max:.3f}' if not np.isnan(W_max) else 'N/A']) |
|||
|
|||
cax_zero = divider.append_axes("bottom", size="5%", pad=0.6) |
|||
cbar_zero = fig.colorbar(im_zero, cax=cax_zero, orientation='horizontal') |
|||
cbar_zero.set_label(f'Near-zero $W$ (normalized) [{bound_low:.2e}, {bound_high:.2e}]', labelpad=5) |
|||
cbar_zero.set_ticks([0, 1]) |
|||
cbar_zero.set_ticklabels([f'{bound_low:.2e}', f'{bound_high:.2e}']) |
|||
|
|||
plt.tight_layout() |
|||
plt.show() |
|||
|
|||
|
|||
# ==================== 主程序 ==================== |
|||
if __name__ == '__main__': |
|||
# 示例 1:两段圆弧拼成近似整圆 |
|||
arcs = [ |
|||
([2.0, 0.0], [0.0, 0.0], float("inf")), # 上半圆 |
|||
([0.0, 0.0], [2.0, 0.0], float("inf")), # 下半圆(注意 bulge>0 表示逆时针) |
|||
] |
|||
plot_multi_arc_gwn_field(arcs, margin=0.5, nx=1024, ny=1024) |
|||
@ -1,85 +1,280 @@ |
|||
#include <iostream> |
|||
#include <array> |
|||
#include <fstream> |
|||
#include <cstdio> |
|||
#include <vector> |
|||
#include "io_primitive.h" |
|||
#include "math/math_defs.hpp" |
|||
#include "mimalloc.h" |
|||
#include "primitive_descriptor.h" |
|||
|
|||
#include <solve.h> |
|||
|
|||
int main() |
|||
void export_obj(const char* filename, const polymesh_t& mesh) { |
|||
std::ofstream f(filename); |
|||
if (!f) { std::cerr << "Cannot open " << filename << std::endl; return; } |
|||
for (uint32_t i = 0; i < mesh.num_vertices; ++i) { |
|||
auto& v = mesh.vertices[i]; |
|||
f << "v " << v.x << " " << v.y << " " << v.z << "\n"; |
|||
} |
|||
uint32_t offset = 0; |
|||
for (uint32_t fi = 0; fi < mesh.num_faces; ++fi) { |
|||
uint32_t count = mesh.vertex_counts[fi]; |
|||
f << "f"; |
|||
for (uint32_t j = 0; j < count; ++j) |
|||
f << " " << (mesh.faces[offset + j] + 1); |
|||
f << "\n"; |
|||
offset += count; |
|||
} |
|||
std::cout << " -> " << filename << ": " << mesh.num_vertices << " verts, " << mesh.num_faces << " faces" << std::endl; |
|||
} |
|||
|
|||
void run_test_case(const char* name, |
|||
polyline_pattern_descriptor_t& pattern_desc, |
|||
polyline_axis_descriptor_t& axis_desc) |
|||
{ |
|||
helixline_axis_descriptor_t axis_desc{}; |
|||
axis_desc.axis_start = {0, 0, 0}; |
|||
axis_desc.axis_end = {0, 0, 13}; |
|||
axis_desc.is_righthanded = true; |
|||
axis_desc.radius = 1.; |
|||
axis_desc.advance_per_round = 1; |
|||
axis_desc.start_direction = {1, 0, 0}; |
|||
|
|||
// polyline_axis_descriptor_t axis_desc{};
|
|||
// std::vector points_{
|
|||
// vector3d{0, 0, 0},
|
|||
// vector3d{0, 1, 1}
|
|||
// };
|
|||
// axis_desc.point_number = 2;
|
|||
// axis_desc.points = points_.data();
|
|||
// std::vector bulges_{std::tan(pi / 8)};
|
|||
// axis_desc.bulge_number = 1;
|
|||
// axis_desc.bulges = bulges_.data();
|
|||
// axis_desc.reference_normal = {1, 0, 0};
|
|||
// axis_desc.is_closed = false;
|
|||
|
|||
polyline_pattern_descriptor_t pattern_desc{}; |
|||
pattern_desc.anchor = {0, 0}; |
|||
std::vector points{ |
|||
vector2d{-.1, 0}, |
|||
vector2d{.1, 0}, |
|||
vector2d{-.1, 0} |
|||
}; |
|||
pattern_desc.point_number = 3; |
|||
pattern_desc.points = points.data(); |
|||
std::vector bulges{1., 1.}; |
|||
pattern_desc.bulge_number = 2; |
|||
pattern_desc.bulges = bulges.data(); |
|||
|
|||
auto primitive_data_center = create_primitive_data_center(); |
|||
auto pattern = create_pattern(primitive_data_center, &pattern_desc, PATTERN_TYPE_POLYLINE); |
|||
auto axis = create_extrude_axis(primitive_data_center, &axis_desc, AXIS_TYPE_EXTRUDE_HELIXLINE); |
|||
auto extrude = create_param_primitive(&axis, &pattern, primitive_data_center, PRIMITIVE_TYPE_EXTRUDE); |
|||
std::cout << "primitive created..." << std::endl; |
|||
|
|||
auto runtime_blobtree = create_blobtree(); |
|||
auto node_iter1 = blobtree_add_primitive_node(runtime_blobtree, extrude); |
|||
auto baked_blobtree = bake_blobtree(runtime_blobtree); |
|||
destroy_blobtree(runtime_blobtree); |
|||
|
|||
std::cout << "blobtree created..." << std::endl; |
|||
|
|||
s_settings settings{}; |
|||
settings.resolution = 32; |
|||
settings.scene_aabb_margin = 1e-5; |
|||
settings.restricted_primitive_bounding_test = true; |
|||
settings.integrand_density = 32; |
|||
auto solver = create_solver(baked_blobtree, settings); |
|||
auto dc = create_primitive_data_center(); |
|||
auto pat = create_pattern(dc, &pattern_desc, PATTERN_TYPE_POLYLINE); |
|||
auto ax = create_extrude_axis(dc, &axis_desc, AXIS_TYPE_EXTRUDE_POLYLINE); |
|||
auto ext = create_param_primitive(&ax, &pat, dc, PRIMITIVE_TYPE_EXTRUDE); |
|||
auto rt = create_blobtree(); |
|||
blobtree_add_primitive_node(rt, ext); |
|||
auto baked = bake_blobtree(rt); |
|||
destroy_blobtree(rt); |
|||
|
|||
s_settings s{}; |
|||
s.resolution = 32; |
|||
s.scene_aabb_margin = 1e-5; |
|||
s.restricted_primitive_bounding_test = true; |
|||
s.integrand_density = 32; |
|||
auto solver = create_solver(baked, s); |
|||
auto result = generate_polymesh(solver); |
|||
// auto integrator = generate_integral_scheme(solver);
|
|||
print_statistics(solver); |
|||
std::cout << name << ": " << (result.success ? "OK" : "FAIL") |
|||
<< " verts=" << result.mesh.num_vertices |
|||
<< " faces=" << result.mesh.num_faces << std::endl; |
|||
|
|||
// integrand_handle_t handle{1., 0, 0, 0};
|
|||
// std::cout << "surface area: " << calculate_surface_integrand(integrator, handle) << std::endl;
|
|||
// std::cout << "volume: " << calculate_volume_integrand(integrator, handle) << std::endl;
|
|||
// std::cout << "baseline (by polymesh): \n";
|
|||
// auto baseline_integral = polymesh_area_and_volume(solver);
|
|||
// std::cout << "surface area: " << baseline_integral.area << std::endl;
|
|||
// std::cout << "volume: " << baseline_integral.volume << std::endl;
|
|||
char filename[256]; |
|||
snprintf(filename, sizeof(filename), "test_extrude_%s.obj", name); |
|||
export_obj(filename, result.mesh); |
|||
|
|||
destroy_solver(solver); |
|||
destroy_baked_blobtree(baked_blobtree); |
|||
destroy_primitive(extrude); |
|||
destroy_primitive_data_center(primitive_data_center); |
|||
destroy_baked_blobtree(baked); |
|||
destroy_primitive(ext); |
|||
destroy_primitive_data_center(dc); |
|||
} |
|||
|
|||
int main() |
|||
{ |
|||
const double b_quarter = std::tan(pi / 8); // tan(theta/4), theta=pi/2 -> quarter circle
|
|||
|
|||
// --- Curved axis: quarter-circle arc from (0,0,0) to (0,1,1) ---
|
|||
polyline_axis_descriptor_t axis_curved{}; |
|||
std::vector axis_pts_curved{ vector3d{0, 0, 0}, vector3d{0, 1, 1} }; |
|||
axis_curved.point_number = 2; |
|||
axis_curved.points = axis_pts_curved.data(); |
|||
std::vector axis_blg_curved{ b_quarter }; |
|||
axis_curved.bulge_number = 1; |
|||
axis_curved.bulges = axis_blg_curved.data(); |
|||
axis_curved.reference_normal = {1, 0, 0}; |
|||
axis_curved.is_closed = false; |
|||
|
|||
// --- Straight axis: from (0,0,0) to (0,0,1) ---
|
|||
polyline_axis_descriptor_t axis_straight{}; |
|||
std::vector axis_pts_straight{ vector3d{0, 0, 0}, vector3d{0, 0, 1} }; |
|||
axis_straight.point_number = 2; |
|||
axis_straight.points = axis_pts_straight.data(); |
|||
std::vector axis_blg_straight{ 0. }; |
|||
axis_straight.bulge_number = 1; |
|||
axis_straight.bulges = axis_blg_straight.data(); |
|||
axis_straight.reference_normal = {1, 0, 0}; |
|||
axis_straight.is_closed = false; |
|||
|
|||
// ==================================================================
|
|||
// Test 1: Original semicircle (reference, all arcs, legacy bulge=1)
|
|||
// ==================================================================
|
|||
{ |
|||
polyline_pattern_descriptor_t pat{}; |
|||
pat.anchor = {0, 0}; |
|||
std::vector pts{ vector2d{-.1, 0}, vector2d{.1, 0}, vector2d{-.1, 0} }; |
|||
pat.point_number = 3; |
|||
pat.points = pts.data(); |
|||
std::vector blg{ 1., 1. }; |
|||
pat.bulge_number = 2; |
|||
pat.bulges = blg.data(); |
|||
run_test_case("01_original_semicircle", pat, axis_curved); |
|||
} |
|||
|
|||
// ==================================================================
|
|||
// Test 2: Rectangle (all lines, bulge=0)
|
|||
// ==================================================================
|
|||
{ |
|||
polyline_pattern_descriptor_t pat{}; |
|||
pat.anchor = {0, 0}; |
|||
std::vector pts{ |
|||
vector2d{-.1, -.1}, vector2d{.1, -.1}, |
|||
vector2d{.1, .1}, vector2d{-.1, .1}, |
|||
vector2d{-.1, -.1} |
|||
}; |
|||
pat.point_number = 5; |
|||
pat.points = pts.data(); |
|||
std::vector blg{ 0., 0., 0., 0. }; |
|||
pat.bulge_number = 4; |
|||
pat.bulges = blg.data(); |
|||
run_test_case("02_rectangle", pat, axis_curved); |
|||
} |
|||
|
|||
// ==================================================================
|
|||
// Test 3: Curved diamond (4 quarter-circle arcs, total theta=2pi)
|
|||
// ==================================================================
|
|||
{ |
|||
polyline_pattern_descriptor_t pat{}; |
|||
pat.anchor = {0, 0}; |
|||
std::vector pts{ |
|||
vector2d{-.12, 0}, vector2d{0, .12}, |
|||
vector2d{.12, 0}, vector2d{0, -.12}, |
|||
vector2d{-.12, 0} |
|||
}; |
|||
pat.point_number = 5; |
|||
pat.points = pts.data(); |
|||
std::vector blg{ b_quarter, b_quarter, b_quarter, b_quarter }; |
|||
pat.bulge_number = 4; |
|||
pat.bulges = blg.data(); |
|||
run_test_case("03_curved_diamond", pat, axis_curved); |
|||
} |
|||
|
|||
// ==================================================================
|
|||
// Test 4: Rectangle with one rounded side (3 lines + 1 arc)
|
|||
// ==================================================================
|
|||
{ |
|||
polyline_pattern_descriptor_t pat{}; |
|||
pat.anchor = {0, 0}; |
|||
std::vector pts{ |
|||
vector2d{-.12, -.08}, vector2d{.12, -.08}, |
|||
vector2d{.12, .08}, vector2d{-.12, .08}, |
|||
vector2d{-.12, -.08} |
|||
}; |
|||
pat.point_number = 5; |
|||
pat.points = pts.data(); |
|||
std::vector blg{ 0., 0., 0., b_quarter }; |
|||
pat.bulge_number = 4; |
|||
pat.bulges = blg.data(); |
|||
run_test_case("04_rect_one_arc", pat, axis_curved); |
|||
} |
|||
|
|||
// ==================================================================
|
|||
// Test 5: Alternating line-arc hexagon (3 lines + 3 quarter arcs)
|
|||
// ==================================================================
|
|||
{ |
|||
polyline_pattern_descriptor_t pat{}; |
|||
pat.anchor = {0, 0}; |
|||
std::vector pts{ |
|||
vector2d{-.10, -.05}, vector2d{0., -.10}, |
|||
vector2d{.10, -.05}, vector2d{.10, .05}, |
|||
vector2d{0., .10}, vector2d{-.10, .05}, |
|||
vector2d{-.10, -.05} |
|||
}; |
|||
pat.point_number = 7; |
|||
pat.points = pts.data(); |
|||
std::vector blg{ 0., b_quarter, 0., b_quarter, 0., b_quarter }; |
|||
pat.bulge_number = 6; |
|||
pat.bulges = blg.data(); |
|||
run_test_case("05_alternating_hex", pat, axis_curved); |
|||
} |
|||
|
|||
// ==================================================================
|
|||
// Test 6: Rectangle on straight axis (baseline, all lines)
|
|||
// ==================================================================
|
|||
{ |
|||
polyline_pattern_descriptor_t pat{}; |
|||
pat.anchor = {0, 0}; |
|||
std::vector pts{ |
|||
vector2d{-.1, -.1}, vector2d{.1, -.1}, |
|||
vector2d{.1, .1}, vector2d{-.1, .1}, |
|||
vector2d{-.1, -.1} |
|||
}; |
|||
pat.point_number = 5; |
|||
pat.points = pts.data(); |
|||
std::vector blg{ 0., 0., 0., 0. }; |
|||
pat.bulge_number = 4; |
|||
pat.bulges = blg.data(); |
|||
run_test_case("06_rect_straight_axis", pat, axis_straight); |
|||
} |
|||
|
|||
// ==================================================================
|
|||
// Test 7: Curved diamond on straight axis
|
|||
// ==================================================================
|
|||
{ |
|||
polyline_pattern_descriptor_t pat{}; |
|||
pat.anchor = {0, 0}; |
|||
std::vector pts{ |
|||
vector2d{-.12, 0}, vector2d{0, .12}, |
|||
vector2d{.12, 0}, vector2d{0, -.12}, |
|||
vector2d{-.12, 0} |
|||
}; |
|||
pat.point_number = 5; |
|||
pat.points = pts.data(); |
|||
std::vector blg{ b_quarter, b_quarter, b_quarter, b_quarter }; |
|||
pat.bulge_number = 4; |
|||
pat.bulges = blg.data(); |
|||
run_test_case("07_curved_diamond_straight", pat, axis_straight); |
|||
} |
|||
|
|||
// ==================================================================
|
|||
// Test 8: Pill/Stadium shape (2 lines + 2 quarter arcs, straight axis)
|
|||
// ==================================================================
|
|||
{ |
|||
polyline_pattern_descriptor_t pat{}; |
|||
pat.anchor = {0, 0}; |
|||
std::vector pts{ |
|||
vector2d{-.06, -.12}, vector2d{-.06, .12}, |
|||
vector2d{ .06, .12}, vector2d{ .06, -.12}, |
|||
vector2d{-.06, -.12} |
|||
}; |
|||
pat.point_number = 5; |
|||
pat.points = pts.data(); |
|||
std::vector blg{ 0., b_quarter, 0., b_quarter }; |
|||
pat.bulge_number = 4; |
|||
pat.bulges = blg.data(); |
|||
run_test_case("08_pill_shape", pat, axis_straight); |
|||
} |
|||
|
|||
// ==================================================================
|
|||
// Test 9: Mixed octagon (4 lines + 4 arcs, straight axis)
|
|||
// ==================================================================
|
|||
{ |
|||
polyline_pattern_descriptor_t pat{}; |
|||
pat.anchor = {0, 0}; |
|||
std::vector pts{ |
|||
vector2d{-.12, -.08}, vector2d{-.06, -.12}, |
|||
vector2d{ .06, -.12}, vector2d{ .12, -.08}, |
|||
vector2d{ .12, .08}, vector2d{ .06, .12}, |
|||
vector2d{-.06, .12}, vector2d{-.12, .08}, |
|||
vector2d{-.12, -.08} |
|||
}; |
|||
pat.point_number = 9; |
|||
pat.points = pts.data(); |
|||
std::vector blg{ b_quarter, 0., b_quarter, 0., b_quarter, 0., b_quarter, 0. }; |
|||
pat.bulge_number = 8; |
|||
pat.bulges = blg.data(); |
|||
run_test_case("09_mixed_octagon", pat, axis_straight); |
|||
} |
|||
|
|||
// ==================================================================
|
|||
// Test 10: Rounded triangle (2 lines + 1 arc, straight axis)
|
|||
// ==================================================================
|
|||
{ |
|||
polyline_pattern_descriptor_t pat{}; |
|||
pat.anchor = {0, 0}; |
|||
std::vector pts{ |
|||
vector2d{-.10, -.10}, vector2d{.10, -.10}, |
|||
vector2d{-.10, .10}, vector2d{-.10, -.10} |
|||
}; |
|||
pat.point_number = 4; |
|||
pat.points = pts.data(); |
|||
std::vector blg{ 0., b_quarter, 0. }; |
|||
pat.bulge_number = 3; |
|||
pat.bulges = blg.data(); |
|||
run_test_case("10_rounded_triangle", pat, axis_straight); |
|||
} |
|||
|
|||
std::cout << "\nAll tests completed." << std::endl; |
|||
return 0; |
|||
} |
|||
@ -0,0 +1,324 @@ |
|||
import numpy as np |
|||
import time |
|||
from math import cos, sin, pi, sqrt |
|||
from typing import List, Optional |
|||
|
|||
# --- OCC Core imports --- |
|||
from OCC.Core.gp import ( |
|||
gp_Pnt, gp_Dir, gp_Ax2, gp_Ax3, gp_Vec, gp_Trsf, gp_Circ |
|||
) |
|||
from OCC.Core.BRepPrimAPI import ( |
|||
BRepPrimAPI_MakeBox, |
|||
BRepPrimAPI_MakeCylinder, |
|||
BRepPrimAPI_MakeSphere |
|||
) |
|||
from OCC.Core.BRepBuilderAPI import ( |
|||
BRepBuilderAPI_MakeEdge, BRepBuilderAPI_MakeWire, |
|||
BRepBuilderAPI_MakeFace, BRepBuilderAPI_Transform |
|||
) |
|||
from OCC.Core.BRepOffsetAPI import BRepOffsetAPI_MakePipe |
|||
from OCC.Core.BRepAlgoAPI import ( |
|||
BRepAlgoAPI_Fuse, BRepAlgoAPI_Cut, BRepAlgoAPI_Common |
|||
) |
|||
from OCC.Core.BRepMesh import BRepMesh_IncrementalMesh |
|||
from OCC.Core.BRep import BRep_Tool |
|||
from OCC.Core.TopExp import TopExp_Explorer |
|||
from OCC.Core.TopAbs import TopAbs_FACE, TopAbs_REVERSED |
|||
from OCC.Core.TopoDS import topods_Face |
|||
from OCC.Core.TopLoc import TopLoc_Location |
|||
from OCC.Core.Bnd import Bnd_Box |
|||
from OCC.Core.BRepBndLib import brepbndlib_Add |
|||
from OCC.Core.TColgp import TColgp_Array1OfPnt |
|||
from OCC.Core.GeomAPI import GeomAPI_PointsToBSpline |
|||
from OCC.Core.BRepCheck import BRepCheck_Analyzer |
|||
|
|||
|
|||
# ============================================================ |
|||
# 1. 基础几何体构造 |
|||
# ============================================================ |
|||
def make_box(x: float, y: float, z: float, |
|||
dx: float, dy: float, dz: float): |
|||
axes = gp_Ax2(gp_Pnt(x, y, z), gp_Dir(0, 0, 1)) |
|||
return BRepPrimAPI_MakeBox(axes, dx, dy, dz).Shape() |
|||
|
|||
|
|||
def make_cylinder(cx: float, cy: float, cz: float, |
|||
radius: float, height: float, |
|||
axis_dir=None): |
|||
if axis_dir is None: |
|||
axis_dir = gp_Dir(0, 0, 1) |
|||
axes = gp_Ax2(gp_Pnt(cx, cy, cz), axis_dir) |
|||
return BRepPrimAPI_MakeCylinder(axes, radius, height).Shape() |
|||
|
|||
|
|||
def make_sphere(cx: float, cy: float, cz: float, radius: float): |
|||
center = gp_Pnt(cx, cy, cz) |
|||
return BRepPrimAPI_MakeSphere(center, radius).Shape() |
|||
|
|||
|
|||
# ============================================================ |
|||
# 2. 螺旋线扫掠 |
|||
# ============================================================ |
|||
def make_bspline_helix(radius: float, pitch: float, |
|||
t0: float, t1: float, n_points: int = 128): |
|||
points = TColgp_Array1OfPnt(1, n_points) |
|||
for i in range(n_points): |
|||
t = t0 + (t1 - t0) * i / (n_points - 1) |
|||
points.SetValue(i + 1, gp_Pnt( |
|||
radius * cos(t), |
|||
radius * sin(t), |
|||
pitch * t / (2.0 * pi) |
|||
)) |
|||
approx = GeomAPI_PointsToBSpline(points) |
|||
return approx.Curve() |
|||
|
|||
|
|||
def make_helix_sweep(helix_r: float, pitch: float, turns: float, |
|||
section_r: float, n_points: int = 128): |
|||
t0, t1 = 0.0, 2.0 * pi * turns |
|||
helix_curve = make_bspline_helix(helix_r, pitch, t0, t1, n_points) |
|||
|
|||
helix_edge = BRepBuilderAPI_MakeEdge(helix_curve).Edge() |
|||
helix_wire = BRepBuilderAPI_MakeWire(helix_edge).Wire() |
|||
|
|||
sec_axes = gp_Ax2(gp_Pnt(helix_r, 0.0, 0.0), gp_Dir(0, 0, 1)) |
|||
sec_circ = gp_Circ(sec_axes, section_r) |
|||
sec_edge = BRepBuilderAPI_MakeEdge(sec_circ).Edge() |
|||
sec_wire = BRepBuilderAPI_MakeWire(sec_edge).Wire() |
|||
sec_face = BRepBuilderAPI_MakeFace(sec_wire).Face() |
|||
|
|||
pipe = BRepOffsetAPI_MakePipe(helix_wire, sec_face) |
|||
pipe.Build() |
|||
if not pipe.IsDone(): |
|||
raise RuntimeError("Helix sweep failed") |
|||
return pipe.Shape() |
|||
|
|||
|
|||
# ============================================================ |
|||
# 3. 布尔运算 |
|||
# ============================================================ |
|||
def boolean_op(a, b, op: str = "fuse"): |
|||
if op == "fuse": |
|||
api = BRepAlgoAPI_Fuse(a, b) |
|||
elif op == "cut": |
|||
api = BRepAlgoAPI_Cut(a, b) |
|||
elif op == "common": |
|||
api = BRepAlgoAPI_Common(a, b) |
|||
else: |
|||
raise ValueError(f"Unknown op: {op}") |
|||
|
|||
if not api.IsDone(): |
|||
raise RuntimeError(f"Boolean {op} failed") |
|||
|
|||
result = api.Shape() |
|||
analyzer = BRepCheck_Analyzer(result) |
|||
if not analyzer.IsValid(): |
|||
print("[Warning] Boolean result has invalid topology") |
|||
return result |
|||
|
|||
|
|||
def cascade_boolean(shapes: List, op: str = "fuse") -> Optional: |
|||
if not shapes: |
|||
return None |
|||
if len(shapes) == 1: |
|||
return shapes[0] |
|||
|
|||
current = shapes[:] |
|||
while len(current) > 1: |
|||
nxt = [] |
|||
for i in range(0, len(current) - 1, 2): |
|||
nxt.append(boolean_op(current[i], current[i + 1], op)) |
|||
if len(current) % 2 == 1: |
|||
nxt.append(current[-1]) |
|||
current = nxt |
|||
return current[0] |
|||
|
|||
|
|||
# ============================================================ |
|||
# 4. 网格提取 |
|||
# ============================================================ |
|||
def extract_triangulation(shape, merge_tol: float = 1e-6): |
|||
vertices = [] |
|||
faces = [] |
|||
vmap = {} |
|||
|
|||
def quantize(v: float) -> int: |
|||
return int(round(v / merge_tol)) |
|||
|
|||
explorer = TopExp_Explorer(shape, TopAbs_FACE) |
|||
while explorer.More(): |
|||
face = topods_Face(explorer.Current()) |
|||
|
|||
loc = TopLoc_Location() |
|||
tri = BRep_Tool.Triangulation(face, loc) |
|||
if tri is None: |
|||
explorer.Next() |
|||
continue |
|||
|
|||
trsf = loc.Transformation() |
|||
n_nodes = tri.NbNodes() |
|||
n_tri = tri.NbTriangles() |
|||
|
|||
local2global = [0] * (n_nodes + 1) |
|||
|
|||
for i in range(1, n_nodes + 1): |
|||
p = tri.Node(i).Transformed(trsf) |
|||
key = (quantize(p.X()), quantize(p.Y()), quantize(p.Z())) |
|||
if key not in vmap: |
|||
idx = len(vertices) |
|||
vmap[key] = idx |
|||
vertices.append((p.X(), p.Y(), p.Z())) |
|||
local2global[i] = idx |
|||
else: |
|||
local2global[i] = vmap[key] |
|||
|
|||
for i in range(1, n_tri + 1): |
|||
t = tri.Triangle(i) |
|||
n1, n2, n3 = t.Get() |
|||
if face.Orientation() == TopAbs_REVERSED: |
|||
n1, n2 = n2, n1 |
|||
faces.append(( |
|||
local2global[n1], |
|||
local2global[n2], |
|||
local2global[n3] |
|||
)) |
|||
|
|||
explorer.Next() |
|||
|
|||
return ( |
|||
np.array(vertices, dtype=np.float64), |
|||
np.array(faces, dtype=np.int32) |
|||
) |
|||
|
|||
|
|||
# ============================================================ |
|||
# 5. 自适应目标面数网格化 |
|||
# ============================================================ |
|||
def bounding_diagonal(shape) -> float: |
|||
bbox = Bnd_Box() |
|||
brepbndlib_Add(shape, bbox) |
|||
x1, y1, z1, x2, y2, z2 = bbox.Get() |
|||
return sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2 + (z2 - z1) ** 2) |
|||
|
|||
|
|||
def mesh_to_target(shape, target_faces: int, |
|||
ang_deflection: float = 0.5, |
|||
tol_percent: float = 0.05, |
|||
max_iter: int = 25): |
|||
diag = bounding_diagonal(shape) |
|||
lo = 1e-4 * diag |
|||
hi = diag |
|||
|
|||
best = None |
|||
best_diff = float('inf') |
|||
|
|||
for _ in range(max_iter): |
|||
mid = (lo + hi) * 0.5 |
|||
|
|||
BRepMesh_IncrementalMesh(shape, mid, |
|||
False, |
|||
ang_deflection, |
|||
True) |
|||
|
|||
verts, faces = extract_triangulation(shape) |
|||
diff = faces.shape[0] - target_faces |
|||
abs_diff = abs(diff) |
|||
|
|||
if abs_diff < best_diff: |
|||
best_diff = abs_diff |
|||
best = (verts.copy(), faces.copy()) |
|||
|
|||
if abs_diff <= target_faces * tol_percent: |
|||
break |
|||
|
|||
if diff > 0: |
|||
lo = mid |
|||
else: |
|||
hi = mid |
|||
|
|||
return best |
|||
|
|||
|
|||
# ============================================================ |
|||
# 6. 导出为 Wavefront OBJ |
|||
# ============================================================ |
|||
def write_obj(path: str, vertices: np.ndarray, faces: np.ndarray): |
|||
""" |
|||
导出 Wavefront OBJ 格式。 |
|||
面索引使用 1-based(OBJ 标准)。 |
|||
""" |
|||
with open(path, 'w') as f: |
|||
f.write("# Generated by pythonOCC\n") |
|||
for v in vertices: |
|||
f.write(f"v {v[0]:.17g} {v[1]:.17g} {v[2]:.17g}\n") |
|||
for tri in faces: |
|||
# OBJ 为 1-based 索引 |
|||
f.write(f"f {tri[0]+1} {tri[1]+1} {tri[2]+1}\n") |
|||
|
|||
|
|||
# ============================================================ |
|||
# 7. 主流程(含计时,仅输出 OBJ) |
|||
# ============================================================ |
|||
if __name__ == "__main__": |
|||
t_total_0 = time.perf_counter() |
|||
|
|||
# -------------------------------------------------------- |
|||
# 7.1 几何体构造 |
|||
# -------------------------------------------------------- |
|||
t_geom_0 = time.perf_counter() |
|||
|
|||
# base = make_box(0, 0, 0, 100, 80, 20) |
|||
# boss = make_cylinder(50, 40, 20, 15, 30) |
|||
# hole = make_cylinder(50, 40, 0, 8, 60) |
|||
# sphere = make_sphere(50, 40, 55, 12) |
|||
|
|||
# spring = make_helix_sweep(20, 10, 3, 3) |
|||
# trsf = gp_Trsf() |
|||
# trsf.SetTranslation(gp_Vec(20, 20, 20)) |
|||
# spring = BRepBuilderAPI_Transform(spring, trsf).Shape() |
|||
|
|||
cylinder = make_cylinder(0, 0, 0, 1, 1) |
|||
sphere = make_sphere(1.9, 0, 0, 1) |
|||
|
|||
t_geom_1 = time.perf_counter() |
|||
print(f"[Timing] Geometry construction: {t_geom_1 - t_geom_0:.4f} s") |
|||
|
|||
# -------------------------------------------------------- |
|||
# 7.2 布尔运算 |
|||
# -------------------------------------------------------- |
|||
t_bool_0 = time.perf_counter() |
|||
|
|||
# body = cascade_boolean([base, boss, sphere, spring], "fuse") |
|||
part = boolean_op(cylinder, sphere, "common") |
|||
|
|||
t_bool_1 = time.perf_counter() |
|||
print(f"[Timing] Boolean operations: {t_bool_1 - t_bool_0:.4f} s") |
|||
|
|||
# -------------------------------------------------------- |
|||
# 7.3 网格化 |
|||
# -------------------------------------------------------- |
|||
t_mesh_0 = time.perf_counter() |
|||
|
|||
verts, faces = mesh_to_target(part, target_faces=10000, |
|||
ang_deflection=0.5, |
|||
tol_percent=0.03) |
|||
|
|||
t_mesh_1 = time.perf_counter() |
|||
print(f"[Timing] Meshing (target 50k faces): {t_mesh_1 - t_mesh_0:.4f} s") |
|||
print(f"[Result] {verts.shape[0]} vertices, {faces.shape[0]} faces") |
|||
|
|||
# -------------------------------------------------------- |
|||
# 7.4 导出 OBJ |
|||
# -------------------------------------------------------- |
|||
t_export_0 = time.perf_counter() |
|||
|
|||
write_obj("common.obj", verts, faces) |
|||
|
|||
t_export_1 = time.perf_counter() |
|||
print(f"[Timing] OBJ export: {t_export_1 - t_export_0:.4f} s") |
|||
|
|||
# -------------------------------------------------------- |
|||
# 总计时 |
|||
# -------------------------------------------------------- |
|||
t_total_1 = time.perf_counter() |
|||
print(f"[Timing] Total elapsed: {t_total_1 - t_total_0:.4f} s") |
|||
@ -0,0 +1,117 @@ |
|||
import vtk |
|||
import datetime |
|||
|
|||
if __name__ == "__main__": |
|||
# Create renderer, render window, and interactor |
|||
renderer = vtk.vtkRenderer() |
|||
render_window = vtk.vtkRenderWindow() |
|||
render_window.AddRenderer(renderer) |
|||
render_window.SetSize(1200, 800) |
|||
render_window_interactor = vtk.vtkRenderWindowInteractor() |
|||
render_window_interactor.SetRenderWindow(render_window) |
|||
|
|||
# 添加坐标轴 |
|||
axes = vtk.vtkAxesActor() |
|||
axes_widget = vtk.vtkOrientationMarkerWidget() |
|||
axes_widget.SetOutlineColor(0.93, 0.57, 0.13) |
|||
axes_widget.SetOrientationMarker(axes) |
|||
axes_widget.SetInteractor(render_window_interactor) |
|||
axes_widget.SetViewport(0.0, 0.0, 0.2, 0.2) |
|||
axes_widget.EnabledOn() |
|||
axes_widget.InteractiveOn() |
|||
|
|||
# Initialize OBJ importer |
|||
obj_importer = vtk.vtkOBJImporter() |
|||
obj_importer.SetFileName("common.obj") # Replace with your OBJ file path |
|||
# obj_importer.SetFileNameMTL("halfpatch_final.mtl") # Replace with your MTL file path |
|||
obj_importer.SetTexturePath("./") # Directory containing texture files |
|||
# Set render window for the importer |
|||
obj_importer.SetRenderWindow(render_window) |
|||
obj_importer.Update() |
|||
|
|||
reader = vtk.vtkOBJReader() |
|||
reader.SetFileName("halfpatch_final.obj") # Replace with your OBJ file path |
|||
reader.Update() |
|||
polydata = reader.GetOutput() |
|||
triangle_filter = vtk.vtkTriangleFilter() |
|||
triangle_filter.SetInputData(polydata) |
|||
triangle_filter.Update() |
|||
polydata_tri = triangle_filter.GetOutput() |
|||
|
|||
mass = vtk.vtkMassProperties() |
|||
mass.SetInputData(polydata_tri) |
|||
print("Surface Area:", mass.GetSurfaceArea()) |
|||
print("Volume:", mass.GetVolume()) |
|||
|
|||
# 获取导入模型的所有actor |
|||
actors = renderer.GetActors() |
|||
actors.InitTraversal() |
|||
|
|||
# 创建线框actor列表 |
|||
wireframe_actors = [] |
|||
|
|||
# 为每个模型actor创建对应的线框actor |
|||
for i in range(actors.GetNumberOfItems()): |
|||
actor = actors.GetNextActor() |
|||
|
|||
# 创建线框表示 |
|||
wireframe_mapper = vtk.vtkPolyDataMapper() |
|||
wireframe_mapper.SetInputData(actor.GetMapper().GetInput()) |
|||
|
|||
# 创建线框actor |
|||
wireframe_actor = vtk.vtkActor() |
|||
wireframe_actor.SetMapper(wireframe_mapper) |
|||
|
|||
# 设置线框属性 |
|||
wireframe_actor.GetProperty().SetRepresentationToWireframe() # 设置为线框模式 |
|||
wireframe_actor.GetProperty().SetColor(1.0, 1.0, 1.0) # 设置线框颜色为白色 |
|||
wireframe_actor.GetProperty().SetLineWidth(2.0) # 设置线框线条粗细,可以调整这个值 |
|||
|
|||
# 使线框显示在模型上方(通过调整渲染顺序) |
|||
wireframe_actor.GetProperty().SetOpacity(0.5) # 设置不透明度 |
|||
wireframe_actor.GetProperty().LightingOff() # 关闭光照,使线框更清晰 |
|||
|
|||
# 设置线框actor的位置和方向与原始actor一致 |
|||
wireframe_actor.SetPosition(actor.GetPosition()) |
|||
wireframe_actor.SetOrientation(actor.GetOrientation()) |
|||
wireframe_actor.SetScale(actor.GetScale()) |
|||
|
|||
# 添加到渲染器和列表 |
|||
renderer.AddActor(wireframe_actor) |
|||
wireframe_actors.append(wireframe_actor) |
|||
|
|||
# Set background color |
|||
renderer.SetBackground(0.1, 0.1, 0.1) # Dark gray background |
|||
|
|||
# 设置默认交互器样式 |
|||
style = vtk.vtkInteractorStyleTrackballCamera() |
|||
render_window_interactor.SetInteractorStyle(style) |
|||
style.SetMotionFactor(5.0) # 默认是10.0,较小的值会减慢移动速度 |
|||
|
|||
# 定义按键回调函数,处理F10截图 |
|||
def on_key_press(obj, event): |
|||
key = obj.GetKeySym() |
|||
if key == 'F10': |
|||
# 创建窗口到图像的过滤器,捕获整个窗口内容 |
|||
window_to_image = vtk.vtkWindowToImageFilter() |
|||
window_to_image.SetInput(obj.GetRenderWindow()) |
|||
window_to_image.Update() |
|||
|
|||
# 生成带时间戳的文件名,避免多次截图覆盖旧文件 |
|||
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") |
|||
filename = f"screenshot_{timestamp}.png" |
|||
|
|||
# 将图像写入PNG文件 |
|||
writer = vtk.vtkPNGWriter() |
|||
writer.SetFileName(filename) |
|||
writer.SetInputConnection(window_to_image.GetOutputPort()) |
|||
writer.Write() |
|||
|
|||
print(f"截图已成功保存到当前目录:{filename}") |
|||
|
|||
# 绑定按键事件观察者,监听键盘输入 |
|||
render_window_interactor.AddObserver('KeyPressEvent', on_key_press) |
|||
|
|||
# Render and start interaction |
|||
render_window.Render() |
|||
render_window_interactor.Start() |
|||
Loading…
Reference in new issue