项目文件夹

文件
wehub-resource-sync 2c632336aa
CI / Viewer CI (push) Successful in 13m37s
CI / Core CI (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:32:38 +08:00

681 行
20 KiB
Python

from __future__ import annotations
import logging
import math
from pathlib import Path
import pytest
from sdk import (
Box,
BoxGeometry,
Cylinder,
Mesh,
MeshGeometry,
Origin,
Part,
Sphere,
SurfaceFrame,
ValidationError,
Visual,
align_centers,
mesh_from_geometry,
part_local_aabb,
place_on_face,
place_on_surface,
sample_catmull_rom_spline_2d,
surface_frame,
wrap_mesh_onto_surface,
wrap_profile_onto_surface,
)
from sdk._core.v0 import placement as placement_module
def _rpy_matrix(origin: Origin) -> tuple[tuple[float, float, float], ...]:
roll, pitch, yaw = origin.rpy
cr = math.cos(roll)
sr = math.sin(roll)
cp = math.cos(pitch)
sp = math.sin(pitch)
cy = math.cos(yaw)
sy = math.sin(yaw)
return (
(cy * cp, cy * sp * sr - sy * cr, cy * sp * cr + sy * sr),
(sy * cp, sy * sp * sr + cy * cr, sy * sp * cr - cy * sr),
(-sp, cp * sr, cp * cr),
)
def _rotate(
mat: tuple[tuple[float, float, float], ...], vec: tuple[float, float, float]
) -> tuple[float, float, float]:
x, y, z = vec
return (
mat[0][0] * x + mat[0][1] * y + mat[0][2] * z,
mat[1][0] * x + mat[1][1] * y + mat[1][2] * z,
mat[2][0] * x + mat[2][1] * y + mat[2][2] * z,
)
def _assert_vec_close(
actual: tuple[float, float, float], expected: tuple[float, float, float], *, tol: float = 1e-6
) -> None:
for a, b in zip(actual, expected):
assert abs(a - b) <= tol
def _radius(vec: tuple[float, float, float]) -> float:
return math.sqrt(vec[0] ** 2 + vec[1] ** 2 + vec[2] ** 2)
def _spherical_log_map(
point: tuple[float, float, float],
*,
center: tuple[float, float, float],
radius: float,
frame: SurfaceFrame,
) -> tuple[float, float]:
normal = (
(point[0] - center[0]) / radius,
(point[1] - center[1]) / radius,
(point[2] - center[2]) / radius,
)
cos_angle = max(-1.0, min(1.0, sum(a * b for a, b in zip(frame.normal, normal))))
angle = math.acos(cos_angle)
if angle <= 1e-12:
return (0.0, 0.0)
tangent = (
normal[0] - cos_angle * frame.normal[0],
normal[1] - cos_angle * frame.normal[1],
normal[2] - cos_angle * frame.normal[2],
)
tangent_norm = math.sqrt(sum(v * v for v in tangent))
tangent_dir = (
tangent[0] / tangent_norm,
tangent[1] / tangent_norm,
tangent[2] / tangent_norm,
)
distance = radius * angle
return (
distance * sum(a * b for a, b in zip(tangent_dir, frame.tangent_u)),
distance * sum(a * b for a, b in zip(tangent_dir, frame.tangent_v)),
)
def _cylindrical_recover_xy(
point: tuple[float, float, float],
*,
radius: float,
frame: SurfaceFrame,
) -> tuple[float, float]:
radial = (point[0], point[1], 0.0)
radial_norm = math.sqrt(radial[0] ** 2 + radial[1] ** 2)
radial_dir = (radial[0] / radial_norm, radial[1] / radial_norm, 0.0)
anchor_radial = (frame.normal[0], frame.normal[1], 0.0)
anchor_radial_norm = math.sqrt(anchor_radial[0] ** 2 + anchor_radial[1] ** 2)
anchor_radial_dir = (
anchor_radial[0] / anchor_radial_norm,
anchor_radial[1] / anchor_radial_norm,
0.0,
)
sin_angle = anchor_radial_dir[0] * radial_dir[1] - anchor_radial_dir[1] * radial_dir[0]
cos_angle = anchor_radial_dir[0] * radial_dir[0] + anchor_radial_dir[1] * radial_dir[1]
circum = radius * math.atan2(sin_angle, cos_angle)
axial = point[2] - frame.point[2]
return (axial, -circum)
def test_place_on_surface_mounts_centered_child_flush_to_sphere() -> None:
origin = place_on_surface(
child=Box((0.020, 0.030, 0.004)),
target=Sphere(radius=1.0),
direction=(1.0, 0.0, 0.0),
child_axis="+z",
)
_assert_vec_close(origin.xyz, (1.002, 0.0, 0.0))
rotated = _rotate(_rpy_matrix(origin), (0.0, 0.0, 1.0))
_assert_vec_close(rotated, (1.0, 0.0, 0.0))
@pytest.mark.parametrize(
("face", "expected_x", "expected_y", "expected_z"),
[
("+x", (0.0, 0.0, -1.0), (0.0, 1.0, 0.0), (1.0, 0.0, 0.0)),
("-x", (0.0, 0.0, 1.0), (0.0, 1.0, 0.0), (-1.0, 0.0, 0.0)),
("+y", (1.0, 0.0, 0.0), (0.0, 0.0, -1.0), (0.0, 1.0, 0.0)),
("-y", (1.0, 0.0, 0.0), (0.0, 0.0, 1.0), (0.0, -1.0, 0.0)),
("+z", (1.0, 0.0, 0.0), (0.0, 1.0, 0.0), (0.0, 0.0, 1.0)),
("-z", (1.0, 0.0, 0.0), (0.0, -1.0, 0.0), (0.0, 0.0, -1.0)),
],
)
def test_place_on_face_uses_documented_face_basis(
face: str,
expected_x: tuple[float, float, float],
expected_y: tuple[float, float, float],
expected_z: tuple[float, float, float],
) -> None:
parent = Part("parent")
parent.visual(Box((2.0, 4.0, 6.0)))
origin = place_on_face(parent, face, prefer_collisions=False)
rot = _rpy_matrix(origin)
_assert_vec_close(_rotate(rot, (1.0, 0.0, 0.0)), expected_x)
_assert_vec_close(_rotate(rot, (0.0, 1.0, 0.0)), expected_y)
_assert_vec_close(_rotate(rot, (0.0, 0.0, 1.0)), expected_z)
def test_align_centers_respects_selected_axes() -> None:
origin = align_centers(
((1.0, 2.0, 3.0), (5.0, 8.0, 9.0)),
((10.0, 20.0, 30.0), (16.0, 28.0, 40.0)),
axes=("x", "z"),
)
assert origin.xyz == (10.0, 0.0, 29.0)
def test_legacy_aabb_placement_helpers_warn() -> None:
with pytest.deprecated_call():
placement_module.align_centers_xy(
((0.0, 0.0, 0.0), (2.0, 4.0, 6.0)),
((10.0, 20.0, 30.0), (14.0, 28.0, 42.0)),
)
with pytest.deprecated_call():
placement_module.place_on_top(
((0.0, 0.0, 0.0), (2.0, 4.0, 6.0)),
((10.0, 20.0, 30.0), (14.0, 28.0, 42.0)),
)
with pytest.deprecated_call():
placement_module.place_in_front_of(
((0.0, 0.0, 0.0), (2.0, 4.0, 6.0)),
((10.0, 20.0, 30.0), (14.0, 28.0, 42.0)),
)
def test_surface_frame_respects_visual_origin_offset() -> None:
target = Visual(
geometry=Sphere(radius=1.0),
origin=Origin(xyz=(0.5, -0.25, 0.1)),
)
frame = surface_frame(
target,
point_hint=(2.5, -0.25, 0.1),
)
assert isinstance(frame, SurfaceFrame)
_assert_vec_close(frame.point, (1.5, -0.25, 0.1))
_assert_vec_close(frame.normal, (1.0, 0.0, 0.0))
def test_surface_frame_uses_mesh_proximity_backend(tmp_path: Path) -> None:
mesh: Mesh = mesh_from_geometry(BoxGeometry((2.0, 2.0, 2.0)), tmp_path / "box.obj")
frame = surface_frame(
mesh,
point_hint=(1.8, 0.2, -0.1),
)
_assert_vec_close(frame.point, (1.0, 0.2, -0.1), tol=1e-4)
_assert_vec_close(frame.normal, (1.0, 0.0, 0.0), tol=1e-4)
def test_surface_frame_uses_scaled_source_geometry_provenance() -> None:
target = Mesh(
filename="dummy.obj",
scale=(2.0, 2.0, 2.0),
source_geometry=Box((1.0, 1.0, 1.0)),
)
frame = surface_frame(
target,
direction=(0.0, 0.0, 1.0),
)
_assert_vec_close(frame.point, (0.0, 0.0, 1.0))
origin = place_on_surface(
child=Box((1.0, 1.0, 1.0)),
target=target,
direction=(0.0, 0.0, 1.0),
child_axis="+z",
)
_assert_vec_close(origin.xyz, (0.0, 0.0, 1.5))
def test_mesh_helpers_resolve_legacy_mesh_prefix(tmp_path: Path) -> None:
mesh_from_geometry(
BoxGeometry((2.0, 4.0, 6.0)),
tmp_path / "assets" / "meshes" / "box.obj",
)
mesh = Mesh(filename="meshes/box.obj")
aabb = part_local_aabb(
Part("body", visuals=[Visual(mesh)]),
asset_root=tmp_path,
prefer_collisions=False,
)
assert aabb == ((-1.0, -2.0, -3.0), (1.0, 2.0, 3.0))
loaded = placement_module._load_trimesh_mesh(mesh, asset_root=tmp_path)
assert tuple(float(v) for v in loaded.extents) == (2.0, 4.0, 6.0)
def test_place_on_surface_uses_mesh_target_for_flush_offset(tmp_path: Path) -> None:
target_mesh: Mesh = mesh_from_geometry(BoxGeometry((2.0, 2.0, 2.0)), tmp_path / "target.obj")
origin = place_on_surface(
child=Box((0.100, 0.100, 0.100)),
target=target_mesh,
point_hint=(1.8, 0.0, 0.0),
child_axis="+z",
clearance=0.010,
)
_assert_vec_close(origin.xyz, (1.06, 0.0, 0.0), tol=1e-4)
def test_surface_mesh_cache_refreshes_when_obj_changes(tmp_path: Path) -> None:
placement_module._TRIMESH_CACHE.clear()
mesh_path = tmp_path / "assets" / "meshes" / "box.obj"
mesh_from_geometry(BoxGeometry((1.0, 1.0, 1.0)), mesh_path)
mesh = Mesh(filename="assets/meshes/box.obj")
loaded1 = placement_module._load_trimesh_mesh(mesh, asset_root=tmp_path)
mesh_from_geometry(BoxGeometry((2.0, 2.0, 2.0)), mesh_path)
loaded2 = placement_module._load_trimesh_mesh(mesh, asset_root=tmp_path)
assert tuple(float(v) for v in loaded1.extents) == (1.0, 1.0, 1.0)
assert tuple(float(v) for v in loaded2.extents) == (2.0, 2.0, 2.0)
assert loaded1 is not loaded2
placement_module._TRIMESH_CACHE.clear()
def test_wrap_mesh_onto_surface_conforms_visible_face_to_sphere() -> None:
geom = MeshGeometry(
vertices=[
(-0.10, 0.0, 0.0),
(0.10, 0.0, 0.0),
(-0.10, 0.0, -0.02),
(0.10, 0.0, -0.02),
],
faces=[(0, 1, 2), (1, 3, 2)],
)
wrapped = wrap_mesh_onto_surface(
geom,
Sphere(radius=1.0),
direction=(1.0, 0.0, 0.0),
child_axis="+z",
visible_relief=0.0,
)
assert isinstance(wrapped, MeshGeometry)
outer = wrapped.vertices[:2]
inner = wrapped.vertices[2:]
for vertex in outer:
radius = math.sqrt(vertex[0] ** 2 + vertex[1] ** 2 + vertex[2] ** 2)
assert abs(radius - 1.0) <= 1e-6
for outer_vertex, inner_vertex in zip(outer, inner):
outer_radius = math.sqrt(outer_vertex[0] ** 2 + outer_vertex[1] ** 2 + outer_vertex[2] ** 2)
inner_radius = math.sqrt(inner_vertex[0] ** 2 + inner_vertex[1] ** 2 + inner_vertex[2] ** 2)
assert inner_radius < outer_radius
assert outer[0][2] < 0.0
assert outer[1][2] > 0.0
def test_wrap_mesh_onto_surface_uses_mesh_target(tmp_path: Path) -> None:
target_mesh: Mesh = mesh_from_geometry(BoxGeometry((2.0, 2.0, 2.0)), tmp_path / "target.obj")
geom = MeshGeometry(
vertices=[
(-0.05, -0.05, 0.01),
(0.05, -0.05, 0.01),
(0.05, 0.05, 0.01),
(-0.05, 0.05, 0.01),
(-0.05, -0.05, -0.01),
(0.05, -0.05, -0.01),
(0.05, 0.05, -0.01),
(-0.05, 0.05, -0.01),
],
faces=[(0, 1, 2), (0, 2, 3), (4, 6, 5), (4, 7, 6)],
)
wrapped = wrap_mesh_onto_surface(
geom,
target_mesh,
point_hint=(1.2, 0.0, 0.0),
child_axis="+z",
visible_relief=0.0,
)
outer = wrapped.vertices[:4]
for vertex in outer:
assert abs(vertex[0] - 1.0) <= 1e-4
def test_wrap_mesh_onto_surface_subdivides_large_patch_with_max_edge() -> None:
geom = MeshGeometry(
vertices=[
(-0.25, -0.20, 0.0),
(0.25, -0.20, 0.0),
(0.00, 0.28, 0.0),
],
faces=[(0, 1, 2)],
)
wrapped = wrap_mesh_onto_surface(
geom,
Sphere(radius=1.0),
direction=(1.0, 0.0, 0.0),
child_axis="+z",
visible_relief=0.0,
max_edge=0.05,
)
assert len(wrapped.faces) > 1
for a, b, c in wrapped.faces:
centroid = (
(wrapped.vertices[a][0] + wrapped.vertices[b][0] + wrapped.vertices[c][0]) / 3.0,
(wrapped.vertices[a][1] + wrapped.vertices[b][1] + wrapped.vertices[c][1]) / 3.0,
(wrapped.vertices[a][2] + wrapped.vertices[b][2] + wrapped.vertices[c][2]) / 3.0,
)
assert _radius(centroid) >= 0.998
def test_wrap_mesh_onto_surface_preserves_shape_on_sphere() -> None:
geom = MeshGeometry(
vertices=[
(-0.03, -0.01, 0.0),
(0.02, -0.015, 0.0),
(0.015, 0.012, 0.0),
(-0.025, 0.008, 0.0),
],
faces=[(0, 1, 2), (0, 2, 3)],
)
direction = (0.12, -0.97, 0.21)
frame = surface_frame(
Sphere(radius=1.0),
direction=direction,
)
wrapped = wrap_mesh_onto_surface(
geom,
Sphere(radius=1.0),
direction=direction,
child_axis="+z",
visible_relief=0.0,
)
recovered = [
_spherical_log_map(vertex, center=(0.0, 0.0, 0.0), radius=1.0, frame=frame)
for vertex in wrapped.vertices
]
expected = [(vertex[0], vertex[1]) for vertex in geom.vertices]
for actual, target in zip(recovered, expected):
assert abs(actual[0] - target[0]) <= 1e-6
assert abs(actual[1] - target[1]) <= 1e-6
def test_wrap_profile_onto_surface_preserves_visible_profile_on_sphere() -> None:
profile = [
(-0.03, -0.01),
(0.02, -0.015),
(0.015, 0.012),
(-0.025, 0.008),
]
direction = (0.12, -0.97, 0.21)
frame = surface_frame(
Sphere(radius=1.0),
direction=direction,
)
wrapped = wrap_profile_onto_surface(
profile,
Sphere(radius=1.0),
thickness=0.02,
direction=direction,
mapping="intrinsic",
visible_relief=0.0,
)
recovered = [
_spherical_log_map(vertex, center=(0.0, 0.0, 0.0), radius=1.0, frame=frame)
for vertex in wrapped.vertices[: len(profile)]
]
for actual, target in zip(recovered, profile):
assert abs(actual[0] - target[0]) <= 1e-6
assert abs(actual[1] - target[1]) <= 1e-6
outer = wrapped.vertices[: len(wrapped.vertices) // 2]
inner = wrapped.vertices[len(wrapped.vertices) // 2 :]
assert len(outer) == len(inner)
for outer_vertex, inner_vertex in zip(outer, inner):
assert abs(_radius(outer_vertex) - 1.0) <= 1e-6
assert _radius(inner_vertex) < _radius(outer_vertex)
def test_wrap_profile_onto_surface_subdivides_only_profile_cap() -> None:
profile = [
(-0.25, -0.20),
(0.25, -0.20),
(0.0, 0.28),
]
wrapped = wrap_profile_onto_surface(
profile,
Sphere(radius=1.0),
thickness=0.02,
direction=(1.0, 0.0, 0.0),
mapping="intrinsic",
visible_relief=0.0,
surface_max_edge=0.05,
)
assert len(wrapped.faces) > 8
outer_vertex_count = len(wrapped.vertices) // 2
outer_faces = [
(a, b, c)
for a, b, c in wrapped.faces
if a < outer_vertex_count and b < outer_vertex_count and c < outer_vertex_count
]
assert outer_faces
for a, b, c in outer_faces:
centroid = (
(wrapped.vertices[a][0] + wrapped.vertices[b][0] + wrapped.vertices[c][0]) / 3.0,
(wrapped.vertices[a][1] + wrapped.vertices[b][1] + wrapped.vertices[c][1]) / 3.0,
(wrapped.vertices[a][2] + wrapped.vertices[b][2] + wrapped.vertices[c][2]) / 3.0,
)
assert _radius(centroid) >= 0.998
def test_wrap_profile_onto_surface_supports_hole_profiles(
caplog: pytest.LogCaptureFixture,
) -> None:
outer = [
(-0.04, -0.04),
(0.04, -0.04),
(0.04, 0.04),
(-0.04, 0.04),
]
hole = [
(-0.012, -0.012),
(0.012, -0.012),
(0.012, 0.012),
(-0.012, 0.012),
]
frame = surface_frame(
Sphere(radius=1.0),
direction=(1.0, 0.0, 0.0),
)
with caplog.at_level(logging.WARNING):
wrapped = wrap_profile_onto_surface(
outer,
Sphere(radius=1.0),
thickness=0.015,
hole_profiles=[hole],
direction=(1.0, 0.0, 0.0),
mapping="intrinsic",
visible_relief=0.0,
surface_max_edge=0.03,
)
visible_points = [
_spherical_log_map(vertex, center=(0.0, 0.0, 0.0), radius=1.0, frame=frame)
for vertex in wrapped.vertices
if abs(_radius(vertex) - 1.0) <= 1e-6
]
assert visible_points
expected_points = outer + hole
for expected in expected_points:
assert (
min(
math.hypot(actual[0] - expected[0], actual[1] - expected[1])
for actual in visible_points
)
<= 1e-6
)
assert "hole_profiles were provided" in caplog.text
def test_wrap_profile_onto_surface_logs_boundary_fallback(
monkeypatch: pytest.MonkeyPatch,
caplog: pytest.LogCaptureFixture,
) -> None:
def fail_planar_cap(*args, **kwargs):
raise ValidationError("profile mesh boundary could not be reconstructed")
monkeypatch.setattr(placement_module, "_solid_from_planar_profile_cap", fail_planar_cap)
with caplog.at_level(logging.WARNING):
wrapped = wrap_profile_onto_surface(
[
(-0.04, -0.04),
(0.04, -0.04),
(0.04, 0.04),
(-0.04, 0.04),
],
Sphere(radius=1.0),
thickness=0.015,
direction=(1.0, 0.0, 0.0),
mapping="intrinsic",
visible_relief=0.0,
surface_max_edge=0.03,
)
assert wrapped.vertices
assert "after planar cap reconstruction failed" in caplog.text
def test_wrap_profile_onto_surface_handles_complex_profile_robustly() -> None:
points = [
(-0.040, 0.010),
(-0.030, 0.025),
(-0.010, 0.036),
(0.014, 0.034),
(0.035, 0.026),
(0.045, 0.010),
(0.040, -0.004),
(0.020, -0.012),
(0.012, -0.032),
(-0.004, -0.040),
(-0.018, -0.024),
(-0.032, -0.012),
]
profile = sample_catmull_rom_spline_2d(
points,
samples_per_segment=10,
closed=True,
)
wrapped = wrap_profile_onto_surface(
profile,
Sphere(radius=1.0),
thickness=0.016,
direction=(0.8, 0.3, 0.52),
mapping="intrinsic",
visible_relief=0.0,
surface_max_edge=0.006,
)
radii = [_radius(vertex) for vertex in wrapped.vertices]
assert len(wrapped.faces) > 1000
assert max(radii) >= 0.999999
assert min(radii) < 0.999
def test_wrap_mesh_onto_surface_preserves_shape_on_cylinder_sidewall() -> None:
geom = MeshGeometry(
vertices=[
(-0.03, -0.01, 0.0),
(0.02, -0.015, 0.0),
(0.015, 0.012, 0.0),
(-0.025, 0.008, 0.0),
],
faces=[(0, 1, 2), (0, 2, 3)],
)
frame = surface_frame(
Cylinder(radius=1.0, length=4.0),
direction=(1.0, 0.0, 0.0),
)
wrapped = wrap_mesh_onto_surface(
geom,
Cylinder(radius=1.0, length=4.0),
direction=(1.0, 0.0, 0.0),
child_axis="+z",
mapping="intrinsic",
visible_relief=0.0,
)
recovered = [
_cylindrical_recover_xy(vertex, radius=1.0, frame=frame) for vertex in wrapped.vertices
]
expected = [(vertex[0], vertex[1]) for vertex in geom.vertices]
for actual, target in zip(recovered, expected):
assert abs(actual[0] - target[0]) <= 1e-6
assert abs(actual[1] - target[1]) <= 1e-6
def test_wrap_mesh_onto_surface_intrinsic_mapping_rejects_unsupported_target(
tmp_path: Path,
) -> None:
target_mesh: Mesh = mesh_from_geometry(BoxGeometry((2.0, 2.0, 2.0)), tmp_path / "target.obj")
geom = MeshGeometry(
vertices=[(-0.05, -0.05, 0.0), (0.05, -0.05, 0.0), (0.0, 0.05, 0.0)],
faces=[(0, 1, 2)],
)
with pytest.raises(ValidationError, match="Intrinsic surface wrapping"):
wrap_mesh_onto_surface(
geom,
target_mesh,
point_hint=(1.2, 0.0, 0.0),
mapping="intrinsic",
)
def test_wrap_mesh_onto_surface_rejects_conflicting_edge_settings() -> None:
geom = MeshGeometry(
vertices=[(-0.05, -0.05, 0.0), (0.05, -0.05, 0.0), (0.0, 0.05, 0.0)],
faces=[(0, 1, 2)],
)
with pytest.raises(ValidationError, match="surface_max_edge or max_edge"):
wrap_mesh_onto_surface(
geom,
Sphere(radius=1.0),
direction=(1.0, 0.0, 0.0),
surface_max_edge=0.05,
max_edge=0.10,
)