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

211 statements  

« prev     ^ index     » next       coverage.py v7.10.3, created at 2025-08-12 20:40 +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 """ 

213 Returns the 3D point's string representation. 

214 

215 :returns: The string representation of the 3D point. 

216 """ 

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

218 

219 def __str__(self) -> str: 

220 """ 

221 Returns the 3D point's string equivalent. 

222 

223 :returns: The string equivalent of the 3D point. 

224 """ 

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

226 

227 

228@export 

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

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

231 

232 def __init__(self) -> None: 

233 """ 

234 Initializes a 3-dimensional origin. 

235 """ 

236 super().__init__(0, 0, 0) 

237 

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

239 """ 

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

241 """ 

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

243 

244 def __repr__(self) -> str: 

245 """ 

246 Returns the 3D origin's string representation. 

247 

248 :returns: The string representation of the 3D origin. 

249 """ 

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

251 

252 

253@export 

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

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

256 

257 xOffset: Coordinate #: The x-direction offset 

258 yOffset: Coordinate #: The y-direction offset 

259 zOffset: Coordinate #: The z-direction offset 

260 

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

262 """ 

263 Initializes a 3-dimensional offset. 

264 

265 :param xOffset: x-direction offset. 

266 :param yOffset: y-direction offset. 

267 :param zOffset: z-direction offset. 

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

269 """ 

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

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

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

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

274 raise ex 

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

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

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

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

279 raise ex 

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

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

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

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

284 raise ex 

285 

286 self.xOffset = xOffset 

287 self.yOffset = yOffset 

288 self.zOffset = zOffset 

289 

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

291 """ 

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

293 

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

295 

296 .. seealso:: 

297 

298 :meth:`+ operator <__add__>` 

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

300 :meth:`- operator <__sub__>` 

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

302 """ 

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

304 

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

306 """ 

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

308 

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

310 """ 

311 return self.xOffset, self.yOffset, self.zOffset 

312 

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

314 """ 

315 Compare two 3D-offsets for equality. 

316 

317 :param other: Parameter to compare against. 

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

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

320 """ 

321 if isinstance(other, Offset3D): 

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

323 elif isinstance(other, tuple): 

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

325 else: 

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

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

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

329 raise ex 

330 

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

332 """ 

333 Compare two 3D-offsets for inequality. 

334 

335 :param other: Parameter to compare against. 

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

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

338 """ 

339 return not self.__eq__(other) 

340 

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

342 """ 

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

344 

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

346 """ 

347 return self.__class__( 

348 -self.xOffset, 

349 -self.yOffset, 

350 -self.zOffset 

351 ) 

352 

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

354 """ 

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

356 

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

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

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

360 """ 

361 if isinstance(other, Offset3D): 

362 return self.__class__( 

363 self.xOffset + other.xOffset, 

364 self.yOffset + other.yOffset, 

365 self.zOffset + other.zOffset 

366 ) 

367 elif isinstance(other, tuple): 

368 return self.__class__( 

369 self.xOffset + other[0], 

370 self.yOffset + other[1], 

371 self.zOffset + other[2] 

372 ) 

373 else: 

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

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

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

377 raise ex 

378 

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

380 """ 

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

382 

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

384 :returns: This 3D-point. 

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

386 """ 

387 if isinstance(other, Offset3D): 

388 self.xOffset += other.xOffset 

389 self.yOffset += other.yOffset 

390 self.zOffset += other.zOffset 

391 elif isinstance(other, tuple): 

392 self.xOffset += other[0] 

393 self.yOffset += other[1] 

394 self.zOffset += other[2] 

395 else: 

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

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

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

399 raise ex 

400 

401 return self 

402 

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

404 """ 

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

406 

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

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

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

410 """ 

411 if isinstance(other, Offset3D): 

412 return self.__class__( 

413 self.xOffset - other.xOffset, 

414 self.yOffset - other.yOffset, 

415 self.zOffset - other.zOffset 

416 ) 

417 elif isinstance(other, tuple): 

418 return self.__class__( 

419 self.xOffset - other[0], 

420 self.yOffset - other[1], 

421 self.zOffset - other[2] 

422 ) 

423 else: 

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

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

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

427 raise ex 

428 

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

430 """ 

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

432 

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

434 :returns: This 3D-point. 

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

436 """ 

437 if isinstance(other, Offset3D): 

438 self.xOffset -= other.xOffset 

439 self.yOffset -= other.yOffset 

440 self.zOffset -= other.zOffset 

441 elif isinstance(other, tuple): 

442 self.xOffset -= other[0] 

443 self.yOffset -= other[1] 

444 self.zOffset -= other[2] 

445 else: 

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

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

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

449 raise ex 

450 

451 return self 

452 

453 def __repr__(self) -> str: 

454 """ 

455 Returns the 3D offset's string representation. 

456 

457 :returns: The string representation of the 3D offset. 

458 """ 

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

460 

461 def __str__(self) -> str: 

462 """ 

463 Returns the 3D offset's string equivalent. 

464 

465 :returns: The string equivalent of the 3D offset. 

466 """ 

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

468 

469 

470@export 

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

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

473 

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

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

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

477 

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

479 """ 

480 Initializes a 2-dimensional size. 

481 

482 :param width: width in x-direction. 

483 :param height: height in y-direction. 

484 :param depth: depth in z-direction. 

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

486 """ 

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

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

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

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

491 raise ex 

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

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

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

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

496 raise ex 

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

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

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

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

501 raise ex 

502 

503 self.width = width 

504 self.height = height 

505 self.depth = depth 

506 

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

508 """ 

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

510 

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

512 """ 

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

514 

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

516 """ 

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

518 

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

520 """ 

521 return self.width, self.height, self.depth 

522 

523 def __repr__(self) -> str: 

524 """ 

525 Returns the 3D size's string representation. 

526 

527 :returns: The string representation of the 3D size. 

528 """ 

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

530 

531 def __str__(self) -> str: 

532 """ 

533 Returns the 3D size's string equivalent. 

534 

535 :returns: The string equivalent of the 3D size. 

536 """ 

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

538 

539 

540@export 

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

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

543 

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

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

546 

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

548 """ 

549 Initializes a 3-dimensional segment. 

550 

551 :param start: Start point of the segment. 

552 :param end: End point of the segment. 

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

554 """ 

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

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

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

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

559 raise ex 

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

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

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

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

564 raise ex 

565 

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

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

568 

569 

570@export 

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

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

573 

574 @readonly 

575 def Length(self) -> float: 

576 """ 

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

578 

579 :return: Euclidean distance between start and end point 

580 """ 

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

582 

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

584 vectorA = self.ToOffset() 

585 vectorB = other.ToOffset() 

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

587 

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

589 

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

591 """ 

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

593 

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

595 """ 

596 return self.end - self.start 

597 

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

599 """ 

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

601 

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

603 """ 

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

605 

606 def __repr__(self) -> str: 

607 """ 

608 Returns the 3D line segment's string representation. 

609 

610 :returns: The string representation of the 3D line segment. 

611 """ 

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

613 

614 def __str__(self) -> str: 

615 """ 

616 Returns the 3D line segment's string equivalent. 

617 

618 :returns: The string equivalent of the 3D line segment. 

619 """ 

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