stbkit 0.1.0a9.dev1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- stbkit/__main__.py +10 -0
- stbkit/api/__init__.py +48 -0
- stbkit/api/experimental/__init__.py +19 -0
- stbkit/api/experimental/repository/__init__.py +30 -0
- stbkit/api/py.typed +0 -0
- stbkit/api/stb_latest.py +7 -0
- stbkit/api/stb_v2_0_2.py +11 -0
- stbkit/api/stb_v2_1_1.py +9 -0
- stbkit/cli/__init__.py +17 -0
- stbkit/cli/_internal/cli_main.py +114 -0
- stbkit/cli/_internal/common.py +126 -0
- stbkit/cli/_internal/constants_exit_code.py +59 -0
- stbkit/cli/_internal/converter.py +693 -0
- stbkit/cli/_internal/diff.py +213 -0
- stbkit/cli/_internal/entry_points.py +25 -0
- stbkit/cli/_internal/global_parent.py +70 -0
- stbkit/cli/_internal/show_versions.py +36 -0
- stbkit/cli/_internal/validate.py +310 -0
- stbkit/cli/commands/convert.py +16 -0
- stbkit/cli/commands/diff.py +16 -0
- stbkit/cli/commands/validate.py +16 -0
- stbkit/cli/py.typed +0 -0
- stbkit/tools/__init__.py +12 -0
- stbkit/tools/_internal/__init__.py +5 -0
- stbkit/tools/_internal/constants.py +21 -0
- stbkit/tools/_internal/data_model/building_geometry.py +1208 -0
- stbkit/tools/_internal/data_model/building_geometry_web.py +106 -0
- stbkit/tools/_internal/data_model/element_data.py +93 -0
- stbkit/tools/_internal/data_model/ifc_data/entity_data.py +67 -0
- stbkit/tools/_internal/data_model/ifc_data/entity_names.py +32 -0
- stbkit/tools/_internal/data_model/shape_data.py +1089 -0
- stbkit/tools/_internal/diff/__init__.py +5 -0
- stbkit/tools/_internal/diff/common.py +50 -0
- stbkit/tools/_internal/diff/diff_xml.py +796 -0
- stbkit/tools/_internal/matrix.py +51 -0
- stbkit/tools/_internal/optional_dependencies/__init__.py +5 -0
- stbkit/tools/_internal/optional_dependencies/ifc_common.py +28 -0
- stbkit/tools/_internal/optional_dependencies/ifc_exporter/ifc_entity_exporter.py +473 -0
- stbkit/tools/_internal/optional_dependencies/ifc_exporter/ifc_profile_exporter.py +267 -0
- stbkit/tools/_internal/optional_dependencies/ifc_exporter/ifc_representation_exporter.py +178 -0
- stbkit/tools/_internal/optional_dependencies/ifc_pipeline.py +133 -0
- stbkit/tools/_internal/optional_dependencies/ifc_to_building_geometry.py +64 -0
- stbkit/tools/_internal/stb_io.py +116 -0
- stbkit/tools/_internal/stb_repair/_common.py +24 -0
- stbkit/tools/_internal/stb_repair/_stb_repair.py +197 -0
- stbkit/tools/_internal/stb_repair/_stb_v2_0_2_repair.py +53 -0
- stbkit/tools/_internal/stb_repair/_stb_v2_1_0_repair.py +109 -0
- stbkit/tools/_internal/stb_repair/_stb_v2_1_1_repair.py +109 -0
- stbkit/tools/_internal/studio_report.py +49 -0
- stbkit/tools/_internal/utils/atomic_writer.py +57 -0
- stbkit/tools/_internal/utils/float_rounding.py +49 -0
- stbkit/tools/_internal/utils/girder_utils.py +91 -0
- stbkit/tools/_internal/utils/guid_utils.py +30 -0
- stbkit/tools/_internal/utils/mesh_utils.py +1305 -0
- stbkit/tools/_internal/utils/node_utils.py +50 -0
- stbkit/tools/_internal/utils/unit_utils.py +23 -0
- stbkit/tools/_internal/vectors.py +222 -0
- stbkit/tools/converters/__init__.py +39 -0
- stbkit/tools/converters/_internal/__init__.py +5 -0
- stbkit/tools/converters/_internal/building_geometry_to_ply_text.py +32 -0
- stbkit/tools/converters/_internal/coord_converter.py +151 -0
- stbkit/tools/converters/_internal/element_data_to_building_geometry.py +549 -0
- stbkit/tools/converters/_internal/ifc_to_ply_text.py +21 -0
- stbkit/tools/converters/_internal/name_converter.py +23 -0
- stbkit/tools/converters/_internal/pipeline/__init__.py +5 -0
- stbkit/tools/converters/_internal/pipeline/data.py +188 -0
- stbkit/tools/converters/_internal/pipeline/execution.py +569 -0
- stbkit/tools/converters/_internal/pipeline/ply_pipeline.py +52 -0
- stbkit/tools/converters/_internal/pipeline/stb_pipeline.py +226 -0
- stbkit/tools/converters/_internal/pipeline/stb_pipeline_latest.py +39 -0
- stbkit/tools/converters/_internal/pipeline/step.py +139 -0
- stbkit/tools/converters/_internal/stb_latest_to_element_data/__init__.py +17 -0
- stbkit/tools/converters/_internal/stb_latest_to_element_data/_stb_latest_to_element_data.py +186 -0
- stbkit/tools/converters/_internal/stb_latest_to_element_data/member/member_footing.py +49 -0
- stbkit/tools/converters/_internal/stb_latest_to_element_data/member/member_line.py +533 -0
- stbkit/tools/converters/_internal/stb_latest_to_element_data/member/member_pile.py +100 -0
- stbkit/tools/converters/_internal/stb_latest_to_element_data/member/member_plane.py +103 -0
- stbkit/tools/converters/_internal/stb_latest_to_element_data/open.py +70 -0
- stbkit/tools/converters/_internal/stb_latest_to_element_data/section/sec.py +214 -0
- stbkit/tools/converters/_internal/stb_latest_to_element_data/section/sec_beam_rc.py +67 -0
- stbkit/tools/converters/_internal/stb_latest_to_element_data/section/sec_beam_s.py +97 -0
- stbkit/tools/converters/_internal/stb_latest_to_element_data/section/sec_beam_src.py +28 -0
- stbkit/tools/converters/_internal/stb_latest_to_element_data/section/sec_brace_s.py +49 -0
- stbkit/tools/converters/_internal/stb_latest_to_element_data/section/sec_column_cft.py +217 -0
- stbkit/tools/converters/_internal/stb_latest_to_element_data/section/sec_column_rc.py +41 -0
- stbkit/tools/converters/_internal/stb_latest_to_element_data/section/sec_column_s.py +127 -0
- stbkit/tools/converters/_internal/stb_latest_to_element_data/section/sec_column_src.py +173 -0
- stbkit/tools/converters/_internal/stb_latest_to_element_data/section/sec_foundation.py +225 -0
- stbkit/tools/converters/_internal/stb_latest_to_element_data/section/sec_parapet_rc.py +53 -0
- stbkit/tools/converters/_internal/stb_latest_to_element_data/section/sec_pile_rc.py +106 -0
- stbkit/tools/converters/_internal/stb_latest_to_element_data/section/sec_pile_s.py +100 -0
- stbkit/tools/converters/_internal/stb_latest_to_element_data/section/sec_steel.py +432 -0
- stbkit/tools/converters/_internal/stb_to_building_geometry.py +44 -0
- stbkit/tools/converters/_internal/stb_to_ifc_data/__init__.py +5 -0
- stbkit/tools/converters/_internal/stb_to_ifc_data/_stb_to_ifc_data.py +59 -0
- stbkit/tools/converters/_internal/stb_to_ifc_data/convert_option.py +22 -0
- stbkit/tools/converters/_internal/stb_to_ply_text.py +22 -0
- stbkit/tools/py.typed +0 -0
- stbkit/tools/upgrade/__init__.py +13 -0
- stbkit/tools/upgrade/_internal/__init__.py +5 -0
- stbkit/tools/upgrade/_internal/common.py +205 -0
- stbkit/tools/upgrade/_internal/upgrade.py +228 -0
- stbkit/tools/upgrade/_internal/v2_0_2_to_v2_1_0/__init__.py +60 -0
- stbkit/tools/upgrade/_internal/v2_0_2_to_v2_1_0/hook_funcs/hook_common.py +62 -0
- stbkit/tools/upgrade/_internal/v2_0_2_to_v2_1_0/hook_funcs/hook_joint_beam_shape_h.py +44 -0
- stbkit/tools/upgrade/_internal/v2_0_2_to_v2_1_0/hook_funcs/hook_sec_pile_rc.py +101 -0
- stbkit/tools/upgrade/_internal/v2_0_2_to_v2_1_0/hook_funcs/hook_sec_pile_s.py +78 -0
- stbkit/tools/upgrade/_internal/v2_0_2_to_v2_1_0/hook_funcs/hook_sec_slab_deck.py +56 -0
- stbkit/tools/upgrade/_internal/v2_0_2_to_v2_1_0/hook_funcs/hook_sec_slab_rc.py +128 -0
- stbkit/tools/upgrade/_internal/v2_0_2_to_v2_1_0/hook_funcs/post_girder.py +126 -0
- stbkit/tools/upgrade/_internal/v2_0_2_to_v2_1_0/hook_funcs/post_open.py +110 -0
- stbkit/tools/upgrade/_internal/v2_0_2_to_v2_1_0/hook_funcs/post_sec_beam_rc.py +227 -0
- stbkit/tools/upgrade/_internal/v2_0_2_to_v2_1_0/hook_funcs/post_sec_beam_s.py +1153 -0
- stbkit/tools/upgrade/_internal/v2_0_2_to_v2_1_0/hooks.py +22 -0
- stbkit/tools/upgrade/_internal/v2_0_2_to_v2_1_0/post.py +42 -0
- stbkit/tools/upgrade/_internal/v2_0_2_to_v2_1_0/tables.py +99 -0
- stbkit/tools/upgrade/_internal/v2_1_0_to_v2_1_1/__init__.py +58 -0
- stbkit/tools/upgrade/_internal/v2_1_0_to_v2_1_1/hooks.py +32 -0
- stbkit/tools/upgrade/_internal/v2_1_0_to_v2_1_1/tables.py +56 -0
- stbkit-0.1.0a9.dev1.dist-info/METADATA +196 -0
- stbkit-0.1.0a9.dev1.dist-info/RECORD +125 -0
- stbkit-0.1.0a9.dev1.dist-info/WHEEL +5 -0
- stbkit-0.1.0a9.dev1.dist-info/entry_points.txt +2 -0
- stbkit-0.1.0a9.dev1.dist-info/licenses/LICENSE +373 -0
- stbkit-0.1.0a9.dev1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,1305 @@
|
|
|
1
|
+
# Copyright 2025-2026 TAISEI CORPORATION
|
|
2
|
+
# SPDX-License-Identifier: MPL-2.0
|
|
3
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
4
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
5
|
+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
6
|
+
|
|
7
|
+
import itertools
|
|
8
|
+
import math
|
|
9
|
+
import warnings
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
|
|
12
|
+
from ..vectors import Vector2d
|
|
13
|
+
|
|
14
|
+
__all__ = ["MeshTriangulationWarning", "create_mesh"]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class MeshTriangulationWarning(UserWarning):
|
|
18
|
+
"""開口を含んだ三角形分割に失敗し、開口を考慮しないことを示す警告。"""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class _TriangulationError(RuntimeError):
|
|
22
|
+
"""内部処理が継続できない場合に投げる例外。"""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(frozen=True, slots=True)
|
|
26
|
+
class _Edge:
|
|
27
|
+
"""開口の辺"""
|
|
28
|
+
|
|
29
|
+
a: Vector2d
|
|
30
|
+
b: Vector2d
|
|
31
|
+
ring_index: int
|
|
32
|
+
edge_index: int
|
|
33
|
+
|
|
34
|
+
def is_intersecting(self, y: float) -> bool:
|
|
35
|
+
"""水平線が辺とぶつかる場合にTrueを返す。"""
|
|
36
|
+
return min(self.a.y, self.b.y) < y < max(self.a.y, self.b.y)
|
|
37
|
+
|
|
38
|
+
def intersection_x(self, y: float, eps: float) -> float:
|
|
39
|
+
"""辺と水平線との交点X座標"""
|
|
40
|
+
if abs(y - self.a.y) <= eps:
|
|
41
|
+
return self.a.x
|
|
42
|
+
if abs(y - self.b.y) <= eps:
|
|
43
|
+
return self.b.x
|
|
44
|
+
|
|
45
|
+
dy: float = self.b.y - self.a.y
|
|
46
|
+
if abs(dy) <= eps:
|
|
47
|
+
raise _TriangulationError("水平辺の交点X座標は計算不可")
|
|
48
|
+
|
|
49
|
+
t: float = (y - self.a.y) / dy
|
|
50
|
+
parameter_tolerance: float = eps / max(abs(dy), eps)
|
|
51
|
+
if t < -parameter_tolerance or t > 1.0 + parameter_tolerance:
|
|
52
|
+
raise _TriangulationError("交点が編の範囲外")
|
|
53
|
+
|
|
54
|
+
t = min(1.0, max(0.0, t))
|
|
55
|
+
return self.a.x + t * (self.b.x - self.a.x)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass(frozen=True, slots=True)
|
|
59
|
+
class _BandInterval:
|
|
60
|
+
"""水平帯中央の、左右の辺を持つX区間。"""
|
|
61
|
+
|
|
62
|
+
left_edge: _Edge
|
|
63
|
+
right_edge: _Edge
|
|
64
|
+
left_x: float
|
|
65
|
+
right_x: float
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class _VertexPool:
|
|
69
|
+
"""許容誤差内の点を同じ頂点インデックスへまとめるクラス。"""
|
|
70
|
+
|
|
71
|
+
def __init__(self, eps: float) -> None:
|
|
72
|
+
self._eps: float = max(eps / 2.0, 1.0e-13)
|
|
73
|
+
self.points: list[Vector2d] = []
|
|
74
|
+
self._bins: dict[tuple[int, int], list[int]] = {}
|
|
75
|
+
|
|
76
|
+
def add(self, point: Vector2d) -> int:
|
|
77
|
+
"""頂点を追加し、既存頂点と一致する場合は既存番号を返す。"""
|
|
78
|
+
size: float = self._eps
|
|
79
|
+
key: tuple[int, int] = (math.floor(point.x / size), math.floor(point.y / size))
|
|
80
|
+
limit: float = size * size
|
|
81
|
+
|
|
82
|
+
for dx in (-1, 0, 1):
|
|
83
|
+
for dy in (-1, 0, 1):
|
|
84
|
+
nearby = self._bins.get((key[0] + dx, key[1] + dy), ())
|
|
85
|
+
for index in nearby:
|
|
86
|
+
if _distance_2(self.points[index], point) <= limit:
|
|
87
|
+
return index
|
|
88
|
+
|
|
89
|
+
index = len(self.points)
|
|
90
|
+
self.points.append(point)
|
|
91
|
+
self._bins.setdefault(key, []).append(index)
|
|
92
|
+
return index
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def create_mesh(
|
|
96
|
+
out_points: list[Vector2d],
|
|
97
|
+
in_points: list[list[Vector2d]] | None = None,
|
|
98
|
+
*,
|
|
99
|
+
merge_tolerance: float | None = None,
|
|
100
|
+
) -> tuple[
|
|
101
|
+
list[Vector2d],
|
|
102
|
+
list[tuple[int, int, int]],
|
|
103
|
+
list[list[Vector2d]] | None,
|
|
104
|
+
list[list[Vector2d]] | None,
|
|
105
|
+
]:
|
|
106
|
+
"""開口付きの形状を三角形分割する。
|
|
107
|
+
|
|
108
|
+
外形と各開口を水平帯に分割し、帯ごとに集合演算を行う。
|
|
109
|
+
|
|
110
|
+
外形のX区間 - 全開口X区間の和集合
|
|
111
|
+
|
|
112
|
+
開口が外形からはみ出す場合、外形内部にある部分のみ差し引く。
|
|
113
|
+
外形に接触する開口は切り欠きとなり、重なる複数開口は一つの開口領域として処理する。
|
|
114
|
+
|
|
115
|
+
今回の分割用途は描画用でありFEMモデル作成用ではないため、メッシュ品質にはこだわらない。
|
|
116
|
+
処理も効率的なものではないが、今回対象としているものは断面の多角形や
|
|
117
|
+
床や壁の開口など、頂点数は多くても2桁のオーダーであるため、そこまでの負荷にはならなと考える。
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
out_points: 外形
|
|
121
|
+
in_points: 開口の一覧
|
|
122
|
+
merge_tolerance: 同じ頂点として扱う許容誤差。
|
|
123
|
+
連続する近接点の除去、頂点の共有、接触判定に使用する。
|
|
124
|
+
省略時は、外形の幅と高さの大きい方x1e-10。
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
次の4要素のtupleを返す。
|
|
128
|
+
|
|
129
|
+
vertices: 分割した頂点。
|
|
130
|
+
faces: 分割した三角形の頂点インデックス一覧。
|
|
131
|
+
new_out_points:
|
|
132
|
+
開口が外形へ統合されるなどが生じて、外形が変化した場合の外形。
|
|
133
|
+
変化がなければNone。
|
|
134
|
+
開口が外形を横断して領域が分断された場合は、複数返す。
|
|
135
|
+
外形全体が開口で除去された場合は空リスト[]。
|
|
136
|
+
new_in_points:
|
|
137
|
+
クリップ、結合、外形への統合、完全な外側開口の除外などにより
|
|
138
|
+
開口が変化した場合の新しい開口一覧。
|
|
139
|
+
変化がなければNone。
|
|
140
|
+
開口が残らない場合は空リスト[]。
|
|
141
|
+
|
|
142
|
+
警告:
|
|
143
|
+
MeshTriangulationWarning: 開口処理に失敗した場合に投げる。
|
|
144
|
+
開口を無視し、外形だけを分割する。この場合new_in_pointsは空リスト[]になる。
|
|
145
|
+
|
|
146
|
+
Raises:
|
|
147
|
+
ValueError:
|
|
148
|
+
外形だけの分割にも失敗した場合、またはmerge_toleranceがおかしい場合に投げる
|
|
149
|
+
"""
|
|
150
|
+
try:
|
|
151
|
+
return _run(out_points, in_points or [], merge_tolerance)
|
|
152
|
+
except _TriangulationError as error:
|
|
153
|
+
if not in_points:
|
|
154
|
+
raise ValueError(f"外形の三角形分割に失敗しました: {error}") from error
|
|
155
|
+
|
|
156
|
+
warnings.warn(
|
|
157
|
+
f"開口を含む三角形分割に失敗したため、開口を無視して外形のみ分割します。 {error}",
|
|
158
|
+
MeshTriangulationWarning,
|
|
159
|
+
stacklevel=2,
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
try:
|
|
163
|
+
vertices, faces, new_outer, _ = _run(
|
|
164
|
+
out_points,
|
|
165
|
+
[],
|
|
166
|
+
merge_tolerance,
|
|
167
|
+
)
|
|
168
|
+
except _TriangulationError as error:
|
|
169
|
+
raise ValueError(f"外形の三角形分割も失敗しました: {error}") from error
|
|
170
|
+
|
|
171
|
+
# 入力された開口除外されたため、[]を返して変更を明示。
|
|
172
|
+
return vertices, faces, new_outer, []
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _run(
|
|
176
|
+
out_points: list[Vector2d],
|
|
177
|
+
in_points: list[list[Vector2d]],
|
|
178
|
+
merge_tolerance: float | None,
|
|
179
|
+
) -> tuple[
|
|
180
|
+
list[Vector2d],
|
|
181
|
+
list[tuple[int, int, int]],
|
|
182
|
+
list[list[Vector2d]] | None,
|
|
183
|
+
list[list[Vector2d]] | None,
|
|
184
|
+
]:
|
|
185
|
+
if len(out_points) < 3:
|
|
186
|
+
raise _TriangulationError("外形の頂点数が3点未満です")
|
|
187
|
+
|
|
188
|
+
min_x: float = min(point.x for point in out_points)
|
|
189
|
+
max_x: float = max(point.x for point in out_points)
|
|
190
|
+
min_y: float = min(point.y for point in out_points)
|
|
191
|
+
max_y: float = max(point.y for point in out_points)
|
|
192
|
+
scale: float = max(max_x - min_x, max_y - min_y)
|
|
193
|
+
if scale <= 0.0 or not math.isfinite(scale):
|
|
194
|
+
raise _TriangulationError("外形の大きさが0です")
|
|
195
|
+
|
|
196
|
+
center_x: float = (min_x + max_x) / 2.0
|
|
197
|
+
center_y: float = (min_y + max_y) / 2.0
|
|
198
|
+
|
|
199
|
+
if merge_tolerance is None:
|
|
200
|
+
eps: float = 1.0e-10
|
|
201
|
+
else:
|
|
202
|
+
if merge_tolerance <= 0.0 or not math.isfinite(merge_tolerance):
|
|
203
|
+
raise ValueError("merge_toleranceは正の有限値で指定してください")
|
|
204
|
+
eps = max(merge_tolerance / scale, 1.0e-14)
|
|
205
|
+
|
|
206
|
+
def normalize(ring: list[Vector2d]) -> list[Vector2d]:
|
|
207
|
+
return [
|
|
208
|
+
Vector2d(
|
|
209
|
+
(point.x - center_x) / scale,
|
|
210
|
+
(point.y - center_y) / scale,
|
|
211
|
+
)
|
|
212
|
+
for point in ring
|
|
213
|
+
]
|
|
214
|
+
|
|
215
|
+
outer: list[Vector2d] = _clean_ring(normalize(out_points), eps, "外形")
|
|
216
|
+
holes: list[list[Vector2d]] = [
|
|
217
|
+
_clean_ring(
|
|
218
|
+
normalize(ring),
|
|
219
|
+
eps,
|
|
220
|
+
f"開口{index}",
|
|
221
|
+
)
|
|
222
|
+
for index, ring in enumerate(in_points, start=1)
|
|
223
|
+
]
|
|
224
|
+
|
|
225
|
+
# ほぼ同じY座標を共通値にする。
|
|
226
|
+
# 同じ高さの異なるリングの頂点が、誤差で薄い水平帯が生じることを防ぐ。
|
|
227
|
+
snapped: list[list[Vector2d]] = _snap_near_y([outer, *holes], eps)
|
|
228
|
+
outer = _clean_ring(snapped[0], eps, "外形")
|
|
229
|
+
holes = [
|
|
230
|
+
_clean_ring(ring, eps, f"開口{index}")
|
|
231
|
+
for index, ring in enumerate(snapped[1:], start=1)
|
|
232
|
+
]
|
|
233
|
+
|
|
234
|
+
_validate_simple_ring(outer, eps, "外形")
|
|
235
|
+
for index, hole in enumerate(holes, start=1):
|
|
236
|
+
_validate_simple_ring(hole, eps, f"開口{index}")
|
|
237
|
+
|
|
238
|
+
normalized_vertices, faces = _triangulate(outer, holes, eps)
|
|
239
|
+
if faces:
|
|
240
|
+
boundary_loops = _extract_boundary_loops(
|
|
241
|
+
normalized_vertices,
|
|
242
|
+
faces,
|
|
243
|
+
eps,
|
|
244
|
+
)
|
|
245
|
+
output_outers, output_holes = _classify_boundary_loops(
|
|
246
|
+
boundary_loops,
|
|
247
|
+
eps,
|
|
248
|
+
)
|
|
249
|
+
else:
|
|
250
|
+
# 外形全体が開口の和集合で覆われた正常な空結果。
|
|
251
|
+
output_outers = []
|
|
252
|
+
output_holes = []
|
|
253
|
+
|
|
254
|
+
comparison_eps: float = max(eps * 8.0, 1.0e-12)
|
|
255
|
+
outer_changed: bool = not _ring_sets_equivalent(
|
|
256
|
+
[_simplify_loop(outer, comparison_eps)],
|
|
257
|
+
output_outers,
|
|
258
|
+
comparison_eps,
|
|
259
|
+
)
|
|
260
|
+
holes_changed = not _ring_sets_equivalent(
|
|
261
|
+
[_simplify_loop(hole, comparison_eps) for hole in holes],
|
|
262
|
+
output_holes,
|
|
263
|
+
comparison_eps,
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
def denormalize(point: Vector2d) -> Vector2d:
|
|
267
|
+
return Vector2d(
|
|
268
|
+
point.x * scale + center_x,
|
|
269
|
+
point.y * scale + center_y,
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
vertices: list[Vector2d] = [denormalize(point) for point in normalized_vertices]
|
|
273
|
+
new_outer: list[list[Vector2d]] | None = (
|
|
274
|
+
[[denormalize(point) for point in outer_loop] for outer_loop in output_outers]
|
|
275
|
+
if outer_changed
|
|
276
|
+
else None
|
|
277
|
+
)
|
|
278
|
+
new_holes: list[list[Vector2d]] | None = (
|
|
279
|
+
[[denormalize(point) for point in hole] for hole in output_holes]
|
|
280
|
+
if holes_changed
|
|
281
|
+
else None
|
|
282
|
+
)
|
|
283
|
+
return vertices, faces, new_outer, new_holes
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def _clean_ring(ring: list[Vector2d], eps: float, label: str) -> list[Vector2d]:
|
|
287
|
+
if not ring:
|
|
288
|
+
raise _TriangulationError(f"{label}に頂点がありません")
|
|
289
|
+
|
|
290
|
+
points: list[Vector2d] = list(ring)
|
|
291
|
+
limit: float = eps * eps
|
|
292
|
+
|
|
293
|
+
if len(points) > 1 and _distance_2(points[0], points[-1]) <= limit:
|
|
294
|
+
points.pop()
|
|
295
|
+
|
|
296
|
+
cleaned: list[Vector2d] = []
|
|
297
|
+
for point in points:
|
|
298
|
+
if not cleaned or _distance_2(cleaned[-1], point) > limit:
|
|
299
|
+
cleaned.append(point)
|
|
300
|
+
|
|
301
|
+
if len(cleaned) > 1 and _distance_2(cleaned[0], cleaned[-1]) <= limit:
|
|
302
|
+
cleaned.pop()
|
|
303
|
+
|
|
304
|
+
if len(cleaned) < 3:
|
|
305
|
+
raise _TriangulationError(f"{label}の有効な頂点が3点未満です")
|
|
306
|
+
|
|
307
|
+
for _ in range(len(cleaned) + 2):
|
|
308
|
+
result: list[Vector2d] = []
|
|
309
|
+
changed: bool = False
|
|
310
|
+
count: int = len(cleaned)
|
|
311
|
+
|
|
312
|
+
for index, point in enumerate(cleaned):
|
|
313
|
+
previous: Vector2d = cleaned[(index - 1) % count]
|
|
314
|
+
following: Vector2d = cleaned[(index + 1) % count]
|
|
315
|
+
local_scale: float = (
|
|
316
|
+
previous.distance_to(point) + point.distance_to(following) + 1.0
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
if abs(_cross(previous, point, following)) <= eps * local_scale:
|
|
320
|
+
if _on_segment(point, previous, following, eps):
|
|
321
|
+
changed = True
|
|
322
|
+
continue
|
|
323
|
+
if _on_segment(previous, point, following, eps) or _on_segment(
|
|
324
|
+
following, previous, point, eps
|
|
325
|
+
):
|
|
326
|
+
raise _TriangulationError(
|
|
327
|
+
f"{label}に折り返しまたは重複する辺があります"
|
|
328
|
+
)
|
|
329
|
+
result.append(point)
|
|
330
|
+
|
|
331
|
+
cleaned = result
|
|
332
|
+
if len(cleaned) < 3:
|
|
333
|
+
raise _TriangulationError(f"{label}の有効な頂点が3点未満です")
|
|
334
|
+
if not changed:
|
|
335
|
+
break
|
|
336
|
+
|
|
337
|
+
area: float = abs(_signed_area(cleaned))
|
|
338
|
+
if area <= max(1.0e-16, eps * _perimeter(cleaned) * 2.0):
|
|
339
|
+
raise _TriangulationError(f"{label}の面積が小さすぎます")
|
|
340
|
+
return cleaned
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def _snap_near_y(rings: list[list[Vector2d]], eps: float) -> list[list[Vector2d]]:
|
|
344
|
+
"""許容誤差内にある頂点Y座標を同じ値へまとめる"""
|
|
345
|
+
values: list[float] = sorted({point.y for ring in rings for point in ring})
|
|
346
|
+
if not values:
|
|
347
|
+
return rings
|
|
348
|
+
|
|
349
|
+
replacement: dict[float, float] = {}
|
|
350
|
+
cluster: list[float] = [values[0]]
|
|
351
|
+
cluster_start: float = values[0]
|
|
352
|
+
|
|
353
|
+
for value in values[1:]:
|
|
354
|
+
if value - cluster_start <= eps:
|
|
355
|
+
cluster.append(value)
|
|
356
|
+
continue
|
|
357
|
+
|
|
358
|
+
representative: float = math.fsum(cluster) / len(cluster)
|
|
359
|
+
replacement.update((item, representative) for item in cluster)
|
|
360
|
+
cluster = [value]
|
|
361
|
+
cluster_start = value
|
|
362
|
+
|
|
363
|
+
representative = math.fsum(cluster) / len(cluster)
|
|
364
|
+
replacement.update((item, representative) for item in cluster)
|
|
365
|
+
|
|
366
|
+
return [
|
|
367
|
+
[Vector2d(point.x, replacement[point.y]) for point in ring] for ring in rings
|
|
368
|
+
]
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def _validate_simple_ring(
|
|
372
|
+
ring: list[Vector2d],
|
|
373
|
+
eps: float,
|
|
374
|
+
label: str,
|
|
375
|
+
) -> None:
|
|
376
|
+
"""リングが自己交差せず、非隣接辺が近すぎないことを確認する。"""
|
|
377
|
+
edges: list[tuple[Vector2d, Vector2d]] = _edge_pairs(ring)
|
|
378
|
+
count: int = len(edges)
|
|
379
|
+
|
|
380
|
+
for first_index, first in enumerate(edges):
|
|
381
|
+
if _distance_2(*first) <= eps * eps:
|
|
382
|
+
raise _TriangulationError(f"{label}に短すぎる辺があります")
|
|
383
|
+
|
|
384
|
+
for second_index in range(first_index + 1, count):
|
|
385
|
+
# 隣接辺は共有端点を持つため、非隣接辺だけ検査する。
|
|
386
|
+
if (
|
|
387
|
+
second_index == (first_index + 1) % count
|
|
388
|
+
or first_index == (second_index + 1) % count
|
|
389
|
+
):
|
|
390
|
+
continue
|
|
391
|
+
|
|
392
|
+
if _segments_too_close(*first, *edges[second_index], eps):
|
|
393
|
+
raise _TriangulationError(
|
|
394
|
+
f"{label}が自己交差しているか、非隣接辺が近すぎます"
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def _triangulate(
|
|
399
|
+
outer: list[Vector2d],
|
|
400
|
+
holes: list[list[Vector2d]],
|
|
401
|
+
eps: float,
|
|
402
|
+
) -> tuple[list[Vector2d], list[tuple[int, int, int]]]:
|
|
403
|
+
"""水平帯ごとの区間差を台形分割し、三角形メッシュを生成する。"""
|
|
404
|
+
rings: list[list[Vector2d]] = [outer, *holes]
|
|
405
|
+
ring_edges: list[list[_Edge]] = [
|
|
406
|
+
_make_edges(ring, ring_index) for ring_index, ring in enumerate(rings)
|
|
407
|
+
]
|
|
408
|
+
all_edges: list[_Edge] = [edge for edges in ring_edges for edge in edges]
|
|
409
|
+
|
|
410
|
+
# 外形辺と開口辺、または異なる開口辺が交差する高さでは、水平帯の境界を追加する。
|
|
411
|
+
# これにより、帯の途中で左右境界が入れ替わらない。
|
|
412
|
+
intersection_points: list[Vector2d] = _collect_cross_ring_intersections(
|
|
413
|
+
all_edges, eps
|
|
414
|
+
)
|
|
415
|
+
event_points: list[Vector2d] = [
|
|
416
|
+
point for ring in rings for point in ring
|
|
417
|
+
] + intersection_points
|
|
418
|
+
|
|
419
|
+
levels: list[float] = _unique(
|
|
420
|
+
[point.y for point in event_points],
|
|
421
|
+
eps / 2.0,
|
|
422
|
+
)
|
|
423
|
+
if len(levels) < 2:
|
|
424
|
+
raise _TriangulationError("Y方向の広がりがありません")
|
|
425
|
+
|
|
426
|
+
events: dict[float, list[float]] = {level: [] for level in levels}
|
|
427
|
+
for point in event_points:
|
|
428
|
+
level: float = _nearest_value(levels, point.y)
|
|
429
|
+
if abs(level - point.y) <= eps * 2.0:
|
|
430
|
+
events[level].append(point.x)
|
|
431
|
+
events = {level: _unique(values, eps / 2.0) for level, values in events.items()}
|
|
432
|
+
|
|
433
|
+
pool: _VertexPool = _VertexPool(eps)
|
|
434
|
+
faces: list[tuple[int, int, int]] = []
|
|
435
|
+
expected_area: float = 0.0
|
|
436
|
+
area_eps: float = max(1.0e-16, eps * eps)
|
|
437
|
+
|
|
438
|
+
for y0, y1 in itertools.pairwise(levels):
|
|
439
|
+
if y1 - y0 <= eps:
|
|
440
|
+
continue
|
|
441
|
+
|
|
442
|
+
middle_y: float = (y0 + y1) / 2.0
|
|
443
|
+
outer_intervals: list[_BandInterval] = _ring_intervals_at_y(
|
|
444
|
+
ring_edges[0],
|
|
445
|
+
middle_y,
|
|
446
|
+
eps,
|
|
447
|
+
"外形",
|
|
448
|
+
)
|
|
449
|
+
|
|
450
|
+
opening_intervals: list[_BandInterval] = []
|
|
451
|
+
for index, edges in enumerate(ring_edges[1:], start=1):
|
|
452
|
+
opening_intervals.extend(
|
|
453
|
+
_ring_intervals_at_y(
|
|
454
|
+
edges,
|
|
455
|
+
middle_y,
|
|
456
|
+
eps,
|
|
457
|
+
f"開口{index}",
|
|
458
|
+
)
|
|
459
|
+
)
|
|
460
|
+
|
|
461
|
+
opening_union: list[_BandInterval] = _merge_intervals(opening_intervals, eps)
|
|
462
|
+
solid_intervals: list[_BandInterval] = []
|
|
463
|
+
for outer_interval in outer_intervals:
|
|
464
|
+
solid_intervals.extend(
|
|
465
|
+
_subtract_intervals(
|
|
466
|
+
outer_interval,
|
|
467
|
+
opening_union,
|
|
468
|
+
eps,
|
|
469
|
+
)
|
|
470
|
+
)
|
|
471
|
+
|
|
472
|
+
for interval in solid_intervals:
|
|
473
|
+
left0: float = interval.left_edge.intersection_x(y0, eps)
|
|
474
|
+
right0: float = interval.right_edge.intersection_x(y0, eps)
|
|
475
|
+
left1: float = interval.left_edge.intersection_x(y1, eps)
|
|
476
|
+
right1: float = interval.right_edge.intersection_x(y1, eps)
|
|
477
|
+
|
|
478
|
+
width0: float = right0 - left0
|
|
479
|
+
width1: float = right1 - left1
|
|
480
|
+
if width0 < -eps or width1 < -eps:
|
|
481
|
+
raise _TriangulationError(
|
|
482
|
+
"水平帯の途中で左右境界が逆転しました。"
|
|
483
|
+
"交点高さの計算に失敗している可能性があります"
|
|
484
|
+
)
|
|
485
|
+
|
|
486
|
+
if abs(width0) <= eps:
|
|
487
|
+
width0 = 0.0
|
|
488
|
+
if abs(width1) <= eps:
|
|
489
|
+
width1 = 0.0
|
|
490
|
+
|
|
491
|
+
cell_area: float = (width0 + width1) * (y1 - y0) / 2.0
|
|
492
|
+
if cell_area <= area_eps:
|
|
493
|
+
continue
|
|
494
|
+
|
|
495
|
+
expected_area += cell_area
|
|
496
|
+
_triangulate_cell(
|
|
497
|
+
pool,
|
|
498
|
+
faces,
|
|
499
|
+
y0,
|
|
500
|
+
y1,
|
|
501
|
+
left0,
|
|
502
|
+
right0,
|
|
503
|
+
left1,
|
|
504
|
+
right1,
|
|
505
|
+
events[y0],
|
|
506
|
+
events[y1],
|
|
507
|
+
eps,
|
|
508
|
+
area_eps,
|
|
509
|
+
)
|
|
510
|
+
|
|
511
|
+
if expected_area <= area_eps:
|
|
512
|
+
# 開口の和集合が外形全体を覆う場合は、空メッシュを正常結果とする。
|
|
513
|
+
return [], []
|
|
514
|
+
if not faces:
|
|
515
|
+
raise _TriangulationError("三角形を生成できませんでした")
|
|
516
|
+
|
|
517
|
+
actual_area: float = math.fsum(
|
|
518
|
+
abs(_cross(pool.points[a], pool.points[b], pool.points[c])) / 2.0
|
|
519
|
+
for a, b, c in faces
|
|
520
|
+
)
|
|
521
|
+
tolerance: float = max(expected_area * 1.0e-8, eps * 16.0, 1.0e-12)
|
|
522
|
+
if abs(actual_area - expected_area) > tolerance:
|
|
523
|
+
raise _TriangulationError(
|
|
524
|
+
"面積検証に失敗しました "
|
|
525
|
+
f"(水平帯={expected_area:.12g}, 三角形={actual_area:.12g})"
|
|
526
|
+
)
|
|
527
|
+
|
|
528
|
+
return pool.points, faces
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
def _make_edges(ring: list[Vector2d], ring_index: int) -> list[_Edge]:
|
|
532
|
+
return [
|
|
533
|
+
_Edge(a, b, ring_index, edge_index)
|
|
534
|
+
for edge_index, (a, b) in enumerate(_edge_pairs(ring))
|
|
535
|
+
]
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
def _collect_cross_ring_intersections(
|
|
539
|
+
edges: list[_Edge],
|
|
540
|
+
eps: float,
|
|
541
|
+
) -> list[Vector2d]:
|
|
542
|
+
result: list[Vector2d] = []
|
|
543
|
+
|
|
544
|
+
for first_index, first in enumerate(edges):
|
|
545
|
+
for second in edges[first_index + 1 :]:
|
|
546
|
+
if first.ring_index == second.ring_index:
|
|
547
|
+
continue
|
|
548
|
+
result.extend(
|
|
549
|
+
_segment_intersection_points(
|
|
550
|
+
first.a,
|
|
551
|
+
first.b,
|
|
552
|
+
second.a,
|
|
553
|
+
second.b,
|
|
554
|
+
eps,
|
|
555
|
+
)
|
|
556
|
+
)
|
|
557
|
+
|
|
558
|
+
return _unique_points(result, eps)
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
def _ring_intervals_at_y(
|
|
562
|
+
edges: list[_Edge],
|
|
563
|
+
y: float,
|
|
564
|
+
eps: float,
|
|
565
|
+
label: str,
|
|
566
|
+
) -> list[_BandInterval]:
|
|
567
|
+
"""単純リングが水平線上で占めるX区間を返す。"""
|
|
568
|
+
crossings: list[tuple[float, _Edge]] = sorted(
|
|
569
|
+
(
|
|
570
|
+
(edge.intersection_x(y, eps), edge)
|
|
571
|
+
for edge in edges
|
|
572
|
+
if edge.is_intersecting(y)
|
|
573
|
+
),
|
|
574
|
+
key=lambda item: (
|
|
575
|
+
item[0],
|
|
576
|
+
item[1].ring_index,
|
|
577
|
+
item[1].edge_index,
|
|
578
|
+
),
|
|
579
|
+
)
|
|
580
|
+
|
|
581
|
+
if len(crossings) % 2:
|
|
582
|
+
raise _TriangulationError(f"高さ{y:g}で{label}の境界交点数が奇数です")
|
|
583
|
+
|
|
584
|
+
result: list[_BandInterval] = []
|
|
585
|
+
for index in range(0, len(crossings), 2):
|
|
586
|
+
left_x, left_edge = crossings[index]
|
|
587
|
+
right_x, right_edge = crossings[index + 1]
|
|
588
|
+
if right_x - left_x <= eps:
|
|
589
|
+
raise _TriangulationError(f"高さ{y:g}で{label}の幅が許容誤差以下です")
|
|
590
|
+
result.append(
|
|
591
|
+
_BandInterval(
|
|
592
|
+
left_edge,
|
|
593
|
+
right_edge,
|
|
594
|
+
left_x,
|
|
595
|
+
right_x,
|
|
596
|
+
)
|
|
597
|
+
)
|
|
598
|
+
return result
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
def _merge_intervals(
|
|
602
|
+
intervals: list[_BandInterval],
|
|
603
|
+
eps: float,
|
|
604
|
+
) -> list[_BandInterval]:
|
|
605
|
+
"""開口区間を重なりと接触を含めて和集合へまとめる。"""
|
|
606
|
+
if not intervals:
|
|
607
|
+
return []
|
|
608
|
+
|
|
609
|
+
ordered: list[_BandInterval] = sorted(
|
|
610
|
+
intervals,
|
|
611
|
+
key=lambda interval: (
|
|
612
|
+
interval.left_x,
|
|
613
|
+
interval.right_x,
|
|
614
|
+
interval.left_edge.ring_index,
|
|
615
|
+
interval.left_edge.edge_index,
|
|
616
|
+
),
|
|
617
|
+
)
|
|
618
|
+
merged: list[_BandInterval] = [ordered[0]]
|
|
619
|
+
|
|
620
|
+
for interval in ordered[1:]:
|
|
621
|
+
current: _BandInterval = merged[-1]
|
|
622
|
+
if interval.left_x > current.right_x + eps:
|
|
623
|
+
merged.append(interval)
|
|
624
|
+
continue
|
|
625
|
+
|
|
626
|
+
# 区間が重なる場合、左端は並び順の先頭を維持し、
|
|
627
|
+
# 右端だけをより遠くまで伸びる境界へ更新する。
|
|
628
|
+
if interval.right_x > current.right_x + eps:
|
|
629
|
+
merged[-1] = _BandInterval(
|
|
630
|
+
current.left_edge,
|
|
631
|
+
interval.right_edge,
|
|
632
|
+
current.left_x,
|
|
633
|
+
interval.right_x,
|
|
634
|
+
)
|
|
635
|
+
|
|
636
|
+
return merged
|
|
637
|
+
|
|
638
|
+
|
|
639
|
+
def _subtract_intervals(
|
|
640
|
+
outer: _BandInterval,
|
|
641
|
+
openings: list[_BandInterval],
|
|
642
|
+
eps: float,
|
|
643
|
+
) -> list[_BandInterval]:
|
|
644
|
+
"""一つの外形区間から開口区間の和集合を差し引く。"""
|
|
645
|
+
result: list[_BandInterval] = []
|
|
646
|
+
cursor_x: float = outer.left_x
|
|
647
|
+
cursor_edge: _Edge = outer.left_edge
|
|
648
|
+
outer_right: float = outer.right_x
|
|
649
|
+
|
|
650
|
+
for opening in openings:
|
|
651
|
+
if opening.right_x <= cursor_x + eps:
|
|
652
|
+
continue
|
|
653
|
+
if opening.left_x >= outer_right - eps:
|
|
654
|
+
break
|
|
655
|
+
|
|
656
|
+
if opening.left_x > cursor_x + eps:
|
|
657
|
+
segment_right: float = min(opening.left_x, outer_right)
|
|
658
|
+
segment_right_edge: _Edge = (
|
|
659
|
+
opening.left_edge
|
|
660
|
+
if opening.left_x < outer_right - eps
|
|
661
|
+
else outer.right_edge
|
|
662
|
+
)
|
|
663
|
+
if segment_right - cursor_x > eps:
|
|
664
|
+
result.append(
|
|
665
|
+
_BandInterval(
|
|
666
|
+
cursor_edge,
|
|
667
|
+
segment_right_edge,
|
|
668
|
+
cursor_x,
|
|
669
|
+
segment_right,
|
|
670
|
+
)
|
|
671
|
+
)
|
|
672
|
+
|
|
673
|
+
if opening.right_x >= outer_right - eps:
|
|
674
|
+
cursor_x = outer_right
|
|
675
|
+
cursor_edge = outer.right_edge
|
|
676
|
+
break
|
|
677
|
+
|
|
678
|
+
if opening.right_x > cursor_x + eps:
|
|
679
|
+
cursor_x = opening.right_x
|
|
680
|
+
cursor_edge = opening.right_edge
|
|
681
|
+
|
|
682
|
+
if outer_right - cursor_x > eps:
|
|
683
|
+
result.append(
|
|
684
|
+
_BandInterval(
|
|
685
|
+
cursor_edge,
|
|
686
|
+
outer.right_edge,
|
|
687
|
+
cursor_x,
|
|
688
|
+
outer_right,
|
|
689
|
+
)
|
|
690
|
+
)
|
|
691
|
+
|
|
692
|
+
return result
|
|
693
|
+
|
|
694
|
+
|
|
695
|
+
def _triangulate_cell(
|
|
696
|
+
pool: _VertexPool,
|
|
697
|
+
faces: list[tuple[int, int, int]],
|
|
698
|
+
y0: float,
|
|
699
|
+
y1: float,
|
|
700
|
+
left0: float,
|
|
701
|
+
right0: float,
|
|
702
|
+
left1: float,
|
|
703
|
+
right1: float,
|
|
704
|
+
events0: list[float],
|
|
705
|
+
events1: list[float],
|
|
706
|
+
eps: float,
|
|
707
|
+
area_eps: float,
|
|
708
|
+
) -> None:
|
|
709
|
+
"""一つの台形を、境界イベントを共有する三角形へ分割する。"""
|
|
710
|
+
width0: float = right0 - left0
|
|
711
|
+
width1: float = right1 - left1
|
|
712
|
+
|
|
713
|
+
if width0 < -eps or width1 < -eps:
|
|
714
|
+
raise _TriangulationError("水平帯の左右境界が逆転しました")
|
|
715
|
+
|
|
716
|
+
if abs(width0) <= eps:
|
|
717
|
+
left0 = right0 = (left0 + right0) / 2.0
|
|
718
|
+
width0 = 0.0
|
|
719
|
+
if abs(width1) <= eps:
|
|
720
|
+
left1 = right1 = (left1 + right1) / 2.0
|
|
721
|
+
width1 = 0.0
|
|
722
|
+
if width0 == 0.0 and width1 == 0.0:
|
|
723
|
+
return
|
|
724
|
+
|
|
725
|
+
bottom: list[Vector2d] = _chain(y0, left0, right0, events0, eps)
|
|
726
|
+
top: list[Vector2d] = _chain(y1, left1, right1, events1, eps)
|
|
727
|
+
|
|
728
|
+
if width0 == 0.0:
|
|
729
|
+
for index in range(len(top) - 1):
|
|
730
|
+
_add_triangle(
|
|
731
|
+
pool,
|
|
732
|
+
faces,
|
|
733
|
+
bottom[0],
|
|
734
|
+
top[index + 1],
|
|
735
|
+
top[index],
|
|
736
|
+
area_eps,
|
|
737
|
+
)
|
|
738
|
+
return
|
|
739
|
+
|
|
740
|
+
if width1 == 0.0:
|
|
741
|
+
for index in range(len(bottom) - 1):
|
|
742
|
+
_add_triangle(
|
|
743
|
+
pool,
|
|
744
|
+
faces,
|
|
745
|
+
top[0],
|
|
746
|
+
bottom[index],
|
|
747
|
+
bottom[index + 1],
|
|
748
|
+
area_eps,
|
|
749
|
+
)
|
|
750
|
+
return
|
|
751
|
+
|
|
752
|
+
# 凸セルを左上から右下へ向かう対角線で二つの扇形に分ける。
|
|
753
|
+
for index in range(len(bottom) - 1):
|
|
754
|
+
_add_triangle(
|
|
755
|
+
pool,
|
|
756
|
+
faces,
|
|
757
|
+
top[0],
|
|
758
|
+
bottom[index],
|
|
759
|
+
bottom[index + 1],
|
|
760
|
+
area_eps,
|
|
761
|
+
)
|
|
762
|
+
for index in range(len(top) - 1):
|
|
763
|
+
_add_triangle(
|
|
764
|
+
pool,
|
|
765
|
+
faces,
|
|
766
|
+
bottom[-1],
|
|
767
|
+
top[index + 1],
|
|
768
|
+
top[index],
|
|
769
|
+
area_eps,
|
|
770
|
+
)
|
|
771
|
+
|
|
772
|
+
|
|
773
|
+
def _chain(
|
|
774
|
+
y: float,
|
|
775
|
+
first: float,
|
|
776
|
+
last: float,
|
|
777
|
+
events: list[float],
|
|
778
|
+
eps: float,
|
|
779
|
+
) -> list[Vector2d]:
|
|
780
|
+
"""水平セル辺を、同じ高さにある境界イベント位置で分割する。"""
|
|
781
|
+
if last < first:
|
|
782
|
+
first, last = last, first
|
|
783
|
+
|
|
784
|
+
values: list[float] = [first, last]
|
|
785
|
+
values.extend(x for x in events if first + eps < x < last - eps)
|
|
786
|
+
return [Vector2d(x, y) for x in _unique(values, eps / 2.0)]
|
|
787
|
+
|
|
788
|
+
|
|
789
|
+
def _add_triangle(
|
|
790
|
+
pool: _VertexPool,
|
|
791
|
+
faces: list[tuple[int, int, int]],
|
|
792
|
+
a: Vector2d,
|
|
793
|
+
b: Vector2d,
|
|
794
|
+
c: Vector2d,
|
|
795
|
+
area_eps: float,
|
|
796
|
+
) -> None:
|
|
797
|
+
area2: float = _cross(a, b, c)
|
|
798
|
+
if abs(area2) <= 2.0 * area_eps:
|
|
799
|
+
return
|
|
800
|
+
if area2 < 0.0:
|
|
801
|
+
b, c = c, b
|
|
802
|
+
|
|
803
|
+
indices: tuple[int, int, int] = (pool.add(a), pool.add(b), pool.add(c))
|
|
804
|
+
if len(set(indices)) == 3:
|
|
805
|
+
faces.append(indices)
|
|
806
|
+
|
|
807
|
+
|
|
808
|
+
def _extract_boundary_loops(
|
|
809
|
+
points: list[Vector2d],
|
|
810
|
+
faces: list[tuple[int, int, int]],
|
|
811
|
+
eps: float,
|
|
812
|
+
) -> list[list[Vector2d]]:
|
|
813
|
+
"""三角形の境界半辺をたどり、外周と内周のループを復元する。
|
|
814
|
+
|
|
815
|
+
頂点だけで接触する複数開口では、一つの頂点に複数の境界が集まる。
|
|
816
|
+
単純な頂点隣接表では経路を一意に決められないため、三角形の隣接面を回りながら次の境界半辺を探す。
|
|
817
|
+
これにより接触点でも各ループを分離する。
|
|
818
|
+
"""
|
|
819
|
+
owner: dict[tuple[int, int], tuple[int, int]] = {}
|
|
820
|
+
face_edges: list[tuple[tuple[int, int], tuple[int, int], tuple[int, int]]] = []
|
|
821
|
+
undirected_count: dict[tuple[int, ...], int] = {}
|
|
822
|
+
|
|
823
|
+
for face_index, (a, b, c) in enumerate(faces):
|
|
824
|
+
edges: tuple[tuple[int, int], tuple[int, int], tuple[int, int]] = (
|
|
825
|
+
(a, b),
|
|
826
|
+
(b, c),
|
|
827
|
+
(c, a),
|
|
828
|
+
)
|
|
829
|
+
face_edges.append(edges)
|
|
830
|
+
|
|
831
|
+
for local_index, edge in enumerate(edges):
|
|
832
|
+
if edge in owner:
|
|
833
|
+
raise _TriangulationError(
|
|
834
|
+
"同じ向きの半辺を複数の三角形が共有しています"
|
|
835
|
+
)
|
|
836
|
+
owner[edge] = (face_index, local_index)
|
|
837
|
+
key: tuple[int, ...] = tuple(sorted(edge))
|
|
838
|
+
undirected_count[key] = undirected_count.get(key, 0) + 1
|
|
839
|
+
|
|
840
|
+
if any(count > 2 for count in undirected_count.values()):
|
|
841
|
+
raise _TriangulationError("三角形メッシュに非多様体辺があります")
|
|
842
|
+
|
|
843
|
+
boundary_edges: set[tuple[int, int]] = {
|
|
844
|
+
edge for edge in owner if (edge[1], edge[0]) not in owner
|
|
845
|
+
}
|
|
846
|
+
if not boundary_edges:
|
|
847
|
+
raise _TriangulationError("メッシュ境界を復元できませんでした")
|
|
848
|
+
|
|
849
|
+
next_boundary: dict[tuple[int, int], tuple[int, int]] = {}
|
|
850
|
+
maximum_steps: int = len(owner) + 1
|
|
851
|
+
|
|
852
|
+
for start_edge in boundary_edges:
|
|
853
|
+
current: tuple[int, int] = start_edge
|
|
854
|
+
for _ in range(maximum_steps):
|
|
855
|
+
face_index, local_index = owner[current]
|
|
856
|
+
candidate: tuple[int, int] = face_edges[face_index][(local_index + 1) % 3]
|
|
857
|
+
reverse: tuple[int, int] = (candidate[1], candidate[0])
|
|
858
|
+
|
|
859
|
+
if reverse not in owner:
|
|
860
|
+
if candidate not in boundary_edges:
|
|
861
|
+
raise _TriangulationError("境界半辺の隣接関係が矛盾しています")
|
|
862
|
+
next_boundary[start_edge] = candidate
|
|
863
|
+
break
|
|
864
|
+
|
|
865
|
+
# 内部辺を反対側の三角形へ渡り、同じ頂点の周囲を回る。
|
|
866
|
+
current = reverse
|
|
867
|
+
else:
|
|
868
|
+
raise _TriangulationError("境界半辺の次辺探索が収束しませんでした")
|
|
869
|
+
|
|
870
|
+
loops: list[list[Vector2d]] = []
|
|
871
|
+
used: set[tuple[int, int]] = set()
|
|
872
|
+
|
|
873
|
+
for start_edge in sorted(boundary_edges):
|
|
874
|
+
if start_edge in used:
|
|
875
|
+
continue
|
|
876
|
+
|
|
877
|
+
loop_indices: list[int] = []
|
|
878
|
+
current = start_edge
|
|
879
|
+
|
|
880
|
+
for _ in range(len(boundary_edges) + 1):
|
|
881
|
+
if current in used:
|
|
882
|
+
if current != start_edge:
|
|
883
|
+
raise _TriangulationError(
|
|
884
|
+
"境界ループが別のループへ途中接続しています"
|
|
885
|
+
)
|
|
886
|
+
break
|
|
887
|
+
|
|
888
|
+
used.add(current)
|
|
889
|
+
loop_indices.append(current[0])
|
|
890
|
+
current = next_boundary[current]
|
|
891
|
+
|
|
892
|
+
if current == start_edge:
|
|
893
|
+
break
|
|
894
|
+
else:
|
|
895
|
+
raise _TriangulationError("境界ループの追跡が収束しませんでした")
|
|
896
|
+
|
|
897
|
+
if current != start_edge:
|
|
898
|
+
raise _TriangulationError("閉じていないメッシュ境界があります")
|
|
899
|
+
|
|
900
|
+
# 点接触する複数境界は、一つの閉路内で同じ頂点を複数回通る場合がある。
|
|
901
|
+
# その閉路を共有頂点ごとの単純ループへ分解する。
|
|
902
|
+
for simple_indices in _split_repeated_vertex_walk(loop_indices):
|
|
903
|
+
loop = _simplify_loop(
|
|
904
|
+
[points[index] for index in simple_indices],
|
|
905
|
+
eps * 4.0,
|
|
906
|
+
)
|
|
907
|
+
if len(loop) < 3:
|
|
908
|
+
raise _TriangulationError("境界ループの頂点が3点未満です")
|
|
909
|
+
loops.append(loop)
|
|
910
|
+
|
|
911
|
+
if used != boundary_edges:
|
|
912
|
+
raise _TriangulationError("一部の境界半辺をループへ復元できませんでした")
|
|
913
|
+
return loops
|
|
914
|
+
|
|
915
|
+
|
|
916
|
+
def _split_repeated_vertex_walk(
|
|
917
|
+
walk: list[int],
|
|
918
|
+
) -> list[list[int]]:
|
|
919
|
+
"""同じ頂点を複数回通る閉路を、単純な閉路へ再帰的に分解する。
|
|
920
|
+
|
|
921
|
+
開口同士が一点で接触する場合、境界半辺の追跡結果が8の字状の一つの閉路になることがある。
|
|
922
|
+
共有頂点の二つの出現位置で閉路を切り分けると、元の二つの単純な境界ループを復元できる。
|
|
923
|
+
"""
|
|
924
|
+
pending: list[list[int]] = [walk]
|
|
925
|
+
result: list[list[int]] = []
|
|
926
|
+
|
|
927
|
+
while pending:
|
|
928
|
+
current = pending.pop()
|
|
929
|
+
first_position: dict[int, int] = {}
|
|
930
|
+
split_pair: tuple[int, int] | None = None
|
|
931
|
+
|
|
932
|
+
for index, vertex in enumerate(current):
|
|
933
|
+
previous = first_position.get(vertex)
|
|
934
|
+
if previous is not None:
|
|
935
|
+
split_pair = (previous, index)
|
|
936
|
+
break
|
|
937
|
+
first_position[vertex] = index
|
|
938
|
+
|
|
939
|
+
if split_pair is None:
|
|
940
|
+
result.append(current)
|
|
941
|
+
continue
|
|
942
|
+
|
|
943
|
+
first, second = split_pair
|
|
944
|
+
first_loop = current[first:second]
|
|
945
|
+
second_loop = current[:first] + current[second:]
|
|
946
|
+
|
|
947
|
+
if len(first_loop) < 3 or len(second_loop) < 3:
|
|
948
|
+
raise _TriangulationError("共有頂点で分解した境界ループの頂点が3点未満です")
|
|
949
|
+
|
|
950
|
+
pending.append(second_loop)
|
|
951
|
+
pending.append(first_loop)
|
|
952
|
+
|
|
953
|
+
return result
|
|
954
|
+
|
|
955
|
+
|
|
956
|
+
def _classify_boundary_loops(
|
|
957
|
+
loops: list[list[Vector2d]],
|
|
958
|
+
eps: float,
|
|
959
|
+
) -> tuple[list[list[Vector2d]], list[list[Vector2d]]]:
|
|
960
|
+
"""符号付き面積で外周と内周を分類し、向きと順序を正規化する。
|
|
961
|
+
|
|
962
|
+
三角形面は反時計回りで生成されるため、材料領域を左側に見てたどる
|
|
963
|
+
境界の外周は反時計回り、内周は時計回りになる。
|
|
964
|
+
開口が外形を横断して材料領域が分断された場合は、複数の外周ループをそのまま返す。
|
|
965
|
+
"""
|
|
966
|
+
area_tolerance: float = max(eps * eps * 4.0, 1.0e-16)
|
|
967
|
+
outer_loops: list[list[Vector2d]] = []
|
|
968
|
+
hole_loops: list[list[Vector2d]] = []
|
|
969
|
+
|
|
970
|
+
for loop in loops:
|
|
971
|
+
area: float = _signed_area(loop)
|
|
972
|
+
if area > area_tolerance:
|
|
973
|
+
outer_loops.append(_rotate_loop_to_stable_start(loop))
|
|
974
|
+
elif area < -area_tolerance:
|
|
975
|
+
hole_loops.append(_rotate_loop_to_stable_start(loop))
|
|
976
|
+
else:
|
|
977
|
+
raise _TriangulationError("面積が小さすぎる境界ループがあります")
|
|
978
|
+
|
|
979
|
+
if not outer_loops:
|
|
980
|
+
raise _TriangulationError("処理後の外形ループを取得できませんでした")
|
|
981
|
+
|
|
982
|
+
outer_loops.sort(key=_loop_sort_key)
|
|
983
|
+
hole_loops.sort(key=_loop_sort_key)
|
|
984
|
+
return outer_loops, hole_loops
|
|
985
|
+
|
|
986
|
+
|
|
987
|
+
def _simplify_loop(loop: list[Vector2d], eps: float) -> list[Vector2d]:
|
|
988
|
+
"""境界ループから連続重複点と一直線上の中間点を除去する。"""
|
|
989
|
+
if not loop:
|
|
990
|
+
return []
|
|
991
|
+
|
|
992
|
+
limit: float = eps * eps
|
|
993
|
+
cleaned: list[Vector2d] = []
|
|
994
|
+
for point in loop:
|
|
995
|
+
if not cleaned or _distance_2(cleaned[-1], point) > limit:
|
|
996
|
+
cleaned.append(point)
|
|
997
|
+
|
|
998
|
+
if len(cleaned) > 1 and _distance_2(cleaned[0], cleaned[-1]) <= limit:
|
|
999
|
+
cleaned.pop()
|
|
1000
|
+
|
|
1001
|
+
for _ in range(len(cleaned) + 2):
|
|
1002
|
+
if len(cleaned) < 3:
|
|
1003
|
+
break
|
|
1004
|
+
|
|
1005
|
+
result: list[Vector2d] = []
|
|
1006
|
+
changed: bool = False
|
|
1007
|
+
count: int = len(cleaned)
|
|
1008
|
+
|
|
1009
|
+
for index, point in enumerate(cleaned):
|
|
1010
|
+
previous: Vector2d = cleaned[(index - 1) % count]
|
|
1011
|
+
following: Vector2d = cleaned[(index + 1) % count]
|
|
1012
|
+
local_scale: float = (
|
|
1013
|
+
previous.distance_to(point) + point.distance_to(following) + 1.0
|
|
1014
|
+
)
|
|
1015
|
+
if abs(
|
|
1016
|
+
_cross(previous, point, following)
|
|
1017
|
+
) <= eps * local_scale and _on_segment(point, previous, following, eps):
|
|
1018
|
+
changed = True
|
|
1019
|
+
continue
|
|
1020
|
+
result.append(point)
|
|
1021
|
+
|
|
1022
|
+
cleaned = result
|
|
1023
|
+
if not changed:
|
|
1024
|
+
break
|
|
1025
|
+
|
|
1026
|
+
return cleaned
|
|
1027
|
+
|
|
1028
|
+
|
|
1029
|
+
def _rings_equivalent(
|
|
1030
|
+
first: list[Vector2d],
|
|
1031
|
+
second: list[Vector2d],
|
|
1032
|
+
eps: float,
|
|
1033
|
+
) -> bool:
|
|
1034
|
+
if len(first) != len(second):
|
|
1035
|
+
return False
|
|
1036
|
+
if not first:
|
|
1037
|
+
return True
|
|
1038
|
+
|
|
1039
|
+
limit: float = eps * eps
|
|
1040
|
+
candidates: list[int] = [
|
|
1041
|
+
index
|
|
1042
|
+
for index, point in enumerate(second)
|
|
1043
|
+
if _distance_2(first[0], point) <= limit
|
|
1044
|
+
]
|
|
1045
|
+
|
|
1046
|
+
for start in candidates:
|
|
1047
|
+
forward: bool = all(
|
|
1048
|
+
_distance_2(first[index], second[(start + index) % len(second)]) <= limit
|
|
1049
|
+
for index in range(len(first))
|
|
1050
|
+
)
|
|
1051
|
+
if forward:
|
|
1052
|
+
return True
|
|
1053
|
+
|
|
1054
|
+
backward: bool = all(
|
|
1055
|
+
_distance_2(first[index], second[(start - index) % len(second)]) <= limit
|
|
1056
|
+
for index in range(len(first))
|
|
1057
|
+
)
|
|
1058
|
+
if backward:
|
|
1059
|
+
return True
|
|
1060
|
+
|
|
1061
|
+
return False
|
|
1062
|
+
|
|
1063
|
+
|
|
1064
|
+
def _ring_sets_equivalent(
|
|
1065
|
+
first: list[list[Vector2d]],
|
|
1066
|
+
second: list[list[Vector2d]],
|
|
1067
|
+
eps: float,
|
|
1068
|
+
) -> bool:
|
|
1069
|
+
if len(first) != len(second):
|
|
1070
|
+
return False
|
|
1071
|
+
|
|
1072
|
+
unmatched: list[int] = list(range(len(second)))
|
|
1073
|
+
for first_ring in first:
|
|
1074
|
+
matched_index: int | None = None
|
|
1075
|
+
for second_index in unmatched:
|
|
1076
|
+
if _rings_equivalent(first_ring, second[second_index], eps):
|
|
1077
|
+
matched_index = second_index
|
|
1078
|
+
break
|
|
1079
|
+
if matched_index is None:
|
|
1080
|
+
return False
|
|
1081
|
+
unmatched.remove(matched_index)
|
|
1082
|
+
|
|
1083
|
+
return not unmatched
|
|
1084
|
+
|
|
1085
|
+
|
|
1086
|
+
def _rotate_loop_to_stable_start(loop: list[Vector2d]) -> list[Vector2d]:
|
|
1087
|
+
"""最小のY、次に最小のXとなる頂点をリング先頭へ移動する。"""
|
|
1088
|
+
if not loop:
|
|
1089
|
+
return []
|
|
1090
|
+
start: int = min(
|
|
1091
|
+
range(len(loop)),
|
|
1092
|
+
key=lambda index: (loop[index].y, loop[index].x),
|
|
1093
|
+
)
|
|
1094
|
+
return loop[start:] + loop[:start]
|
|
1095
|
+
|
|
1096
|
+
|
|
1097
|
+
def _loop_sort_key(loop: list[Vector2d]) -> tuple[float, float, float, int]:
|
|
1098
|
+
"""開口リングを決定的な順序へ並べるためのキー"""
|
|
1099
|
+
min_y = min(point.y for point in loop)
|
|
1100
|
+
min_x = min(point.x for point in loop)
|
|
1101
|
+
return (min_y, min_x, -abs(_signed_area(loop)), len(loop))
|
|
1102
|
+
|
|
1103
|
+
|
|
1104
|
+
def _segment_intersection_points(
|
|
1105
|
+
a: Vector2d,
|
|
1106
|
+
b: Vector2d,
|
|
1107
|
+
c: Vector2d,
|
|
1108
|
+
d: Vector2d,
|
|
1109
|
+
eps: float,
|
|
1110
|
+
) -> list[Vector2d]:
|
|
1111
|
+
"""2線分の交点。共線重複時は重複区間の端点。"""
|
|
1112
|
+
if not _segments_intersect(a, b, c, d, eps):
|
|
1113
|
+
return []
|
|
1114
|
+
|
|
1115
|
+
rx: float = b.x - a.x
|
|
1116
|
+
ry: float = b.y - a.y
|
|
1117
|
+
sx: float = d.x - c.x
|
|
1118
|
+
sy: float = d.y - c.y
|
|
1119
|
+
denominator: float = rx * sy - ry * sx
|
|
1120
|
+
scale: float = math.hypot(rx, ry) * math.hypot(sx, sy) + 1.0
|
|
1121
|
+
|
|
1122
|
+
if abs(denominator) > eps * scale:
|
|
1123
|
+
qpx: float = c.x - a.x
|
|
1124
|
+
qpy: float = c.y - a.y
|
|
1125
|
+
t: float = (qpx * sy - qpy * sx) / denominator
|
|
1126
|
+
u: float = (qpx * ry - qpy * rx) / denominator
|
|
1127
|
+
|
|
1128
|
+
parameter_tolerance: float = eps / max(
|
|
1129
|
+
math.hypot(rx, ry),
|
|
1130
|
+
math.hypot(sx, sy),
|
|
1131
|
+
eps,
|
|
1132
|
+
)
|
|
1133
|
+
if (
|
|
1134
|
+
-parameter_tolerance <= t <= 1.0 + parameter_tolerance
|
|
1135
|
+
and -parameter_tolerance <= u <= 1.0 + parameter_tolerance
|
|
1136
|
+
):
|
|
1137
|
+
t = min(1.0, max(0.0, t))
|
|
1138
|
+
u = min(1.0, max(0.0, u))
|
|
1139
|
+
first_point: Vector2d = Vector2d(a.x + t * rx, a.y + t * ry)
|
|
1140
|
+
second_point: Vector2d = Vector2d(c.x + u * sx, c.y + u * sy)
|
|
1141
|
+
return [
|
|
1142
|
+
Vector2d(
|
|
1143
|
+
(first_point.x + second_point.x) / 2.0,
|
|
1144
|
+
(first_point.y + second_point.y) / 2.0,
|
|
1145
|
+
)
|
|
1146
|
+
]
|
|
1147
|
+
return []
|
|
1148
|
+
|
|
1149
|
+
# 平行線分の場合は、両方の線分上にある端点を集める。
|
|
1150
|
+
candidates: list[Vector2d] = [
|
|
1151
|
+
point
|
|
1152
|
+
for point in (a, b, c, d)
|
|
1153
|
+
if _on_segment(point, a, b, eps) and _on_segment(point, c, d, eps)
|
|
1154
|
+
]
|
|
1155
|
+
return _unique_points(candidates, eps)
|
|
1156
|
+
|
|
1157
|
+
|
|
1158
|
+
def _unique_points(points: list[Vector2d], eps: float) -> list[Vector2d]:
|
|
1159
|
+
"""許容誤差内で一致する点を一つへまとめる。"""
|
|
1160
|
+
result: list[Vector2d] = []
|
|
1161
|
+
limit: float = eps * eps
|
|
1162
|
+
|
|
1163
|
+
for point in sorted(points, key=lambda item: (item.y, item.x)):
|
|
1164
|
+
if any(_distance_2(point, existing) <= limit for existing in result):
|
|
1165
|
+
continue
|
|
1166
|
+
result.append(point)
|
|
1167
|
+
return result
|
|
1168
|
+
|
|
1169
|
+
|
|
1170
|
+
def _nearest_value(values: list[float], target: float) -> float:
|
|
1171
|
+
return min(values, key=lambda value: abs(value - target))
|
|
1172
|
+
|
|
1173
|
+
|
|
1174
|
+
def _unique(values: list[float], eps: float) -> list[float]:
|
|
1175
|
+
"""近接する数値を平均値へまとめ、昇順で返す。"""
|
|
1176
|
+
result: list[float] = []
|
|
1177
|
+
for value in sorted(values):
|
|
1178
|
+
if not result or value - result[-1] > eps:
|
|
1179
|
+
result.append(value)
|
|
1180
|
+
else:
|
|
1181
|
+
result[-1] = (result[-1] + value) / 2.0
|
|
1182
|
+
return result
|
|
1183
|
+
|
|
1184
|
+
|
|
1185
|
+
def _edge_pairs(ring: list[Vector2d]) -> list[tuple[Vector2d, Vector2d]]:
|
|
1186
|
+
return list(zip(ring, ring[1:] + ring[:1]))
|
|
1187
|
+
|
|
1188
|
+
|
|
1189
|
+
def _distance_2(a: Vector2d, b: Vector2d) -> float:
|
|
1190
|
+
return (a.x - b.x) ** 2 + (a.y - b.y) ** 2
|
|
1191
|
+
|
|
1192
|
+
|
|
1193
|
+
def _cross(a: Vector2d, b: Vector2d, c: Vector2d) -> float:
|
|
1194
|
+
return (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x)
|
|
1195
|
+
|
|
1196
|
+
|
|
1197
|
+
def _signed_area(ring: list[Vector2d]) -> float:
|
|
1198
|
+
return math.fsum(a.x * b.y - b.x * a.y for a, b in _edge_pairs(ring)) / 2.0
|
|
1199
|
+
|
|
1200
|
+
|
|
1201
|
+
def _perimeter(ring: list[Vector2d]) -> float:
|
|
1202
|
+
return math.fsum(a.distance_to(b) for a, b in _edge_pairs(ring))
|
|
1203
|
+
|
|
1204
|
+
|
|
1205
|
+
def _point_segment_distance_2(
|
|
1206
|
+
point: Vector2d,
|
|
1207
|
+
a: Vector2d,
|
|
1208
|
+
b: Vector2d,
|
|
1209
|
+
) -> float:
|
|
1210
|
+
dx: float = b.x - a.x
|
|
1211
|
+
dy: float = b.y - a.y
|
|
1212
|
+
length2: float = dx * dx + dy * dy
|
|
1213
|
+
|
|
1214
|
+
if length2 == 0.0:
|
|
1215
|
+
return _distance_2(point, a)
|
|
1216
|
+
|
|
1217
|
+
t: float = ((point.x - a.x) * dx + (point.y - a.y) * dy) / length2
|
|
1218
|
+
t = min(1.0, max(0.0, t))
|
|
1219
|
+
nearest: Vector2d = Vector2d(a.x + t * dx, a.y + t * dy)
|
|
1220
|
+
return _distance_2(point, nearest)
|
|
1221
|
+
|
|
1222
|
+
|
|
1223
|
+
def _on_segment(
|
|
1224
|
+
point: Vector2d,
|
|
1225
|
+
a: Vector2d,
|
|
1226
|
+
b: Vector2d,
|
|
1227
|
+
eps: float,
|
|
1228
|
+
) -> bool:
|
|
1229
|
+
"""点が許容誤差内で線分上にあるかの判定"""
|
|
1230
|
+
return (
|
|
1231
|
+
min(a.x, b.x) - eps <= point.x <= max(a.x, b.x) + eps
|
|
1232
|
+
and min(a.y, b.y) - eps <= point.y <= max(a.y, b.y) + eps
|
|
1233
|
+
and _point_segment_distance_2(point, a, b) <= eps * eps
|
|
1234
|
+
)
|
|
1235
|
+
|
|
1236
|
+
|
|
1237
|
+
def _orientation(
|
|
1238
|
+
a: Vector2d,
|
|
1239
|
+
b: Vector2d,
|
|
1240
|
+
c: Vector2d,
|
|
1241
|
+
eps: float,
|
|
1242
|
+
) -> int:
|
|
1243
|
+
"""3点の向きを反時計回り1、時計回り-1、共線0で返す。"""
|
|
1244
|
+
value: float = _cross(a, b, c)
|
|
1245
|
+
tolerance: float = eps * (a.distance_to(b) + a.distance_to(c) + 1.0)
|
|
1246
|
+
if value > tolerance:
|
|
1247
|
+
return 1
|
|
1248
|
+
if value < -tolerance:
|
|
1249
|
+
return -1
|
|
1250
|
+
return 0
|
|
1251
|
+
|
|
1252
|
+
|
|
1253
|
+
def _segments_intersect(
|
|
1254
|
+
a: Vector2d,
|
|
1255
|
+
b: Vector2d,
|
|
1256
|
+
c: Vector2d,
|
|
1257
|
+
d: Vector2d,
|
|
1258
|
+
eps: float,
|
|
1259
|
+
) -> bool:
|
|
1260
|
+
"""2線分が許容誤差内で交差または接触するかの判定"""
|
|
1261
|
+
if (
|
|
1262
|
+
max(a.x, b.x) + eps < min(c.x, d.x)
|
|
1263
|
+
or max(c.x, d.x) + eps < min(a.x, b.x)
|
|
1264
|
+
or max(a.y, b.y) + eps < min(c.y, d.y)
|
|
1265
|
+
or max(c.y, d.y) + eps < min(a.y, b.y)
|
|
1266
|
+
):
|
|
1267
|
+
return False
|
|
1268
|
+
|
|
1269
|
+
o1: int = _orientation(a, b, c, eps)
|
|
1270
|
+
o2: int = _orientation(a, b, d, eps)
|
|
1271
|
+
o3: int = _orientation(c, d, a, eps)
|
|
1272
|
+
o4: int = _orientation(c, d, b, eps)
|
|
1273
|
+
|
|
1274
|
+
if o1 * o2 < 0 and o3 * o4 < 0:
|
|
1275
|
+
return True
|
|
1276
|
+
|
|
1277
|
+
return (
|
|
1278
|
+
(o1 == 0 and _on_segment(c, a, b, eps))
|
|
1279
|
+
or (o2 == 0 and _on_segment(d, a, b, eps))
|
|
1280
|
+
or (o3 == 0 and _on_segment(a, c, d, eps))
|
|
1281
|
+
or (o4 == 0 and _on_segment(b, c, d, eps))
|
|
1282
|
+
)
|
|
1283
|
+
|
|
1284
|
+
|
|
1285
|
+
def _segments_too_close(
|
|
1286
|
+
a: Vector2d,
|
|
1287
|
+
b: Vector2d,
|
|
1288
|
+
c: Vector2d,
|
|
1289
|
+
d: Vector2d,
|
|
1290
|
+
eps: float,
|
|
1291
|
+
) -> bool:
|
|
1292
|
+
"""2線分が交差するか、許容距離以内まで近接するかの判定"""
|
|
1293
|
+
if _segments_intersect(a, b, c, d, eps):
|
|
1294
|
+
return True
|
|
1295
|
+
|
|
1296
|
+
limit: float = eps * eps
|
|
1297
|
+
return (
|
|
1298
|
+
min(
|
|
1299
|
+
_point_segment_distance_2(a, c, d),
|
|
1300
|
+
_point_segment_distance_2(b, c, d),
|
|
1301
|
+
_point_segment_distance_2(c, a, b),
|
|
1302
|
+
_point_segment_distance_2(d, a, b),
|
|
1303
|
+
)
|
|
1304
|
+
<= limit
|
|
1305
|
+
)
|