Coverage for pyTooling/Cartesian3D/__init__.py: 94%

211 statements  

« prev     ^ index     » next       coverage.py v7.8.0, created at 2025-05-18 22:20 +0000

1# ==================================================================================================================== # 

2# _____ _ _ ____ _ _ _____ ____ # 

3# _ __ _ |_ _|__ ___ | (_)_ __ __ _ / ___|__ _ _ __| |_ ___ ___(_) __ _ _ __ |___ /| _ \ # 

4# | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` || | / _` | '__| __/ _ \/ __| |/ _` | '_ \ |_ \| | | | # 

5# | |_) | |_| || | (_) | (_) | | | | | | (_| || |__| (_| | | | || __/\__ \ | (_| | | | |___) | |_| | # 

6# | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)____\__,_|_| \__\___||___/_|\__,_|_| |_|____/|____/ # 

7# |_| |___/ |___/ # 

8# ==================================================================================================================== # 

9# Authors: # 

10# Patrick Lehmann # 

11# # 

12# License: # 

13# ==================================================================================================================== # 

14# Copyright 2025-2025 Patrick Lehmann - Bötzingen, Germany # 

15# # 

16# Licensed under the Apache License, Version 2.0 (the "License"); # 

17# you may not use this file except in compliance with the License. # 

18# You may obtain a copy of the License at # 

19# # 

20# http://www.apache.org/licenses/LICENSE-2.0 # 

21# # 

22# Unless required by applicable law or agreed to in writing, software # 

23# distributed under the License is distributed on an "AS IS" BASIS, # 

24# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # 

25# See the License for the specific language governing permissions and # 

26# limitations under the License. # 

27# # 

28# SPDX-License-Identifier: Apache-2.0 # 

29# ==================================================================================================================== # 

30# 

31"""An implementation of 3D cartesian data structures for Python.""" 

32from sys import version_info 

33 

34from math import sqrt, acos 

35from typing import Union, Generic, Any, Tuple 

36 

37try: 

38 from pyTooling.Decorators import readonly, export 

39 from pyTooling.Exceptions import ToolingException 

40 from pyTooling.MetaClasses import ExtendedType 

41 from pyTooling.Common import getFullyQualifiedName 

42 from pyTooling.Cartesian2D import Coordinate 

43except (ImportError, ModuleNotFoundError): # pragma: no cover 

44 print("[pyTooling.Cartesian2D] Could not import from 'pyTooling.*'!") 

45 

46 try: 

47 from Decorators import readonly, export 

48 from Exceptions import ToolingException 

49 from MetaClasses import ExtendedType 

50 from Common import getFullyQualifiedName 

51 from Cartesian2D import Coordinate 

52 except (ImportError, ModuleNotFoundError) as ex: # pragma: no cover 

53 print("[pyTooling.Cartesian2D] Could not import directly!") 

54 raise ex 

55 

56 

57@export 

58class Point3D(Generic[Coordinate], metaclass=ExtendedType, slots=True): 

59 """An implementation of a 3D cartesian point.""" 

60 

61 x: Coordinate #: The x-direction coordinate. 

62 y: Coordinate #: The y-direction coordinate. 

63 z: Coordinate #: The z-direction coordinate. 

64 

65 def __init__(self, x: Coordinate, y: Coordinate, z: Coordinate) -> None: 

66 """ 

67 Initializes a 3-dimensional point. 

68 

69 :param x: X-coordinate. 

70 :param y: Y-coordinate. 

71 :param z: Z-coordinate. 

72 :raises TypeError: If x/y/z-coordinate is not of type integer or float. 

73 """ 

74 if not isinstance(x, (int, float)): 

75 ex = TypeError(f"Parameter 'x' is not of type integer or float.") 

76 if version_info >= (3, 11): # pragma: no cover 

77 ex.add_note(f"Got type '{getFullyQualifiedName(x)}'.") 

78 raise ex 

79 if not isinstance(y, (int, float)): 

80 ex = TypeError(f"Parameter 'y' is not of type integer or float.") 

81 if version_info >= (3, 11): # pragma: no cover 

82 ex.add_note(f"Got type '{getFullyQualifiedName(y)}'.") 

83 raise ex 

84 if not isinstance(z, (int, float)): 

