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.
Files changed (125) hide show
  1. stbkit/__main__.py +10 -0
  2. stbkit/api/__init__.py +48 -0
  3. stbkit/api/experimental/__init__.py +19 -0
  4. stbkit/api/experimental/repository/__init__.py +30 -0
  5. stbkit/api/py.typed +0 -0
  6. stbkit/api/stb_latest.py +7 -0
  7. stbkit/api/stb_v2_0_2.py +11 -0
  8. stbkit/api/stb_v2_1_1.py +9 -0
  9. stbkit/cli/__init__.py +17 -0
  10. stbkit/cli/_internal/cli_main.py +114 -0
  11. stbkit/cli/_internal/common.py +126 -0
  12. stbkit/cli/_internal/constants_exit_code.py +59 -0
  13. stbkit/cli/_internal/converter.py +693 -0
  14. stbkit/cli/_internal/diff.py +213 -0
  15. stbkit/cli/_internal/entry_points.py +25 -0
  16. stbkit/cli/_internal/global_parent.py +70 -0
  17. stbkit/cli/_internal/show_versions.py +36 -0
  18. stbkit/cli/_internal/validate.py +310 -0
  19. stbkit/cli/commands/convert.py +16 -0
  20. stbkit/cli/commands/diff.py +16 -0
  21. stbkit/cli/commands/validate.py +16 -0
  22. stbkit/cli/py.typed +0 -0
  23. stbkit/tools/__init__.py +12 -0
  24. stbkit/tools/_internal/__init__.py +5 -0
  25. stbkit/tools/_internal/constants.py +21 -0
  26. stbkit/tools/_internal/data_model/building_geometry.py +1208 -0
  27. stbkit/tools/_internal/data_model/building_geometry_web.py +106 -0
  28. stbkit/tools/_internal/data_model/element_data.py +93 -0
  29. stbkit/tools/_internal/data_model/ifc_data/entity_data.py +67 -0
  30. stbkit/tools/_internal/data_model/ifc_data/entity_names.py +32 -0
  31. stbkit/tools/_internal/data_model/shape_data.py +1089 -0
  32. stbkit/tools/_internal/diff/__init__.py +5 -0
  33. stbkit/tools/_internal/diff/common.py +50 -0
  34. stbkit/tools/_internal/diff/diff_xml.py +796 -0
  35. stbkit/tools/_internal/matrix.py +51 -0
  36. stbkit/tools/_internal/optional_dependencies/__init__.py +5 -0
  37. stbkit/tools/_internal/optional_dependencies/ifc_common.py +28 -0
  38. stbkit/tools/_internal/optional_dependencies/ifc_exporter/ifc_entity_exporter.py +473 -0
  39. stbkit/tools/_internal/optional_dependencies/ifc_exporter/ifc_profile_exporter.py +267 -0
  40. stbkit/tools/_internal/optional_dependencies/ifc_exporter/ifc_representation_exporter.py +178 -0
  41. stbkit/tools/_internal/optional_dependencies/ifc_pipeline.py +133 -0
  42. stbkit/tools/_internal/optional_dependencies/ifc_to_building_geometry.py +64 -0
  43. stbkit/tools/_internal/stb_io.py +116 -0
  44. stbkit/tools/_internal/stb_repair/_common.py +24 -0
  45. stbkit/tools/_internal/stb_repair/_stb_repair.py +197 -0
  46. stbkit/tools/_internal/stb_repair/_stb_v2_0_2_repair.py +53 -0
  47. stbkit/tools/_internal/stb_repair/_stb_v2_1_0_repair.py +109 -0
  48. stbkit/tools/_internal/stb_repair/_stb_v2_1_1_repair.py +109 -0
  49. stbkit/tools/_internal/studio_report.py +49 -0
  50. stbkit/tools/_internal/utils/atomic_writer.py +57 -0
  51. stbkit/tools/_internal/utils/float_rounding.py +49 -0
  52. stbkit/tools/_internal/utils/girder_utils.py +91 -0
  53. stbkit/tools/_internal/utils/guid_utils.py +30 -0
  54. stbkit/tools/_internal/utils/mesh_utils.py +1305 -0
  55. stbkit/tools/_internal/utils/node_utils.py +50 -0
  56. stbkit/tools/_internal/utils/unit_utils.py +23 -0
  57. stbkit/tools/_internal/vectors.py +222 -0
  58. stbkit/tools/converters/__init__.py +39 -0
  59. stbkit/tools/converters/_internal/__init__.py +5 -0
  60. stbkit/tools/converters/_internal/building_geometry_to_ply_text.py +32 -0
  61. stbkit/tools/converters/_internal/coord_converter.py +151 -0
  62. stbkit/tools/converters/_internal/element_data_to_building_geometry.py +549 -0
  63. stbkit/tools/converters/_internal/ifc_to_ply_text.py +21 -0
  64. stbkit/tools/converters/_internal/name_converter.py +23 -0
  65. stbkit/tools/converters/_internal/pipeline/__init__.py +5 -0
  66. stbkit/tools/converters/_internal/pipeline/data.py +188 -0
  67. stbkit/tools/converters/_internal/pipeline/execution.py +569 -0
  68. stbkit/tools/converters/_internal/pipeline/ply_pipeline.py +52 -0
  69. stbkit/tools/converters/_internal/pipeline/stb_pipeline.py +226 -0
  70. stbkit/tools/converters/_internal/pipeline/stb_pipeline_latest.py +39 -0
  71. stbkit/tools/converters/_internal/pipeline/step.py +139 -0
  72. stbkit/tools/converters/_internal/stb_latest_to_element_data/__init__.py +17 -0
  73. stbkit/tools/converters/_internal/stb_latest_to_element_data/_stb_latest_to_element_data.py +186 -0
  74. stbkit/tools/converters/_internal/stb_latest_to_element_data/member/member_footing.py +49 -0
  75. stbkit/tools/converters/_internal/stb_latest_to_element_data/member/member_line.py +533 -0
  76. stbkit/tools/converters/_internal/stb_latest_to_element_data/member/member_pile.py +100 -0
  77. stbkit/tools/converters/_internal/stb_latest_to_element_data/member/member_plane.py +103 -0
  78. stbkit/tools/converters/_internal/stb_latest_to_element_data/open.py +70 -0
  79. stbkit/tools/converters/_internal/stb_latest_to_element_data/section/sec.py +214 -0
  80. stbkit/tools/converters/_internal/stb_latest_to_element_data/section/sec_beam_rc.py +67 -0
  81. stbkit/tools/converters/_internal/stb_latest_to_element_data/section/sec_beam_s.py +97 -0
  82. stbkit/tools/converters/_internal/stb_latest_to_element_data/section/sec_beam_src.py +28 -0
  83. stbkit/tools/converters/_internal/stb_latest_to_element_data/section/sec_brace_s.py +49 -0
  84. stbkit/tools/converters/_internal/stb_latest_to_element_data/section/sec_column_cft.py +217 -0
  85. stbkit/tools/converters/_internal/stb_latest_to_element_data/section/sec_column_rc.py +41 -0
  86. stbkit/tools/converters/_internal/stb_latest_to_element_data/section/sec_column_s.py +127 -0
  87. stbkit/tools/converters/_internal/stb_latest_to_element_data/section/sec_column_src.py +173 -0
  88. stbkit/tools/converters/_internal/stb_latest_to_element_data/section/sec_foundation.py +225 -0
  89. stbkit/tools/converters/_internal/stb_latest_to_element_data/section/sec_parapet_rc.py +53 -0
  90. stbkit/tools/converters/_internal/stb_latest_to_element_data/section/sec_pile_rc.py +106 -0
  91. stbkit/tools/converters/_internal/stb_latest_to_element_data/section/sec_pile_s.py +100 -0
  92. stbkit/tools/converters/_internal/stb_latest_to_element_data/section/sec_steel.py +432 -0
  93. stbkit/tools/converters/_internal/stb_to_building_geometry.py +44 -0
  94. stbkit/tools/converters/_internal/stb_to_ifc_data/__init__.py +5 -0
  95. stbkit/tools/converters/_internal/stb_to_ifc_data/_stb_to_ifc_data.py +59 -0
  96. stbkit/tools/converters/_internal/stb_to_ifc_data/convert_option.py +22 -0
  97. stbkit/tools/converters/_internal/stb_to_ply_text.py +22 -0
  98. stbkit/tools/py.typed +0 -0
  99. stbkit/tools/upgrade/__init__.py +13 -0
  100. stbkit/tools/upgrade/_internal/__init__.py +5 -0
  101. stbkit/tools/upgrade/_internal/common.py +205 -0
  102. stbkit/tools/upgrade/_internal/upgrade.py +228 -0
  103. stbkit/tools/upgrade/_internal/v2_0_2_to_v2_1_0/__init__.py +60 -0
  104. stbkit/tools/upgrade/_internal/v2_0_2_to_v2_1_0/hook_funcs/hook_common.py +62 -0
  105. stbkit/tools/upgrade/_internal/v2_0_2_to_v2_1_0/hook_funcs/hook_joint_beam_shape_h.py +44 -0
  106. stbkit/tools/upgrade/_internal/v2_0_2_to_v2_1_0/hook_funcs/hook_sec_pile_rc.py +101 -0
  107. stbkit/tools/upgrade/_internal/v2_0_2_to_v2_1_0/hook_funcs/hook_sec_pile_s.py +78 -0
  108. stbkit/tools/upgrade/_internal/v2_0_2_to_v2_1_0/hook_funcs/hook_sec_slab_deck.py +56 -0
  109. stbkit/tools/upgrade/_internal/v2_0_2_to_v2_1_0/hook_funcs/hook_sec_slab_rc.py +128 -0
  110. stbkit/tools/upgrade/_internal/v2_0_2_to_v2_1_0/hook_funcs/post_girder.py +126 -0
  111. stbkit/tools/upgrade/_internal/v2_0_2_to_v2_1_0/hook_funcs/post_open.py +110 -0
  112. stbkit/tools/upgrade/_internal/v2_0_2_to_v2_1_0/hook_funcs/post_sec_beam_rc.py +227 -0
  113. stbkit/tools/upgrade/_internal/v2_0_2_to_v2_1_0/hook_funcs/post_sec_beam_s.py +1153 -0
  114. stbkit/tools/upgrade/_internal/v2_0_2_to_v2_1_0/hooks.py +22 -0
  115. stbkit/tools/upgrade/_internal/v2_0_2_to_v2_1_0/post.py +42 -0
  116. stbkit/tools/upgrade/_internal/v2_0_2_to_v2_1_0/tables.py +99 -0
  117. stbkit/tools/upgrade/_internal/v2_1_0_to_v2_1_1/__init__.py +58 -0
  118. stbkit/tools/upgrade/_internal/v2_1_0_to_v2_1_1/hooks.py +32 -0
  119. stbkit/tools/upgrade/_internal/v2_1_0_to_v2_1_1/tables.py +56 -0
  120. stbkit-0.1.0a9.dev1.dist-info/METADATA +196 -0
  121. stbkit-0.1.0a9.dev1.dist-info/RECORD +125 -0
  122. stbkit-0.1.0a9.dev1.dist-info/WHEEL +5 -0
  123. stbkit-0.1.0a9.dev1.dist-info/entry_points.txt +2 -0
  124. stbkit-0.1.0a9.dev1.dist-info/licenses/LICENSE +373 -0
  125. stbkit-0.1.0a9.dev1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1208 @@
