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

230 statements  

« prev     ^ index     » next       coverage.py v7.11.3, created at 2025-11-14 22:21 +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.""" 

32 

33from math import sqrt, acos 

34from typing import Union, Generic, Any, Tuple 

35 

36try: 

37 from pyTooling.Decorators import readonly, export 

38 from pyTooling.Exceptions import ToolingException 

39 from pyTooling.MetaClasses import ExtendedType 

40 from pyTooling.Common import getFullyQualifiedName 

41 from pyTooling.Cartesian2D import Coordinate 

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

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

44 

45 try: 

46 from Decorators import readonly, export 

47 from Exceptions import ToolingException 

48 from MetaClasses import ExtendedType 

49 from Common import getFullyQualifiedName 

50 from Cartesian2D import Coordinate 

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

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

53 raise ex 

54 

55 

56@export 

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

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

59 

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

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

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

63 

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

65 """ 

66 Initializes a 3-dimensional point. 

67 

68 :param x: X-coordinate. 

69 :param y: Y-coordinate. 

70 :param z: Z-coordinate. 

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

72 """ 

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

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

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

76 raise ex 

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

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

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

80 raise ex 

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

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

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

84 raise ex 

85 

86 self.x = x 

87 self.y = y 

88 self.z = z 

89 

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

91 """ 

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

93 

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

95 

96 .. seealso:: 

97 

98 :meth:`+ operator <__add__>` 

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

100 :meth:`- operator <__sub__>` 

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

102 """ 

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

104 

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

106 """ 

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

108 

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

110 """ 

111 return self.x, self.y, self.z 

112 

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

114 """ 

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

116 

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

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

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

120 """ 

121 if isinstance(other, Offset3D): 

122 return self.__class__( 

123 self.x + other.xOffset, 

124 self.y + other.yOffset, 

125 self.z + other.zOffset 

126 ) 

127 elif isinstance(other, tuple): 

128 return self.__class__( 

129 self.x + other[0], 

130 self.y + other[1], 

131 self.z + other[2] 

132 ) 

133 else: 

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

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

136 raise ex 

137 

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

139 """ 

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

141 

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

143 :return: This 3D-point. 

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

145 """ 

146 if isinstance(other, Offset3D): 

147 self.x += other.xOffset 

148 self.y += other.yOffset 

149 self.z += other.zOffset 

150 elif isinstance(other, tuple): 

151 self.x += other[0] 

152 self.y += other[1] 

153 self.z += other[2] 

154 else: 

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

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

157 raise ex 

158 

159 return self 

160 

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

162 """ 

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

164 

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

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

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

168 """ 

169 if isinstance(other, Point3D): 

170 return Offset3D( 

171 self.x - other.x, 

172 self.y - other.y, 

173 self.z - other.z 

174 ) 

175 else: 

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

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

178 raise ex 

179 

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

181 """ 

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

183 

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

185 :return: This 3D-point. 

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

187 """ 

188 if isinstance(other, Offset3D): 

189 self.x -= other.xOffset 

190 self.y -= other.yOffset 

191 self.z -= other.zOffset 

192 elif isinstance(other, tuple): 

193 self.x -= other[0] 

194 self.y -= other[1] 

195 self.z -= other[2] 

196 else: 

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

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

199 raise ex 

200 

201 return self 

202 

203 def __repr__(self) -> str: 

204 """ 

205 Returns the 3D point's string representation. 

206 

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

208 """ 

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

210 

211 def __str__(self) -> str: 

212 """ 

213 Returns the 3D point's string equivalent. 

214 

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

216 """ 

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

218 

219 

220@export 

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

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

223 

224 def __init__(self) -> None: 

225 """ 

226 Initializes a 3-dimensional origin. 

227 """ 

228 super().__init__(0, 0, 0) 

229 

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

231 """ 

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

233 """ 

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

235 

236 def __repr__(self) -> str: 

237 """ 

238 Returns the 3D origin's string representation. 

239 

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

241 """ 

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

243 

244 

245@export 

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

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

248 

249 xOffset: Coordinate #: The x-direction offset 

250 yOffset: Coordinate #: The y-direction offset 

251 zOffset: Coordinate #: The z-direction offset 

252 

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

254 """ 

255 Initializes a 3-dimensional offset. 

256 

257 :param xOffset: x-direction offset. 

258 :param yOffset: y-direction offset. 

259 :param zOffset: z-direction offset. 

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

261 """ 

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

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

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

265 raise ex 

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

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

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

269 raise ex 

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

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

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

273 raise ex 

274 

275 self.xOffset = xOffset 

276 self.yOffset = yOffset 

277 self.zOffset = zOffset 

278 

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

280 """ 

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

282 

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

284 

285 .. seealso:: 