85 ex = TypeError(f"Parameter 'z' is not of type integer or float.") 

86 if version_info >= (3, 11): # pragma: no cover 

87 ex.add_note(f"Got type '{getFullyQualifiedName(z)}'.") 

88 raise ex 

89 

90 self.x = x 

91 self.y = y 

92 self.z = z 

93 

94 def Copy(self) -> "Point3D[Coordinate]": # TODO: Python 3.11: -> Self: 

95 """ 

96 Create a new 3D-point as a copy of this 3D point. 

97 

98 :return: Copy of this 3D-point. 

99 

100 .. seealso:: 

101 

102 :meth:`+ operator <__add__>` 

103 Create a new 3D-point moved by a positive 3D-offset. 

104 :meth:`- operator <__sub__>` 

105 Create a new 3D-point moved by a negative 3D-offset. 

106 """ 

107 return self.__class__(self.x, self.y, self.z) 

108 

109 def ToTuple(self) -> Tuple[Coordinate, Coordinate, Coordinate]: 

110 """ 

111 Convert this 3D-Point to a simple 3-element tuple. 

112 

113 :return: ``(x, y, z)`` tuple. 

114 """ 

115 return self.x, self.y, self.z 

116 

117 def __add__(self, other: Any) -> "Point3D[Coordinate]": 

118 """ 

119 Adds a 3D-offset to this 3D-point and creates a new 3D-point. 

120 

121 :param other: A 3D-offset as :class:`Offset3D` or :class:`tuple`. 

122 :return: A new 3D-point shifted by the 3D-offset. 

123 :raises TypeError: If parameter 'other' is not a :class:`Offset3D` or :class:`tuple`. 

124 """ 

125 if isinstance(other, Offset3D): 

126 return self.__class__( 

127 self.x + other.xOffset, 

128 self.y + other.yOffset, 

129 self.z + other.zOffset 

130 ) 

131 elif isinstance(other, tuple): 

132 return self.__class__( 

133 self.x + other[0], 

134 self.y + other[1], 

135 self.z + other[2] 

136 ) 

137 else: 

138 ex = TypeError(f"Parameter 'other' is not of type Offset3D or tuple.") 

139 if version_info >= (3, 11): # pragma: no cover 

140 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.") 

141 raise ex 

142 

143 def __iadd__(self, other: Any) -> "Point3D[Coordinate]": # TODO: Python 3.11: -> Self: 

144 """ 

145 Adds a 3D-offset to this 3D-point (inplace). 

146 

147 :param other: A 3D-offset as :class:`Offset3D` or :class:`tuple`. 

148 :return: This 3D-point. 

149 :raises TypeError: If parameter 'other' is not a :class:`Offset3D` or :class:`tuple`. 

150 """ 

151 if isinstance(other, Offset3D): 

152 self.x += other.xOffset 

153 self.y += other.yOffset 

154 self.z += other.zOffset 

155 elif isinstance(other, tuple): 

156 self.x += other[0] 

157 self.y += other[1] 

158 self.z += other[2] 

159 else: 

160 ex = TypeError(f"Parameter 'other' is not of type Offset3D or tuple.") 

161 if version_info >= (3, 11): # pragma: no cover 

162 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.") 

163 raise ex 

164 

165 return self 

166 

167 def __sub__(self, other: Any) -> Union["Offset3D[Coordinate]", "Point3D[Coordinate]"]: 

168 """ 

169 Subtract two 3D-Points from each other and create a new 3D-offset. 

170 

171 :param other: A 3D-point as :class:`Point3D`. 

172 :return: A new 3D-offset representing the distance between these two points. 

173 :raises TypeError: If parameter 'other' is not a :class:`Point3D`. 

174 """ 

175 if isinstance(other, Point3D): 

176 return Offset3D( 

177 self.x - other.x, 

178 self.y - other.y, 

179 self.z - other.z 

180 ) 

181 else: 

182 ex = TypeError(f"Parameter 'other' is not of type Point3D.") 

183 if version_info >= (3, 11): # pragma: no cover 

184 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.") 

185 raise ex 

186 

187 def __isub__(self, other: Any) -> "Point3D[Coordinate]": # TODO: Python 3.11: -> Self: 