1
+ # Copyright 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
+ from array import array
8
+ from collections.abc import Iterable, Sequence, Sized
9
+ from dataclasses import dataclass, field
10
+ from enum import IntEnum, IntFlag
11
+ from math import isfinite
12
+ from uuid import UUID
13
+
14
+ from ..vectors import Vector3d
15
+
16
+ UINT32_NONE = 0xFFFF_FFFF
17
+ """u32列で参照なし表す値。Rustのu32::MAXと一致させる。"""
18
+
19
+ _GUID_SIZE = 16
20
+
21
+ # 処理系依存対策
22
+ if array("f").itemsize != 4:
23
+ raise RuntimeError("array('f')のサイズが32bitになっていません")
24
+ if array("d").itemsize != 8:
25
+ raise RuntimeError("array('d')のサイズが64bitになっていません")
26
+ if array("I").itemsize != 4:
27
+ raise RuntimeError("array('I')のサイズが32bitになっていません")
28
+
29
+
30
+ def _new_f32_array() -> array[float]:
31
+ # GPU描画用
32
+ return array("f")
33
+
34
+
35
+ def _new_f64_array() -> array[float]:
36
+ return array("d")
37
+
38
+
39
+ def _new_u32_array() -> array[int]:
40
+ # id用
41
+ return array("I")
42
+
43
+
44
+ def _new_offsets_array() -> array[int]:
45
+ # データの範囲をoffsets[i]:offsets[i + 1] で求めるため、要素が0個でも先頭の0を作っておく。
46
+ return array("I", [0])
47
+
48
+
49
+ def _vector3(value: Sequence[float], *, name: str) -> Vector3d:
50
+ """任意コレクションのVector3dへの変換。
51
+ NaNや無限大のチェックも行うことで、rustへ渡る前に不整値を排除する。
52
+ """
53
+ if len(value) != 3:
54
+ raise ValueError(f"{name}のlenは3である必要があります")
55
+
56
+ vec: Vector3d = Vector3d(float(value[0]), float(value[1]), float(value[2]))
57
+ if not all(isfinite(component) for component in vec):
58
+ raise ValueError(f"{name}の全ての要素は有限値である必要があります")
59
+ return vec
60
+
61
+
62
+ class ElementKind(IntEnum):
63
+ UNKNOWN = 0
64
+ COLUMN = 1
65
+ POST = 2
66
+ GIRDER = 3
67
+ BEAM = 4
68
+ BRACE = 5
69
+ SLAB = 6
70
+ WALL = 7
71
+ ISOLATING_DEVICE = 8
72
+ DAMPING_DEVICE = 9
73
+ FRAME_DAMPING_DEVICE = 10
74
+ FOOTING = 11
75
+ STRIP_FOOTING = 12
76
+ PILE = 13
77
+ FOUNDATION_COLUMN = 14
78
+ PARAPET = 15
79
+ OPEN = 16
80
+ PENETRATION = 17
81
+ JOINT = 18
82
+ PANEL_ZONE = 19
83
+ CONNECTION = 20
84
+ OTHER = 21
85
+
86
+
87
+ class ElementFlags(IntFlag):
88
+ NONE = 0
89
+ HAS_MESH = 1 << 0
90
+ HAS_LINE_SHAPE = 1 << 1
91
+ HAS_PLANE_SHAPE = 1 << 2
92
+ HAS_NODE_RELATION = 1 << 3
93
+ HAS_STORY_RELATION = 1 << 4
94
+
95
+ # 合成構造断面(SRC・CFT)を、RCとSの2要素へ分けたときの部位。
96
+ # 単一断面の要素はどちらのビットも立たない。また、両方が同時に立つこともない。
97
+ IS_COMPOSITE_CONCRETE = 1 << 5
98
+ IS_COMPOSITE_STEEL = 1 << 6
99
+
100
+
101
+ class SurfaceLoopType(IntEnum):
102
+ # 1つの面につきOUTERは1つ、HOLEは0個以上
103
+ OUTER = 0
104
+ HOLE = 1
105
+
106
+
107
+ @dataclass(slots=True)
108
+ class OptionalUuidColumn:
109
+ """UUID | Noneを、固定長bytearrayと有効フラグで保持する"""
110
+
111
+ data: bytearray = field(default_factory=bytearray, repr=False)
112
+ """要素[i]のUUIDをdata[i * 16:(i + 1) * 16]に格納"""
113
+ valid: bytearray = field(default_factory=bytearray, repr=False)
114
+ """0:None, 1:UUIDあり"""
115
+
116
+ def __len__(self) -> int:
117
+ return len(self.valid)
118
+
119
+ def append(self, value: UUID | str | None) -> None:
120
+ if value is None:
121
+ # 固定長を維持するため、Noneでもメモリを確保する
122
+ self.data.extend(b"\x00" * _GUID_SIZE)
123
+ self.valid.append(0)
124
+ return
125
+ uuid_value: UUID = value if isinstance(value, UUID) else UUID(value)
126
+ self.data.extend(uuid_value.bytes)
127
+ self.valid.append(1)
128
+
129
+ def get(self, index: int) -> UUID | None:
130
+ if not 0 <= index < len(self):
131
+ raise IndexError(index)
132
+ if self.valid[index] == 0:
133
+ return None
134
+ start: int = index * _GUID_SIZE
135
+ return UUID(bytes=bytes(self.data[start : start + _GUID_SIZE]))
136
+
137
+ def validate(self, expected_count: int | None = None) -> None:
138
+ if len(self.data) != len(self.valid) * _GUID_SIZE:
139
+ raise ValueError(
140
+ "UUIDを格納推している配列サイズが不正です: "
141
+ f"{len(self.data)} != {len(self.valid) * _GUID_SIZE}"
142
+ )
143
+ if expected_count is not None and len(self) != expected_count:
144
+ raise ValueError(
145
+ f"UUIDの数が不正です: expect={expected_count}, actual={len(self)}"
146
+ )
147
+ if any(value not in (0, 1) for value in self.valid):
148
+ raise ValueError("UUID valid フラグは0か1でなければなりません")
149
+
150
+
151
+ @dataclass(frozen=True, slots=True)
152
+ class MeshRange:
153
+ # PolygonMesh.positions上のvertex開始位置と長さ
154
+ vertex_start: int
155
+ vertex_count: int
156
+
157
+ # PolygonMesh.face_offsets上のface開始位置と長さ
158
+ face_start: int
159
+ face_count: int
160
+
161
+
162
+ @dataclass(slots=True)
163
+ class PolygonMesh:
164
+ positions: array[float] = field(default_factory=_new_f32_array, repr=False)
165
+ """x, y, z, x, y, z, ...。BuildingGeometry.origin_xyzからの相対座標。"""
166
+
167
+ face_vertex_indices: array[int] = field(
168
+ default_factory=_new_u32_array,
169
+ repr=False,
170
+ )
171
+ """面の頂点。
172
+
173
+ 例えば面が(0, 1, 2, 3),(4, 5, 6)なら、 [0, 1, 2, 3, 4, 5, 6]のように格納する"""
174
+
175
+ face_offsets: array[int] = field(
176
+ default_factory=_new_offsets_array,
177
+ repr=False,
178
+ )
179
+ """面の頂点のオフセット開始位置。
180
+
181
+ 例えば面が(0, 1, 2, 3),(4, 5, 6)なら、 [0, 4, 7]のように格納する。
182
+ 面iはface_offsets[i]:face_offsets[i + 1]を参照する。"""
183
+
184
+ @property
185
+ def vertex_count(self) -> int:
186
+ return len(self.positions) // 3
187
+
188
+ @property
189
+ def face_count(self) -> int:
190
+ return len(self.face_offsets) - 1
191
+
192
+ def validate(self) -> None:
193
+ if len(self.positions) % 3 != 0:
194
+ raise ValueError("len(mesh.positions)が3の倍数ではありません")
195
+ if not self.face_offsets:
196
+ raise ValueError("mesh.face_offsetsが空です")
197
+ if self.face_offsets[0] != 0:
198
+ raise ValueError("mesh.face_offsetsの先頭は0でなければなりません")
199
+ if self.face_offsets[-1] != len(self.face_vertex_indices):
200
+ raise ValueError(
201
+ "最後のface offsetはface vertex indexの数と一致しなければなりません"
202
+ )
203
+
204
+ previous: int = 0
205
+ for face_index in range(self.face_count):
206
+ start: int = self.face_offsets[face_index]
207
+ end: int = self.face_offsets[face_index + 1]
208
+ if start < previous or end < start:
209
+ raise ValueError("mesh.face_offsetsは単調増加でなければなりません")
210
+ if end - start < 3:
211
+ raise ValueError(f"mesh face {face_index}は最低3つの頂点が必要です")
212
+ previous = end
213
+
214
+ vertex_count: int = self.vertex_count
215
+ for index in self.face_vertex_indices:
216
+ if index >= vertex_count:
217
+ raise ValueError(
218
+ f"mesh vertex index {index} は {vertex_count} 頂点の範囲外です"
219
+ )
220
+
221
+
222
+ @dataclass(frozen=True, slots=True)
223
+ class LineShape:
224
+ start: Vector3d
225
+ end: Vector3d
226
+
227
+ reference_y: Vector3d | None
228
+
229
+
230
+ @dataclass(slots=True)
231
+ class LineShapeTable:
232
+ start_end_xyz: array[float] = field(default_factory=_new_f64_array, repr=False)
233
+ """1要素につき start xyz + end xyz の6値。"""
234
+
235
+ reference_y_xyz: array[float] = field(default_factory=_new_f64_array, repr=False)
236
+ """1要素につき reference_y xyz の3値。"""
237
+ reference_y_valid: bytearray = field(default_factory=bytearray, repr=False)
238
+ """1要素につき1値。0:None, 1:設定済み。"""
239
+
240
+ def __len__(self) -> int:
241
+ return len(self.reference_y_valid)
242
+
243
+ def add(
244
+ self,
245
+ start: Sequence[float],
246
+ end: Sequence[float],
247
+ reference_y: Sequence[float] | None = None,
248
+ ) -> int:
249
+ start_value: Vector3d = _vector3(start, name="start")
250
+ end_value: Vector3d = _vector3(end, name="end")
251
+
252
+ index: int = len(self)
253
+ self.start_end_xyz.extend((*start_value, *end_value))
254
+
255
+ if reference_y is None:
256
+ # 固定長を維持するため、Noneでもメモリを確保する。
257
+ self.reference_y_xyz.extend((0.0, 0.0, 0.0))
258
+ self.reference_y_valid.append(0)
259
+ else:
260
+ reference_y_value: Vector3d = _vector3(reference_y, name="reference_y")
261
+ self.reference_y_xyz.extend(reference_y_value)
262
+ self.reference_y_valid.append(1)
263
+
264
+ return index
265
+
266
+ def get(self, index: int) -> LineShape:
267
+ if not 0 <= index < len(self):
268
+ raise IndexError(index)
269
+
270
+ start_offset: int = index * 6
271
+ reference_offset: int = index * 3
272
+ start: tuple[float, ...] = tuple(
273
+ self.start_end_xyz[start_offset : start_offset + 3]
274
+ )
275
+ end: tuple[float, ...] = tuple(
276
+ self.start_end_xyz[start_offset + 3 : start_offset + 6]
277
+ )
278
+ reference_y: tuple[float, ...] | None = None
279
+ if self.reference_y_valid[index]:
280
+ reference_y = tuple(
281
+ self.reference_y_xyz[reference_offset : reference_offset + 3]
282
+ )
283
+
284
+ return LineShape(
285
+ start=Vector3d(float(start[0]), float(start[1]), float(start[2])),
286
+ end=Vector3d(float(end[0]), float(end[1]), float(end[2])),
287
+ reference_y=(
288
+ None
289
+ if reference_y is None
290
+ else Vector3d(
291
+ float(reference_y[0]),
292
+ float(reference_y[1]),
293
+ float(reference_y[2]),
294
+ )
295
+ ),
296
+ )
297
+
298
+ def validate(self) -> None:
299
+ count = len(self)
300
+ if len(self.start_end_xyz) != count * 6:
301
+ raise ValueError(
302
+ "line start_end_xyzの長さは count * 6 である必要があります"
303
+ )
304
+ if len(self.reference_y_xyz) != count * 3:
305
+ raise ValueError(
306
+ "line reference_y_xyzの長さは count * 3 である必要があります"
307
+ )
308
+ if any(value not in (0, 1) for value in self.reference_y_valid):
309
+ raise ValueError(
310
+ "line reference_y_valid の値は 0 または 1 である必要があります"
311
+ )
312
+
313
+
314
+ @dataclass(frozen=True, slots=True)
315
+ class PlaneLoop:
316
+ role: SurfaceLoopType
317
+ points: tuple[Vector3d, ...]
318
+
319
+
320
+ @dataclass(frozen=True, slots=True)
321
+ class PlaneShape:
322
+ loops: tuple[PlaneLoop, ...]
323
+
324
+
325
+ @dataclass(slots=True)
326
+ class PlaneShapeTable:
327
+ points_xyz: array[float] = field(default_factory=_new_f64_array, repr=False)
328
+ """全ループの頂点。x, y, z の順で1点につき3値。ループの順序はplane_loop_offsetsで表す。"""
329
+ loop_point_offsets: array[int] = field(
330
+ default_factory=_new_offsets_array,
331
+ repr=False,
332
+ )
333
+ """ループiが使用する頂点の範囲は
334
+ loop_point_offsets[i]:loop_point_offsets[i + 1]。"""
335
+
336
+ plane_loop_offsets: array[int] = field(
337
+ default_factory=_new_offsets_array,
338
+ repr=False,
339
+ )
340
+ """面iが使用するループ範囲は
341
+ plane_loop_offsets[i]:plane_loop_offsets[i + 1]。
342
+ """
343
+
344
+ loop_types: bytearray = field(default_factory=bytearray, repr=False)
345
+ """loop_point_offsetsで表される各ループと同じ順序でOUTER/HOLEを保持する。"""
346
+
347
+ @property
348
+ def loop_count(self) -> int:
349
+ return len(self.loop_point_offsets) - 1
350
+
351
+ @property
352
+ def plane_count(self) -> int:
353
+ return len(self.plane_loop_offsets) - 1
354
+
355
+ def add(
356
+ self,
357
+ loops: Sequence[tuple[SurfaceLoopType | int, Sequence[Sequence[float]]]],
358
+ ) -> int:
359
+ if not loops:
360
+ raise ValueError("planeには少なくとも1つのループが必要です")
361
+
362
+ plane_index: int = self.plane_count
363
+ outer_count: int = 0
364
+
365
+ for raw_role, raw_points in loops:
366
+ role: SurfaceLoopType = SurfaceLoopType(raw_role)
367
+ if role is SurfaceLoopType.OUTER:
368
+ outer_count += 1
369
+
370
+ if len(raw_points) < 3:
371
+ raise ValueError("planeの各ループには少なくとも3点が必要です")
372
+
373
+ for point_index, point in enumerate(raw_points):
374
+ self.points_xyz.extend(
375
+ _vector3(point, name=f"plane point {point_index}")
376
+ )
377
+
378
+ self.loop_point_offsets.append(len(self.points_xyz) // 3)
379
+ self.loop_types.append(int(role))
380
+
381
+ if outer_count != 1:
382
+ raise ValueError("planeにはOUTERループが1つである必要があります")
383
+
384
+ self.plane_loop_offsets.append(self.loop_count)
385
+ return plane_index
386
+
387
+ def get(self, index: int) -> PlaneShape:
388
+ if not 0 <= index < self.plane_count:
389
+ raise IndexError(index)
390
+
391
+ loop_start: int = self.plane_loop_offsets[index]
392
+ loop_end: int = self.plane_loop_offsets[index + 1]
393
+ loops: list[PlaneLoop] = []
394
+
395
+ for loop_index in range(loop_start, loop_end):
396
+ point_start: int = self.loop_point_offsets[loop_index]
397
+ point_end: int = self.loop_point_offsets[loop_index + 1]
398
+ points: list[Vector3d] = []
399
+ for point_index in range(point_start, point_end):
400
+ offset = point_index * 3
401
+ points.append(
402
+ Vector3d(
403
+ float(self.points_xyz[offset]),
404
+ float(self.points_xyz[offset + 1]),
405
+ float(self.points_xyz[offset + 2]),
406
+ )
407
+ )
408
+ loops.append(
409
+ PlaneLoop(
410
+ role=SurfaceLoopType(self.loop_types[loop_index]),
411
+ points=tuple(points),
412
+ )
413
+ )
414
+
415
+ return PlaneShape(loops=tuple(loops))
416
+
417
+ def validate(self) -> None:
418
+ if len(self.points_xyz) % 3 != 0:
419
+ raise ValueError("plane points_xyzの長さは3の倍数である必要があります")
420
+ if not self.loop_point_offsets or self.loop_point_offsets[0] != 0:
421
+ raise ValueError("plane loop_point_offsetsは0で開始する必要があります")
422
+ if not self.plane_loop_offsets or self.plane_loop_offsets[0] != 0:
423
+ raise ValueError("plane plane_loop_offsetsは0で開始する必要があります")
424
+ if self.loop_point_offsets[-1] != len(self.points_xyz) // 3:
425
+ raise ValueError("最後のloop point offsetは点数と一致する必要があります")
426
+ if self.plane_loop_offsets[-1] != self.loop_count:
427
+ raise ValueError(
428
+ "最後のplane loop offsetはループ数と一致する必要があります"
429
+ )
430
+ if len(self.loop_types) != self.loop_count:
431
+ raise ValueError("plane loop type数はループ数と一致する必要があります")
432
+
433
+ for loop_index in range(self.loop_count):
434
+ start: int = self.loop_point_offsets[loop_index]
435
+ end: int = self.loop_point_offsets[loop_index + 1]
436
+ if end < start or end - start < 3:
437
+ raise ValueError(f"plane loop {loop_index}には少なくとも3点が必要です")
438
+ SurfaceLoopType(self.loop_types[loop_index])
439
+
440
+ for plane_index in range(self.plane_count):
441
+ plane_start: int = self.plane_loop_offsets[plane_index]
442
+ plane_end: int = self.plane_loop_offsets[plane_index + 1]
443
+ if plane_end <= plane_start:
444
+ raise ValueError(
445
+ f"plane {plane_index}には少なくとも1つのループが必要です"
446
+ )
447
+ outer_count: int = sum(
448
+ self.loop_types[loop_index] == SurfaceLoopType.OUTER
449
+ for loop_index in range(plane_start, plane_end)
450
+ )
451
+ if outer_count != 1:
452
+ raise ValueError(
453
+ f"plane {plane_index}にはouter loopが1つである必要があります"
454
+ )
455
+
456
+
457
+ @dataclass(frozen=True, slots=True)
458
+ class Node:
459
+ position: Vector3d
460
+ source_id: str
461
+ guid: UUID | None
462
+ story_index: int | None
463
+ xpath: str | None
464
+
465
+
466
+ @dataclass(slots=True)
467
+ class NodeTable:
468
+ positions_xyz: array[float] = field(default_factory=_new_f64_array, repr=False)
469
+ source_id: list[str] = field(default_factory=list, repr=False)
470
+ guid: OptionalUuidColumn = field(default_factory=OptionalUuidColumn)
471
+ story_index: array[int] = field(default_factory=_new_u32_array, repr=False)
472
+ xpath: list[str | None] = field(default_factory=list, repr=False)
473
+
474
+ def __len__(self) -> int:
475
+ return len(self.source_id)
476
+
477
+ def add(
478
+ self,
479
+ position: Sequence[float],
480
+ *,
481
+ source_id: str,
482
+ guid: UUID | str | None = None,
483
+ story_index: int | None = None,
484
+ xpath: str | None = None,
485
+ ) -> int:
486
+ index: int = len(self)
487
+ self.positions_xyz.extend(_vector3(position, name="node position"))
488
+ self.source_id.append(source_id)
489
+ self.guid.append(guid)
490
+ # u32列ではNone直接保持できないため、UINT32_NONEとして持たせる。
491
+ self.story_index.append(
492
+ UINT32_NONE if story_index is None else int(story_index)
493
+ )
494
+ self.xpath.append(xpath)
495
+ return index
496
+
497
+ def get(self, index: int) -> Node:
498
+ if not 0 <= index < len(self):
499
+ raise IndexError(index)
500
+ offset: int = index * 3
501
+ raw_story: int = self.story_index[index]
502
+ return Node(
503
+ position=Vector3d(
504
+ float(self.positions_xyz[offset]),
505
+ float(self.positions_xyz[offset + 1]),
506
+ float(self.positions_xyz[offset + 2]),
507
+ ),
508
+ source_id=self.source_id[index],
509
+ guid=self.guid.get(index),
510
+ story_index=None if raw_story == UINT32_NONE else int(raw_story),
511
+ xpath=self.xpath[index],
512
+ )
513
+
514
+ def validate(self, story_count: int) -> None:
515
+ count: int = len(self)
516
+ if len(self.positions_xyz) != count * 3:
517
+ raise ValueError("node positionsの長さはnode数 * 3と一致する必要があります")
518
+ self.guid.validate(count)
519
+ if len(self.story_index) != count:
520
+ raise ValueError("node story_index数はnode数と一致する必要があります")
521
+ if len(self.xpath) != count:
522
+ raise ValueError("node xpath数はnode数と一致する必要があります")
523
+ for index in self.story_index:
524
+ if index != UINT32_NONE and index >= story_count:
525
+ raise ValueError(f"node story index {index} は範囲外です")
526
+
527
+
528
+ @dataclass(frozen=True, slots=True)
529
+ class Story:
530
+ source_id: str
531
+ name: str | None
532
+ elevation: float
533
+
534
+
535
+ @dataclass(slots=True)
536
+ class StoryTable:
537
+ source_id: list[str] = field(default_factory=list, repr=False)
538
+ name: list[str | None] = field(default_factory=list, repr=False)
539
+ elevations: array[float] = field(default_factory=_new_f64_array, repr=False)
540
+
541
+ def __len__(self) -> int:
542
+ return len(self.source_id)
543
+
544
+ def add(
545
+ self,
546
+ *,
547
+ source_id: str,
548
+ elevation: float,
549
+ name: str | None = None,
550
+ ) -> int:
551
+ elevation_value: float = float(elevation)
552
+ if not isfinite(elevation_value):
553
+ raise ValueError("story elevationは有限値である必要があります")
554
+ index: int = len(self)
555
+ self.source_id.append(source_id)
556
+ self.name.append(name)
557
+ self.elevations.append(elevation_value)
558
+ return index
559
+
560
+ def get(self, index: int) -> Story:
561
+ if not 0 <= index < len(self):
562
+ raise IndexError(index)
563
+ return Story(
564
+ source_id=self.source_id[index],
565
+ name=self.name[index],
566
+ elevation=float(self.elevations[index]),
567
+ )
568
+
569
+ def validate(self) -> None:
570
+ count: int = len(self)
571
+ if len(self.name) != count or len(self.elevations) != count:
572
+ raise ValueError("storyの各列は同じ長さである必要があります")
573
+
574
+
575
+ @dataclass(frozen=True, slots=True)
576
+ class Axis:
577
+ source_id: str
578
+ name: str | None
579
+ points: tuple[Vector3d, ...]
580
+
581
+
582
+ @dataclass(slots=True)
583
+ class AxisTable:
584
+ points_xyz: array[float] = field(default_factory=_new_f64_array, repr=False)
585
+ """全通り芯の点をx, y, zで連続格納する。"""
586
+ point_start: array[int] = field(default_factory=_new_u32_array, repr=False)
587
+ """各通り芯がpoints_xyz中で使用する点単位の開始位置。"""
588
+ point_count: array[int] = field(default_factory=_new_u32_array, repr=False)
589
+ """各通り芯がpoints_xyz中で使用する点単位の長さ。"""
590
+ source_id: list[str] = field(default_factory=list, repr=False)
591
+ name: list[str | None] = field(default_factory=list, repr=False)
592
+
593
+ def __len__(self) -> int:
594
+ return len(self.source_id)
595
+
596
+ def add(
597
+ self,
598
+ points: Sequence[Sequence[float]],
599
+ *,
600
+ source_id: str,
601
+ name: str | None = None,
602
+ ) -> int:
603
+ if len(points) < 2:
604
+ raise ValueError("axisには少なくとも2点が必要です")
605
+ index: int = len(self)
606
+ start: int = len(self.points_xyz) // 3
607
+ for point_index, point in enumerate(points):
608
+ self.points_xyz.extend(_vector3(point, name=f"axis point {point_index}"))
609
+ self.point_start.append(start)
610
+ self.point_count.append(len(points))
611
+ self.source_id.append(source_id)
612
+ self.name.append(name)
613
+ return index
614
+
615
+ def get(self, index: int) -> Axis:
616
+ if not 0 <= index < len(self):
617
+ raise IndexError(index)
618
+ start: int = self.point_start[index]
619
+ end: int = start + self.point_count[index]
620
+ points: list[Vector3d] = []
621
+ for point_index in range(start, end):
622
+ offset: int = point_index * 3
623
+ points.append(
624
+ Vector3d(
625
+ float(self.points_xyz[offset]),
626
+ float(self.points_xyz[offset + 1]),
627
+ float(self.points_xyz[offset + 2]),
628
+ )
629
+ )
630
+ return Axis(
631
+ source_id=self.source_id[index],
632
+ name=self.name[index],
633
+ points=tuple(points),
634
+ )
635
+
636
+ def validate(self) -> None:
637
+ count: int = len(self)
638
+ if len(self.points_xyz) % 3 != 0:
639
+ raise ValueError("axis pointsの長さは3の倍数である必要があります")
640
+ if not (
641
+ len(self.point_start) == len(self.point_count) == len(self.name) == count
642
+ ):
643
+ raise ValueError("axisの各列は同じ長さである必要があります")
644
+ total_points: int = len(self.points_xyz) // 3
645
+ for axis_index in range(count):
646
+ start: int = self.point_start[axis_index]
647
+ point_count: int = self.point_count[axis_index]
648
+ if point_count < 2 or start + point_count > total_points:
649
+ raise ValueError(f"axis {axis_index} の点範囲が不正です")
650
+
651
+
652
+ @dataclass(slots=True)
653
+ class ElementTable:
654
+ kind: array[int] = field(default_factory=_new_u32_array, repr=False)
655
+ flags: array[int] = field(default_factory=_new_u32_array, repr=False)
656
+
657
+ # PolygonMesh中でこの要素が占有する頂点・面の連続範囲。
658
+ # メッシュ未設定時はstartをUINT32_NONE、countを0とする。
659
+ vertex_start: array[int] = field(default_factory=_new_u32_array, repr=False)
660
+ vertex_count: array[int] = field(default_factory=_new_u32_array, repr=False)
661
+ face_start: array[int] = field(default_factory=_new_u32_array, repr=False)
662
+ face_count: array[int] = field(default_factory=_new_u32_array, repr=False)
663
+
664
+ # 軸・面テーブルの参照。存在しなければUINT32_NONE。
665
+ line_shape_index: array[int] = field(default_factory=_new_u32_array, repr=False)
666
+ plane_shape_index: array[int] = field(default_factory=_new_u32_array, repr=False)
667
+
668
+ # BuildingGeometry.element_node_indices / element_story_indices中の可変長範囲。
669
+ node_start: array[int] = field(default_factory=_new_u32_array, repr=False)
670
+ node_count: array[int] = field(default_factory=_new_u32_array, repr=False)
671
+ story_start: array[int] = field(default_factory=_new_u32_array, repr=False)
672
+ story_count: array[int] = field(default_factory=_new_u32_array, repr=False)
673
+
674
+ guid: OptionalUuidColumn = field(default_factory=OptionalUuidColumn)
675
+ xpath: list[str | None] = field(default_factory=list, repr=False)
676
+ name: list[str | None] = field(default_factory=list, repr=False)
677
+ source_type: list[str] = field(default_factory=list, repr=False)
678
+ source_id: list[str | None] = field(default_factory=list, repr=False)
679
+
680
+ def __len__(self) -> int:
681
+ return len(self.kind)
682
+
683
+ def add(
684
+ self,
685
+ *,
686
+ kind: ElementKind | int,
687
+ guid: UUID | str | None,
688
+ xpath: str | None,
689
+ source_type: str,
690
+ source_id: str | None = None,
691
+ name: str | None = None,
692
+ flags: ElementFlags = ElementFlags.NONE,
693
+ ) -> int:
694
+ # flagsはメッシュや関連の有無から導けない属性だけを渡す。
695
+ # HAS_*は対応するデータを追加するメソッドがORするため、ここで渡してはいけない。
696
+
697
+ index: int = len(self)
698
+ self.kind.append(int(ElementKind(kind)))
699
+ self.flags.append(int(ElementFlags(flags)))
700
+
701
+ # メッシュや関連は後から追加するため、最初は未設定で初期化する。
702
+ self.vertex_start.append(UINT32_NONE)
703
+ self.vertex_count.append(0)
704
+ self.face_start.append(UINT32_NONE)
705
+ self.face_count.append(0)
706
+
707
+ self.line_shape_index.append(UINT32_NONE)
708
+ self.plane_shape_index.append(UINT32_NONE)
709
+ self.node_start.append(UINT32_NONE)
710
+ self.node_count.append(0)
711
+ self.story_start.append(UINT32_NONE)
712
+ self.story_count.append(0)
713
+
714
+ self.guid.append(guid)
715
+ self.xpath.append(xpath)
716
+ self.name.append(name)
717
+ self.source_type.append(source_type)
718
+ self.source_id.append(source_id)
719
+ return index
720
+
721
+ def mesh_range(self, element_index: int) -> MeshRange | None:
722
+ if not 0 <= element_index < len(self):
723
+ raise IndexError(element_index)
724
+ vertex_start: int = self.vertex_start[element_index]
725
+ if vertex_start == UINT32_NONE:
726
+ return None
727
+ return MeshRange(
728
+ vertex_start=int(vertex_start),
729
+ vertex_count=int(self.vertex_count[element_index]),
730
+ face_start=int(self.face_start[element_index]),
731
+ face_count=int(self.face_count[element_index]),
732
+ )
733
+
734
+ def validate(self) -> None:
735
+ count: int = len(self)
736
+ columns: dict[str, Sized] = {
737
+ "flags": self.flags,
738
+ "vertex_start": self.vertex_start,
739
+ "vertex_count": self.vertex_count,
740
+ "face_start": self.face_start,
741
+ "face_count": self.face_count,
742
+ "line_shape_index": self.line_shape_index,
743
+ "plane_shape_index": self.plane_shape_index,
744
+ "node_start": self.node_start,
745
+ "node_count": self.node_count,
746
+ "story_start": self.story_start,
747
+ "story_count": self.story_count,
748
+ "xpath": self.xpath,
749
+ "name": self.name,
750
+ "source_type": self.source_type,
751
+ "source_id": self.source_id,
752
+ }
753
+ for name, column in columns.items():
754
+ if len(column) != count:
755
+ raise ValueError(
756
+ f"element列 {name!r} の長さは {count} である必要があります: "
757
+ f"actual={len(column)}"
758
+ )
759
+ self.guid.validate(count)
760
+ for value in self.kind:
761
+ ElementKind(value)
762
+
763
+
764
+ @dataclass(slots=True)
765
+ class BuildingGeometry:
766
+ """ST-Bridge 3D描画中間データ。
767
+
768
+ 長さの単位はm。ST-Bridgeの長さ単位はmmであるので注意。
769
+
770
+ 構築中は各add_*/set_*メソッドで追記し、完成後にfreeze()を呼び出す。
771
+ 凍結時に全参照を検証し、その後のPyO3転送やmemoryviewによるゼロコピー参照問題が生じないようにする。
772
+ """
773
+
774
+ origin_xyz: Vector3d = field(default_factory=Vector3d)
775
+ """原点"""
776
+ mesh: PolygonMesh = field(default_factory=PolygonMesh)
777
+ elements: ElementTable = field(default_factory=ElementTable)
778
+ line_shapes: LineShapeTable = field(default_factory=LineShapeTable)
779
+ plane_shapes: PlaneShapeTable = field(default_factory=PlaneShapeTable)
780
+ nodes: NodeTable = field(default_factory=NodeTable)
781
+ stories: StoryTable = field(default_factory=StoryTable)
782
+ axes: AxisTable = field(default_factory=AxisTable)
783
+ element_node_indices: array[int] = field(
784
+ default_factory=_new_u32_array,
785
+ repr=False,
786
+ )
787
+ element_story_indices: array[int] = field(
788
+ default_factory=_new_u32_array,
789
+ repr=False,
790
+ )
791
+ _frozen: bool = field(default=False, init=False, repr=False)
792
+
793
+ def __post_init__(self) -> None:
794
+ self.origin_xyz = _vector3(self.origin_xyz, name="origin_xyz")
795
+
796
+ @property
797
+ def frozen(self) -> bool:
798
+ return self._frozen
799
+
800
+ def _ensure_mutable(self) -> None:
801
+ if self._frozen:
802
+ raise RuntimeError("BuildingGeometryは凍結済みです")
803
+
804
+ def _check_element_index(self, element_index: int) -> None:
805
+ if not 0 <= element_index < len(self.elements):
806
+ raise IndexError(element_index)
807
+
808
+ def add_element(
809
+ self,
810
+ *,
811
+ guid: UUID | str | None = None,
812
+ xpath: str | None = None,
813
+ source_type: str = "",
814
+ kind: ElementKind | int = ElementKind.OTHER,
815
+ source_id: str | None = None,
816
+ name: str | None = None,
817
+ flags: ElementFlags = ElementFlags.NONE,
818
+ ) -> int:
819
+ """ST-Bridge要素の行を追加し、indexを返す。
820
+
821
+ この時点ではメッシュや節点関連を持たなくてもよい。
822
+ 返されたindexをadd_element_meshやset_element_nodesへ渡して情報を追加する。
823
+
824
+ flagsにはSRC断面の部位のように、他の列から導けない属性だけを渡す。
825
+ """
826
+
827
+ self._ensure_mutable()
828
+ return self.elements.add(
829
+ kind=kind,
830
+ guid=guid,
831
+ xpath=xpath,
832
+ source_type=source_type,
833
+ source_id=source_id,
834
+ name=name,
835
+ flags=flags,
836
+ )
837
+
838
+ def add_story(
839
+ self,
840
+ *,
841
+ source_id: str,
842
+ elevation: float,
843
+ name: str | None = None,
844
+ ) -> int:
845
+ self._ensure_mutable()
846
+ return self.stories.add(
847
+ source_id=source_id,
848
+ elevation=elevation,
849
+ name=name,
850
+ )
851
+
852
+ def add_node(
853
+ self,
854
+ position: Sequence[float],
855
+ *,
856
+ source_id: str,
857
+ guid: UUID | str | None = None,
858
+ story_index: int | None = None,
859
+ xpath: str | None = None,
860
+ ) -> int:
861
+ self._ensure_mutable()
862
+ if story_index is not None and not 0 <= story_index < len(self.stories):
863
+ raise IndexError(f"story_indexが範囲外です: {story_index}")
864
+ return self.nodes.add(
865
+ position,
866
+ source_id=source_id,
867
+ guid=guid,
868
+ story_index=story_index,
869
+ xpath=xpath,
870
+ )
871
+
872
+ def add_axis(
873
+ self,
874
+ points: Sequence[Sequence[float]],
875
+ *,
876
+ source_id: str,
877
+ name: str | None = None,
878
+ ) -> int:
879
+ self._ensure_mutable()
880
+ return self.axes.add(points, source_id=source_id, name=name)
881
+
882
+ def add_element_mesh(
883
+ self,
884
+ element_index: int,
885
+ vertices: Iterable[Sequence[float]],
886
+ faces: Iterable[Sequence[int]],
887
+ ) -> MeshRange:
888
+ """要素単位の頂点と面を追加する。
889
+
890
+ verticesとfacesは通常のタプルやリストで渡せる簡易版API。
891
+ """
892
+
893
+ positions: array[float] = array("f")
894
+ for vertex_index, vertex in enumerate(vertices):
895
+ positions.extend(_vector3(vertex, name=f"vertex {vertex_index}"))
896
+
897
+ face_vertex_indices: array[int] = array("I")
898
+ face_offsets: array[int] = array("I", [0])
899
+ for face_index, face in enumerate(faces):
900
+ face_values: tuple[int, ...] = tuple(int(index) for index in face)
901
+ if len(face_values) < 3:
902
+ raise ValueError(
903
+ f"face {face_index} には少なくとも3つのindexが必要です"
904
+ )
905
+ if any(index < 0 for index in face_values):
906
+ raise ValueError(f"face {face_index} に負のindexが含まれています")
907
+ face_vertex_indices.extend(face_values)
908
+ face_offsets.append(len(face_vertex_indices))
909
+
910
+ return self.add_element_mesh_flat(
911
+ element_index,
912
+ positions,
913
+ face_vertex_indices,
914
+ face_offsets,
915
+ )
916
+
917
+ def add_element_mesh_flat(
918
+ self,
919
+ element_index: int,
920
+ positions: array[float],
921
+ local_face_vertex_indices: array[int],
922
+ local_face_offsets: array[int],
923
+ ) -> MeshRange:
924
+ """配列から要素メッシュを追加する。
925
+
926
+ 入力頂点番号は要素の先頭頂点を0とする。
927
+ 追加時に全体頂点配列の開始番号を加え、モデル全体に対する絶対頂点番号へ変換する。
928
+ """
929
+
930
+ self._ensure_mutable()
931
+ self._check_element_index(element_index)
932
+
933
+ if self.elements.vertex_start[element_index] != UINT32_NONE:
934
+ raise ValueError(f"element {element_index} には既にmeshが設定されています")
935
+ if positions.typecode != "f":
936
+ raise TypeError("positionsはarray('f')である必要があります")
937
+ if local_face_vertex_indices.typecode != "I":
938
+ raise TypeError("local_face_vertex_indicesはarray('I')である必要があります")
939
+ if local_face_offsets.typecode != "I":
940
+ raise TypeError("local_face_offsetsはarray('I')である必要があります")
941
+ if len(positions) % 3 != 0:
942
+ raise ValueError("positionsの長さは3の倍数である必要があります")
943
+ if not local_face_offsets or local_face_offsets[0] != 0:
944
+ raise ValueError("local_face_offsetsは0で開始する必要があります")
945
+ if local_face_offsets[-1] != len(local_face_vertex_indices):
946
+ raise ValueError(
947
+ "最後のlocal face offsetはlocal face index数と一致する必要があります"
948
+ )
949
+
950
+ local_vertex_count: int = len(positions) // 3
951
+ local_face_count: int = len(local_face_offsets) - 1
952
+ if local_vertex_count == 0 or local_face_count == 0:
953
+ raise ValueError("element meshには頂点と面の両方が必要です")
954
+
955
+ for face_index in range(local_face_count):
956
+ start: int = local_face_offsets[face_index]
957
+ end: int = local_face_offsets[face_index + 1]
958
+ if end < start or end - start < 3:
959
+ raise ValueError(
960
+ f"face {face_index} には少なくとも3つのindexが必要です"
961
+ )
962
+
963
+ for index in local_face_vertex_indices:
964
+ if index >= local_vertex_count:
965
+ raise ValueError(
966
+ f"local vertex index {index} は "
967
+ f"{local_vertex_count} 頂点の範囲外です"
968
+ )
969
+
970
+ vertex_start: int = self.mesh.vertex_count
971
+ face_start: int = self.mesh.face_count
972
+ index_base: int = len(self.mesh.face_vertex_indices)
973
+
974
+ self.mesh.positions.extend(positions)
975
+ self.mesh.face_vertex_indices.extend(
976
+ vertex_start + index for index in local_face_vertex_indices
977
+ )
978
+
979
+ self.mesh.face_offsets.extend(
980
+ index_base + offset for offset in local_face_offsets[1:]
981
+ )
982
+
983
+ self.elements.vertex_start[element_index] = vertex_start
984
+ self.elements.vertex_count[element_index] = local_vertex_count
985
+ self.elements.face_start[element_index] = face_start
986
+ self.elements.face_count[element_index] = local_face_count
987
+ self.elements.flags[element_index] |= int(ElementFlags.HAS_MESH)
988
+
989
+ return MeshRange(
990
+ vertex_start=vertex_start,
991
+ vertex_count=local_vertex_count,
992
+ face_start=face_start,
993
+ face_count=local_face_count,
994
+ )
995
+
996
+ def set_element_nodes(
997
+ self,
998
+ element_index: int,
999
+ node_indices: Iterable[int],
1000
+ ) -> None:
1001
+
1002
+ self._ensure_mutable()
1003
+ self._check_element_index(element_index)
1004
+ if self.elements.node_start[element_index] != UINT32_NONE:
1005
+ raise ValueError(f"element {element_index} のnodesは既に設定済みです")
1006
+
1007
+ values: tuple[int, ...] = tuple(int(index) for index in node_indices)
1008
+ for index in values:
1009
+ if not 0 <= index < len(self.nodes):
1010
+ raise IndexError(f"node indexが範囲外です: {index}")
1011
+
1012
+ self.elements.node_start[element_index] = len(self.element_node_indices)
1013
+ self.elements.node_count[element_index] = len(values)
1014
+ self.element_node_indices.extend(values)
1015
+ self.elements.flags[element_index] |= int(ElementFlags.HAS_NODE_RELATION)
1016
+
1017
+ def get_element_node_indices(self, element_index: int) -> tuple[int, ...]:
1018
+ self._check_element_index(element_index)
1019
+ start: int = self.elements.node_start[element_index]
1020
+ if start == UINT32_NONE:
1021
+ return ()
1022
+ count: int = self.elements.node_count[element_index]
1023
+ return tuple(
1024
+ int(value) for value in self.element_node_indices[start : start + count]
1025
+ )
1026
+
1027
+ def set_element_stories(
1028
+ self,
1029
+ element_index: int,
1030
+ story_indices: Iterable[int],
1031
+ ) -> None:
1032
+
1033
+ self._ensure_mutable()
1034
+ self._check_element_index(element_index)
1035
+ if self.elements.story_start[element_index] != UINT32_NONE:
1036
+ raise ValueError(f"element {element_index} のlevelsは既に設定済みです")
1037
+
1038
+ values: tuple[int, ...] = tuple(int(index) for index in story_indices)
1039
+ for index in values:
1040
+ if not 0 <= index < len(self.stories):
1041
+ raise IndexError(f"story indexが範囲外です: {index}")
1042
+
1043
+ self.elements.story_start[element_index] = len(self.element_story_indices)
1044
+ self.elements.story_count[element_index] = len(values)
1045
+ self.element_story_indices.extend(values)
1046
+ self.elements.flags[element_index] |= int(ElementFlags.HAS_STORY_RELATION)
1047
+
1048
+ def add_element_line_shape(
1049
+ self,
1050
+ element_index: int,
1051
+ *,
1052
+ start: Sequence[float],
1053
+ end: Sequence[float],
1054
+ reference_y: Sequence[float] | None = None,
1055
+ ) -> int:
1056
+
1057
+ self._ensure_mutable()
1058
+ self._check_element_index(element_index)
1059
+ if self.elements.line_shape_index[element_index] != UINT32_NONE:
1060
+ raise ValueError(
1061
+ f"element {element_index} には既にline shapeが設定されています"
1062
+ )
1063
+
1064
+ shape_index: int = self.line_shapes.add(start, end, reference_y)
1065
+ self.elements.line_shape_index[element_index] = shape_index
1066
+ self.elements.flags[element_index] |= int(ElementFlags.HAS_LINE_SHAPE)
1067
+ return shape_index
1068
+
1069
+ def get_element_line_shape(self, element_index: int) -> LineShape | None:
1070
+
1071
+ self._check_element_index(element_index)
1072
+ shape_index: int = self.elements.line_shape_index[element_index]
1073
+ if shape_index == UINT32_NONE:
1074
+ return None
1075
+ return self.line_shapes.get(shape_index)
1076
+
1077
+ def add_element_plane_shape(
1078
+ self,
1079
+ element_index: int,
1080
+ loops: Sequence[tuple[SurfaceLoopType | int, Sequence[Sequence[float]]]],
1081
+ ) -> int:
1082
+
1083
+ self._ensure_mutable()
1084
+ self._check_element_index(element_index)
1085
+ if self.elements.plane_shape_index[element_index] != UINT32_NONE:
1086
+ raise ValueError(
1087
+ f"element {element_index} には既にplane shapeが設定されています"
1088
+ )
1089
+
1090
+ shape_index: int = self.plane_shapes.add(loops)
1091
+ self.elements.plane_shape_index[element_index] = shape_index
1092
+ self.elements.flags[element_index] |= int(ElementFlags.HAS_PLANE_SHAPE)
1093
+ return shape_index
1094
+
1095
+ def get_element_plane_shape(
1096
+ self,
1097
+ element_index: int,
1098
+ ) -> PlaneShape | None:
1099
+
1100
+ self._check_element_index(element_index)
1101
+ shape_index: int = self.elements.plane_shape_index[element_index]
1102
+ if shape_index == UINT32_NONE:
1103
+ return None
1104
+ return self.plane_shapes.get(shape_index)
1105
+
1106
+ def validate(self) -> None:
1107
+ self.mesh.validate()
1108
+ self.elements.validate()
1109
+ self.line_shapes.validate()
1110
+ self.plane_shapes.validate()
1111
+ self.stories.validate()
1112
+ self.nodes.validate(len(self.stories))
1113
+ self.axes.validate()
1114
+
1115
+ vertex_count: int = self.mesh.vertex_count
1116
+ face_count: int = self.mesh.face_count
1117
+
1118
+ for element_index in range(len(self.elements)):
1119
+ span: MeshRange | None = self.elements.mesh_range(element_index)
1120
+ flags: ElementFlags = ElementFlags(self.elements.flags[element_index])
1121
+
1122
+ composite_part: ElementFlags = flags & (
1123
+ ElementFlags.IS_COMPOSITE_CONCRETE | ElementFlags.IS_COMPOSITE_STEEL
1124
+ )
1125
+ if composite_part == (
1126
+ ElementFlags.IS_COMPOSITE_CONCRETE | ElementFlags.IS_COMPOSITE_STEEL
1127
+ ):
1128
+ raise ValueError(f"element {element_index} はconcreteとsteelの両方です")
1129
+
1130
+ if span is None:
1131
+ if flags & ElementFlags.HAS_MESH:
1132
+ raise ValueError(
1133
+ f"element {element_index} はHAS_MESHですがmesh spanがありません"
1134
+ )
1135
+ else:
1136
+ if not flags & ElementFlags.HAS_MESH:
1137
+ raise ValueError(
1138
+ f"element {element_index} はmesh spanがありますがHAS_MESHではありません"
1139
+ )
1140
+ if span.vertex_start + span.vertex_count > vertex_count:
1141
+ raise ValueError(
1142
+ f"element {element_index} のvertex spanは範囲外です"
1143
+ )
1144
+ if span.face_start + span.face_count > face_count:
1145
+ raise ValueError(f"element {element_index} のface spanは範囲外です")
1146
+
1147
+ face_end: int = span.face_start + span.face_count
1148
+ vertex_end: int = span.vertex_start + span.vertex_count
1149
+ for face_index in range(span.face_start, face_end):
1150
+ start: int = self.mesh.face_offsets[face_index]
1151
+ end: int = self.mesh.face_offsets[face_index + 1]
1152
+ for vertex_index in self.mesh.face_vertex_indices[start:end]:
1153
+ if not span.vertex_start <= vertex_index < vertex_end:
1154
+ raise ValueError(
1155
+ f"element {element_index} face {face_index} "
1156
+ "が他のelementの頂点を参照しています"
1157
+ )
1158
+
1159
+ line_index: int = self.elements.line_shape_index[element_index]
1160
+ if line_index != UINT32_NONE and line_index >= len(self.line_shapes):
1161
+ raise ValueError(
1162
+ f"element {element_index} のline shape indexは範囲外です"
1163
+ )
1164
+ plane_index: int = self.elements.plane_shape_index[element_index]
1165
+ if (
1166
+ plane_index != UINT32_NONE
1167
+ and plane_index >= self.plane_shapes.plane_count
1168
+ ):
1169
+ raise ValueError(
1170
+ f"element {element_index} のplane shape indexは範囲外です"
1171
+ )
1172
+
1173
+ node_start: int = self.elements.node_start[element_index]
1174
+ node_count: int = self.elements.node_count[element_index]
1175
+ if node_start != UINT32_NONE:
1176
+ if node_start + node_count > len(self.element_node_indices):
1177
+ raise ValueError(f"element {element_index} のnode関連は範囲外です")
1178
+ for node_index in self.element_node_indices[
1179
+ node_start : node_start + node_count
1180
+ ]:
1181
+ if node_index >= len(self.nodes):
1182
+ raise ValueError(
1183
+ f"element {element_index} のnode indexは範囲外です"
1184
+ )
1185
+
1186
+ story_start: int = self.elements.story_start[element_index]
1187
+ story_count: int = self.elements.story_count[element_index]
1188
+ if story_start != UINT32_NONE:
1189
+ if story_start + story_count > len(self.element_story_indices):
1190
+ raise ValueError(f"element {element_index} のstory関連は範囲外です")
1191
+ for story_index in self.element_story_indices[
1192
+ story_start : story_start + story_count
1193
+ ]:
1194
+ if story_index >= len(self.stories):
1195
+ raise ValueError(
1196
+ f"element {element_index} のstory indexは範囲外です"
1197
+ )
1198
+
1199
+ def freeze(self) -> None:
1200
+ """全体をチェックし、Rust転送やmemoryview参照を行える状態にする。
1201
+
1202
+ 凍結後は追加系メソッドがRuntimeErrorをなげる。
1203
+ PyO3転送はRust所有のVecへコピーするため凍結後もPythonオブジェクトを保持し続ける必要はない。
1204
+ """
1205
+ if self._frozen:
1206
+ return
1207
+ self.validate()
1208
+ self._frozen = True