286 

287 :meth:`+ operator <__add__>` 

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

289 :meth:`- operator <__sub__>` 

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

291 """ 

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

293 

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

295 """ 

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

297 

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

299 """ 

300 return self.xOffset, self.yOffset, self.zOffset 

301 

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

303 """ 

304 Compare two 3D-offsets for equality. 

305 

306 :param other: Parameter to compare against. 

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

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

309 """ 

310 if isinstance(other, Offset3D): 

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

312 elif isinstance(other, tuple): 

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

314 else: 

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

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

317 raise ex 

318 

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

320 """ 

321 Compare two 3D-offsets for inequality. 

322 

323 :param other: Parameter to compare against. 

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

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

326 """ 

327 return not self.__eq__(other) 

328 

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

330 """ 

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

332 

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

334 """ 

335 return self.__class__( 

336 -self.xOffset, 

337 -self.yOffset, 

338 -self.zOffset 

339 ) 

340 

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

342 """ 

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

344 

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

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

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

348 """ 

349 if isinstance(other, Offset3D): 

350 return self.__class__( 

351 self.xOffset + other.xOffset, 

352 self.yOffset + other.yOffset, 

353 self.zOffset + other.zOffset 

354 ) 

355 elif isinstance(other, tuple): 

356 return self.__class__( 

357 self.xOffset + other[0], 

358 self.yOffset + other[1], 

359 self.zOffset + other[2] 

360 ) 

361 else: 

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

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

364 raise ex 

365 

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

367 """ 

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

369 

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

371 :returns: This 3D-point. 

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

373 """ 

374 if isinstance(other, Offset3D): 

375 self.xOffset += other.xOffset 

376 self.yOffset += other.yOffset 

377 self.zOffset += other.zOffset 

378 elif isinstance(other, tuple): 

379 self.xOffset += other[0] 

380 self.yOffset += other[1] 

381 self.zOffset += other[2] 

382 else: 

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

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

385 raise ex 

386 

387 return self 

388 

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

390 """ 

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

392 

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

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

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

396 """ 

397 if isinstance(other, Offset3D): 

398 return self.__class__( 

399 self.xOffset - other.xOffset, 

400 self.yOffset - other.yOffset, 

401 self.zOffset - other.zOffset 

402 ) 

403 elif isinstance(other, tuple): 

404 return self.__class__( 

405 self.xOffset - other[0], 

406 self.yOffset - other[1], 

407 self.zOffset - other[2] 

408 ) 

409 else: 

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

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 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.") 

433 raise ex 

434 

435 return self 

436 

437 def __repr__(self) -> str: 

438 """ 

439 Returns the 3D offset's string representation. 

440 

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

442 """ 

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

444 

445 def __str__(self) -> str: 

446 """ 

447 Returns the 3D offset's string equivalent. 

448 

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

450 """ 

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

452 

453 

454@export 

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

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

457 

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

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

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

461 

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

463 """ 

464 Initializes a 2-dimensional size. 

465 

466 :param width: width in x-direction. 

467 :param height: height in y-direction. 

468 :param depth: depth in z-direction. 

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

470 """ 

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

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

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

474 raise ex 

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

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

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

478 raise ex 

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

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

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

482 raise ex 

483 

484 self.width = width 

485 self.height = height 

486 self.depth = depth 

487 

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

489 """ 

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

491 

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

493 """ 

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

495 

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

497 """ 

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

499 

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

501 """ 

502 return self.width, self.height, self.depth 

503 

504 def __repr__(self) -> str: 

505 """ 

506 Returns the 3D size's string representation. 

507 

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

509 """ 

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

511 

512 def __str__(self) -> str: 

513 """ 

514 Returns the 3D size's string equivalent. 

515 

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

517 """ 

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

519 

520 

521@export 

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

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

524 

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

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

527 

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

529 """ 

530 Initializes a 3-dimensional segment. 

531 

532 :param start: Start point of the segment. 

533 :param end: End point of the segment. 

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

535 """ 

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

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

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

539 raise ex 

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

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

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

543 raise ex 

544 

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

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

547 

548 

549@export 

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

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

552 

553 @readonly 

554 def Length(self) -> float: 

555 """ 

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

557 

558 :return: Euclidean distance between start and end point 

559 """ 

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

561 

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

563 vectorA = self.ToOffset() 

564 vectorB = other.ToOffset() 

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

566 

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

568 

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

570 """ 

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

572 

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

574 """ 

575 return self.end - self.start 

576 

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

578 """ 

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

580 

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

582 """ 

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

584 

585 def __repr__(self) -> str: 

586 """ 

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

588 

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

590 """ 

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

592 

593 def __str__(self) -> str: 

594 """ 

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

596 

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

598 """ 

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