188 """ 

189 Subtracts a 3D-offset to this 3D-point (inplace). 

190 

191 :param other: A 3D-offset as :class:`Offset3D` or :class:`tuple`. 

192 :return: This 3D-point. 

193 :raises TypeError: If parameter 'other' is not a :class:`Offset3D` or :class:`tuple`. 

194 """ 

195 if isinstance(other, Offset3D): 

196 self.x -= other.xOffset 

197 self.y -= other.yOffset 

198 self.z -= other.zOffset 

199 elif isinstance(other, tuple): 

200 self.x -= other[0] 

201 self.y -= other[1] 

202 self.z -= other[2] 

203 else: 

204 ex = TypeError(f"Parameter 'other' is not of type Offset3D or tuple.") 

205 if version_info >= (3, 11): # pragma: no cover 

206 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.") 

207 raise ex 

208 

209 return self 

210 

211 def __repr__(self) -> str: 

212 return f"Point3D({self.x}, {self.y}, {self.z})" 

213 

214 def __str__(self) -> str: 

215 return f"({self.x}, {self.y}, {self.z})" 

216 

217 

218@export 

219class Origin3D(Point3D[Coordinate], Generic[Coordinate]): 

220 """An implementation of a 3D cartesian origin.""" 

221 

222 def __init__(self) -> None: 

223 """ 

224 Initializes a 3-dimensional origin. 

225 """ 

226 super().__init__(0, 0, 0) 

227 

228 def Copy(self) -> "Origin3D[Coordinate]": # TODO: Python 3.11: -> Self: 

229 """ 

230 :raises RuntimeError: Because an origin can't be copied. 

231 """ 

232 raise RuntimeError(f"An origin can't be copied.") 

233 

234 def __repr__(self) -> str: 

235 return f"Origin3D({self.x}, {self.y}, {self.z})" 

236 

237 

238@export 

239class Offset3D(Generic[Coordinate], metaclass=ExtendedType, slots=True): 

240 """An implementation of a 3D cartesian offset.""" 

241 

242 xOffset: Coordinate #: The x-direction offset 

243 yOffset: Coordinate #: The y-direction offset 

244 zOffset: Coordinate #: The z-direction offset 

245 

246 def __init__(self, xOffset: Coordinate, yOffset: Coordinate, zOffset: Coordinate) -> None: 

247 """ 

248 Initializes a 3-dimensional offset. 

249 

250 :param xOffset: x-direction offset. 

251 :param yOffset: y-direction offset. 

252 :param zOffset: z-direction offset. 

253 :raises TypeError: If x/y/z-offset is not of type integer or float. 

254 """ 

255 if not isinstance(xOffset, (int, float)): 

256 ex = TypeError(f"Parameter 'xOffset' is not of type integer or float.") 

257 if version_info >= (3, 11): # pragma: no cover 

258 ex.add_note(f"Got type '{getFullyQualifiedName(xOffset)}'.") 

259 raise ex 

260 if not isinstance(yOffset, (int, float)): 

261 ex = TypeError(f"Parameter 'yOffset' is not of type integer or float.") 

262 if version_info >= (3, 11): # pragma: no cover 

263 ex.add_note(f"Got type '{getFullyQualifiedName(yOffset)}'.") 

264 raise ex 

265 if not isinstance(zOffset, (int, float)): 

266 ex = TypeError(f"Parameter 'zOffset' is not of type integer or float.") 

267 if version_info >= (3, 11): # pragma: no cover 

268 ex.add_note(f"Got type '{getFullyQualifiedName(zOffset)}'.") 

269 raise ex 

270 

271 self.xOffset = xOffset 

272 self.yOffset = yOffset 

273 self.zOffset = zOffset 

274 

275 def Copy(self) -> "Offset3D[Coordinate]": # TODO: Python 3.11: -> Self: 

276 """ 

277 Create a new 3D-offset as a copy of this 3D-offset. 

278 

279 :returns: Copy of this 3D-offset. 

280 

281 .. seealso:: 

282 

283 :meth:`+ operator <__add__>` 

284 Create a new 3D-offset moved by a positive 3D-offset. 

285 :meth:`- operator <__sub__>` 

286 Create a new 3D-offset moved by a negative 3D-offset. 

287 """ 

288 return self.__class__(self.xOffset, self.yOffset, self.zOffset) 

289 

290 def ToTuple(self) -> Tuple[Coordinate, Coordinate, Coordinate]: 

291 """ 

292 Convert this 3D-offset to a simple 3-element tuple. 

293 

294 :returns: ``(x, y, z)`` tuple. 

295 """ 

296 return self.xOffset, self.yOffset, self.zOffset 

297 

298 def __eq__(self, other) -> bool: 

299 """ 

300 Compare two 3D-offsets for equality. 

301 

302 :param other: Parameter to compare against. 

303 :returns: ``True``, if both 3D-offsets are equal. 

304 :raises TypeError: If parameter ``other`` is not of type :class:`Offset3D` or :class:`tuple`. 

305 """ 

306 if isinstance(other, Offset3D): 

307 return self.xOffset == other.xOffset and self.yOffset == other.yOffset and self.zOffset == other.zOffset 

308 elif isinstance(other, tuple): 

309 return self.xOffset == other[0] and self.yOffset == other[1] and self.zOffset == other[2] 

310 else: 

311 ex = TypeError(f"Parameter 'other' is not of type Offset3D or tuple.") 

312 if version_info >= (3, 11): # pragma: no cover 

313 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.") 

314 raise ex 

315 

316 def __ne__(self, other) -> bool: 

317 """ 

318 Compare two 3D-offsets for inequality. 

319 

320 :param other: Parameter to compare against. 

321 :returns: ``True``, if both 3D-offsets are unequal. 

322 :raises TypeError: If parameter ``other`` is not of type :class:`Offset3D` or :class:`tuple`. 

323 """ 

324 return not self.__eq__(other) 

325 

326 def __neg__(self) -> "Offset3D[Coordinate]": 

327 """ 

328 Negate all components of this 3D-offset and create a new 3D-offset. 

329 

330 :returns: 3D-offset with negated offset components. 

331 """ 

332 return self.__class__( 

333 -self.xOffset, 

334 -self.yOffset, 

335 -self.zOffset 

336 ) 

337 

338 def __add__(self, other: Any) -> "Offset3D[Coordinate]": 

339 """ 

340 Adds a 3D-offset to this 3D-offset and creates a new 3D-offset. 

341 

342 :param other: A 3D-offset as :class:`Offset3D` or :class:`tuple`. 

343 :returns: A new 3D-offset extended by the 3D-offset. 

344 :raises TypeError: If parameter 'other' is not a :class:`Offset3D` or :class:`tuple`. 

345 """ 

346 if isinstance(other, Offset3D): 

347 return self.__class__( 

348 self.xOffset + other.xOffset, 

349 self.yOffset + other.yOffset, 

350 self.zOffset + other.zOffset 

351 ) 

352 elif isinstance(other, tuple): 

353 return self.__class__( 

354 self.xOffset + other[0], 

355 self.yOffset + other[1], 

356 self.zOffset + other[2] 

357 ) 

358 else: 

359 ex = TypeError(f"Parameter 'other' is not of type Offset3D or tuple.") 

360 if version_info >= (3, 11): # pragma: no cover 

361 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.") 

362 raise ex 

363 

364 def __iadd__(self, other: Any) -> "Offset3D[Coordinate]": # TODO: Python 3.11: -> Self: 

365 """ 

366 Adds a 3D-offset to this 3D-offset (inplace). 

367 

368 :param other: A 3D-offset as :class:`Offset3D` or :class:`tuple`. 

369 :returns: This 3D-point. 

370 :raises TypeError: If parameter 'other' is not a :class:`Offset3D` or :class:`tuple`. 

371 """ 

372 if isinstance(other, Offset3D): 

373 self.xOffset += other.xOffset 

374 self.yOffset += other.yOffset 

375 self.zOffset += other.zOffset 

376 elif isinstance(other, tuple): 

377 self.xOffset += other[0] 

378 self.yOffset += other[1] 

379 self.zOffset += other[2] 

380 else: 

381 ex = TypeError(f"Parameter 'other' is not of type Offset3D or tuple.") 

382 if version_info >= (3, 11): # pragma: no cover 

383 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.") 

384 raise ex 

385 

386 return self 

387 

388 def __sub__(self, other: Any) -> "Offset3D[Coordinate]": 

389 """ 

390 Subtracts a 3D-offset from this 3D-offset and creates a new 3D-offset. 

391 

392 :param other: A 3D-offset as :class:`Offset3D` or :class:`tuple`. 

393 :returns: A new 3D-offset reduced by the 3D-offset. 

394 :raises TypeError: If parameter 'other' is not a :class:`Offset3D` or :class:`tuple`. 

395 """ 

396 if isinstance(other, Offset3D): 

397 return self.__class__( 

398 self.xOffset - other.xOffset, 

399 self.yOffset - other.yOffset, 

400 self.zOffset - other.zOffset 

401 ) 

402 elif isinstance(other, tuple): 

403 return self.__class__( 

404 self.xOffset - other[0], 

405 self.yOffset - other[1], 

406 self.zOffset - other[2] 

407 ) 

408 else: 

409 ex = TypeError(f"Parameter 'other' is not of type Offset3D or tuple.") 

410 if version_info >= (3, 11): # pragma: no cover 

411 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.") 

412 raise ex 

413 

414 def __isub__(self, other: Any) -> "Offset3D[Coordinate]": # TODO: Python 3.11: -> Self: 

415 """ 

416 Subtracts a 3D-offset from this 3D-offset (inplace). 

417 

418 :param other: A 3D-offset as :class:`Offset3D` or :class:`tuple`. 

419 :returns: This 3D-point. 

420 :raises TypeError: If parameter 'other' is not a :class:`Offset3D` or :class:`tuple`. 

421 """ 

422 if isinstance(other, Offset3D): 

423 self.xOffset -= other.xOffset 

424 self.yOffset -= other.yOffset 

425 self.zOffset -= other.zOffset 

426 elif isinstance(other, tuple): 

427 self.xOffset -= other[0] 

428 self.yOffset -= other[1] 

429 self.zOffset -= other[2] 

430 else: 

431 ex = TypeError(f"Parameter 'other' is not of type Offset3D or tuple.") 

432 if version_info >= (3, 11): # pragma: no cover 

433 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.") 

434 raise ex 

435 

436 return self 

437 

438 def __repr__(self) -> str: 

439 return f"Offset3D({self.xOffset}, {self.yOffset}, {self.zOffset})" 

440 

441 def __str__(self) -> str: 

442 return f"({self.xOffset}, {self.yOffset}, {self.zOffset})" 

443 

444 

445@export 

446class Size3D(Generic[Coordinate], metaclass=ExtendedType, slots=True): 

447 """An implementation of a 3D cartesian size.""" 

448 

449 width: Coordinate #: width in x-direction. 

450 height: Coordinate #: height in y-direction. 

451 depth: Coordinate #: depth in z-direction. 

452 

453 def __init__(self, width: Coordinate, height: Coordinate, depth: Coordinate) -> None: 

454 """ 

455 Initializes a 2-dimensional size. 

456 

457 :param width: width in x-direction. 

458 :param height: height in y-direction. 

459 :param depth: depth in z-direction. 

460 :raises TypeError: If width/height/depth is not of type integer or float. 

461 """ 

462 if not isinstance(width, (int, float)): 

463 ex = TypeError(f"Parameter 'width' is not of type integer or float.") 

464 if version_info >= (3, 11): # pragma: no cover 

465 ex.add_note(f"Got type '{getFullyQualifiedName(width)}'.") 

466 raise ex 

467 if not isinstance(height, (int, float)): 

468 ex = TypeError(f"Parameter 'height' is not of type integer or float.") 

469 if version_info >= (3, 11): # pragma: no cover 

470 ex.add_note(f"Got type '{getFullyQualifiedName(height)}'.") 

471 raise ex 

472 if not isinstance(depth, (int, float)): 

473 ex = TypeError(f"Parameter 'depth' is not of type integer or float.") 

474 if version_info >= (3, 11): # pragma: no cover 

475 ex.add_note(f"Got type '{getFullyQualifiedName(depth)}'.") 

476 raise ex 

477 

478 self.width = width 

479 self.height = height 

480 self.depth = depth 

481 

482 def Copy(self) -> "Size3D[Coordinate]": # TODO: Python 3.11: -> Self: 

483 """ 

484 Create a new 3D-size as a copy of this 3D-size. 

485 

486 :returns: Copy of this 3D-size. 

487 """ 

488 return self.__class__(self.width, self.height, self.depth) 

489 

490 def ToTuple(self) -> Tuple[Coordinate, Coordinate, Coordinate]: 

491 """ 

492 Convert this 3D-size to a simple 3-element tuple. 

493 

494 :return: ``(width, height, depth)`` tuple. 

495 """ 

496 return self.width, self.height, self.depth 

497 

498 def __repr__(self) -> str: 

499 return f"Size3D({self.width}, {self.height}, {self.depth})" 

500 

501 def __str__(self) -> str: 

502 return f"({self.width}, {self.height}, {self.depth})" 

503 

504 

505@export 

506class Segment3D(Generic[Coordinate], metaclass=ExtendedType, slots=True): 

507 """An implementation of a 3D cartesian segment.""" 

508 

509 start: Point3D[Coordinate] #: Start point of a segment. 

510 end: Point3D[Coordinate] #: End point of a segment. 

511 

512 def __init__(self, start: Point3D[Coordinate], end: Point3D[Coordinate], copyPoints: bool = True) -> None: 

513 """ 

514 Initializes a 3-dimensional segment. 

515 

516 :param start: Start point of the segment. 

517 :param end: End point of the segment. 

518 :raises TypeError: If start/end is not of type Point3D. 

519 """ 

520 if not isinstance(start, Point3D): 520 ↛ 521line 520 didn't jump to line 521 because the condition on line 520 was never true

521 ex = TypeError(f"Parameter 'start' is not of type Point3D.") 

522 if version_info >= (3, 11): # pragma: no cover 

523 ex.add_note(f"Got type '{getFullyQualifiedName(start)}'.") 

524 raise ex 

525 if not isinstance(end, Point3D): 525 ↛ 526line 525 didn't jump to line 526 because the condition on line 525 was never true

526 ex = TypeError(f"Parameter 'end' is not of type Point3D.") 

527 if version_info >= (3, 11): # pragma: no cover 

528 ex.add_note(f"Got type '{getFullyQualifiedName(end)}'.") 

529 raise ex 

530 

531 self.start = start.Copy() if copyPoints else start 

532 self.end = end.Copy() if copyPoints else end 

533 

534 

535@export 

536class LineSegment3D(Segment3D[Coordinate], Generic[Coordinate]): 

537 """An implementation of a 3D cartesian line segment.""" 

538 

539 @readonly 

540 def Length(self) -> float: 

541 """ 

542 Read-only property to return the Euclidean distance between start and end point. 

543 

544 :return: Euclidean distance between start and end point 

545 """ 

546 return sqrt((self.end.x - self.start.x) ** 2 + (self.end.y - self.start.y) ** 2 + (self.end.z - self.start.z) ** 2) 

547 

548 def AngleTo(self, other: "LineSegment3D[Coordinate]") -> float: 

549 vectorA = self.ToOffset() 

550 vectorB = other.ToOffset() 

551 scalarProductAB = vectorA.xOffset * vectorB.xOffset + vectorA.yOffset * vectorB.yOffset + vectorA.zOffset * vectorB.zOffset 

552 

553 return acos(scalarProductAB / (abs(self.Length) * abs(other.Length))) 

554 

555 def ToOffset(self) -> Offset3D[Coordinate]: 

556 """ 

557 Convert this 3D line segment to a 3D-offset. 

558 

559 :return: 3D-offset as :class:`Offset3D` 

560 """ 

561 return self.end - self.start 

562 

563 def ToTuple(self) -> Tuple[Tuple[Coordinate, Coordinate, Coordinate], Tuple[Coordinate, Coordinate, Coordinate]]: 

564 """ 

565 Convert this 3D line segment to a simple 2-element tuple of 3D-point tuples. 

566 

567 :return: ``((x1, y1, z1), (x2, y2, z2))`` tuple. 

568 """ 

569 return self.start.ToTuple(), self.end.ToTuple() 

570 

571 def __repr__(self) -> str: 

572 return f"LineSegment3D({self.start}, {self.end})" 

573 

574 def __str__(self) -> str: 

575 return f"({self.start}{self.end})"