navaltoolbox 0.3.0__tar.gz → 0.4.0__tar.gz

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 (46) hide show
  1. {navaltoolbox-0.3.0 → navaltoolbox-0.4.0}/PKG-INFO +1 -1
  2. {navaltoolbox-0.3.0 → navaltoolbox-0.4.0}/navaltoolbox/__init__.py +6 -0
  3. {navaltoolbox-0.3.0 → navaltoolbox-0.4.0}/navaltoolbox/navaltoolbox.pyi +239 -1
  4. {navaltoolbox-0.3.0 → navaltoolbox-0.4.0}/navaltoolbox/visualization.py +226 -18
  5. {navaltoolbox-0.3.0 → navaltoolbox-0.4.0}/rust/Cargo.lock +1 -1
  6. {navaltoolbox-0.3.0 → navaltoolbox-0.4.0}/rust/Cargo.toml +1 -3
  7. {navaltoolbox-0.3.0 → navaltoolbox-0.4.0}/rust/README.md +1 -1
  8. navaltoolbox-0.4.0/rust/src/appendage.rs +449 -0
  9. navaltoolbox-0.4.0/rust/src/deckedge.rs +350 -0
  10. {navaltoolbox-0.3.0 → navaltoolbox-0.4.0}/rust/src/downflooding/mod.rs +5 -0
  11. {navaltoolbox-0.3.0 → navaltoolbox-0.4.0}/rust/src/hydrostatics/calculator.rs +77 -0
  12. {navaltoolbox-0.3.0 → navaltoolbox-0.4.0}/rust/src/hydrostatics/dataclasses.rs +8 -0
  13. {navaltoolbox-0.3.0 → navaltoolbox-0.4.0}/rust/src/lib.rs +4 -0
  14. {navaltoolbox-0.3.0 → navaltoolbox-0.4.0}/rust/src/python.rs +384 -3
  15. {navaltoolbox-0.3.0 → navaltoolbox-0.4.0}/rust/src/scripting/engine.rs +110 -35
  16. {navaltoolbox-0.3.0 → navaltoolbox-0.4.0}/rust/src/silhouette/core.rs +5 -0
  17. {navaltoolbox-0.3.0 → navaltoolbox-0.4.0}/rust/src/tanks/tank.rs +10 -0
  18. {navaltoolbox-0.3.0 → navaltoolbox-0.4.0}/rust/src/vessel.rs +159 -0
  19. {navaltoolbox-0.3.0 → navaltoolbox-0.4.0}/rust/tests/integration_tests.rs +2 -2
  20. {navaltoolbox-0.3.0 → navaltoolbox-0.4.0}/README.md +0 -0
  21. {navaltoolbox-0.3.0 → navaltoolbox-0.4.0}/navaltoolbox/plotting.py +0 -0
  22. {navaltoolbox-0.3.0 → navaltoolbox-0.4.0}/navaltoolbox/py.typed +0 -0
  23. {navaltoolbox-0.3.0 → navaltoolbox-0.4.0}/pyproject.toml +0 -0
  24. {navaltoolbox-0.3.0 → navaltoolbox-0.4.0}/rust/src/downflooding/loader.rs +0 -0
  25. {navaltoolbox-0.3.0 → navaltoolbox-0.4.0}/rust/src/hull.rs +0 -0
  26. {navaltoolbox-0.3.0 → navaltoolbox-0.4.0}/rust/src/hydrostatics/mod.rs +0 -0
  27. {navaltoolbox-0.3.0 → navaltoolbox-0.4.0}/rust/src/hydrostatics/waterplane.rs +0 -0
  28. {navaltoolbox-0.3.0 → navaltoolbox-0.4.0}/rust/src/mesh/clipper.rs +0 -0
  29. {navaltoolbox-0.3.0 → navaltoolbox-0.4.0}/rust/src/mesh/loader.rs +0 -0
  30. {navaltoolbox-0.3.0 → navaltoolbox-0.4.0}/rust/src/mesh/mod.rs +0 -0
  31. {navaltoolbox-0.3.0 → navaltoolbox-0.4.0}/rust/src/mesh/transform.rs +0 -0
  32. {navaltoolbox-0.3.0 → navaltoolbox-0.4.0}/rust/src/scripting/context.rs +0 -0
  33. {navaltoolbox-0.3.0 → navaltoolbox-0.4.0}/rust/src/scripting/mod.rs +0 -0
  34. {navaltoolbox-0.3.0 → navaltoolbox-0.4.0}/rust/src/scripting/result.rs +0 -0
  35. {navaltoolbox-0.3.0 → navaltoolbox-0.4.0}/rust/src/silhouette/loader.rs +0 -0
  36. {navaltoolbox-0.3.0 → navaltoolbox-0.4.0}/rust/src/silhouette/mod.rs +0 -0
  37. {navaltoolbox-0.3.0 → navaltoolbox-0.4.0}/rust/src/stability/calculator.rs +0 -0
  38. {navaltoolbox-0.3.0 → navaltoolbox-0.4.0}/rust/src/stability/complete.rs +0 -0
  39. {navaltoolbox-0.3.0 → navaltoolbox-0.4.0}/rust/src/stability/dataclasses.rs +0 -0
  40. {navaltoolbox-0.3.0 → navaltoolbox-0.4.0}/rust/src/stability/mod.rs +0 -0
  41. {navaltoolbox-0.3.0 → navaltoolbox-0.4.0}/rust/src/tanks/dataclasses.rs +0 -0
  42. {navaltoolbox-0.3.0 → navaltoolbox-0.4.0}/rust/src/tanks/mod.rs +0 -0
  43. {navaltoolbox-0.3.0 → navaltoolbox-0.4.0}/rust/tests/data/box_10x10.stl +0 -0
  44. {navaltoolbox-0.3.0 → navaltoolbox-0.4.0}/rust/tests/data/dtmb5415.stl +0 -0
  45. {navaltoolbox-0.3.0 → navaltoolbox-0.4.0}/rust/tests/dtmb_validation.rs +0 -0
  46. {navaltoolbox-0.3.0 → navaltoolbox-0.4.0}/rust/tests/extended_hydrostatics.rs +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: navaltoolbox
3
- Version: 0.3.0
3
+ Version: 0.4.0
4
4
  Classifier: Development Status :: 3 - Alpha
5
5
  Classifier: Intended Audience :: Science/Research
6
6
  Classifier: License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)
@@ -58,6 +58,9 @@ from .navaltoolbox import (
58
58
  CriteriaResult,
59
59
  CriteriaContext,
60
60
  ScriptEngine,
61
+ Appendage,
62
+ DeckEdge,
63
+ DeckEdgeSide,
61
64
  )
62
65
 
63
66
  __all__ = [
@@ -77,4 +80,7 @@ __all__ = [
77
80
  "CriteriaResult",
78
81
  "CriteriaContext",
79
82
  "ScriptEngine",
83
+ "Appendage",
84
+ "DeckEdge",
85
+ "DeckEdgeSide",
80
86
  ]
@@ -27,6 +27,9 @@ __all__ = [
27
27
  "Hull",
28
28
  "Vessel",
29
29
  "Silhouette",
30
+ "Appendage",
31
+ "DeckEdge",
32
+ "DeckEdgeSide",
30
33
  "OpeningType",
31
34
  "DownfloodingOpening",
32
35
  "HydrostaticState",
@@ -342,6 +345,58 @@ class Vessel:
342
345
  List of DownfloodingOpening objects.
343
346
  """
344
347
  ...
348
+
349
+ # Appendage methods
350
+
351
+ def add_appendage(self, appendage: "Appendage") -> None:
352
+ """Add an appendage to the vessel."""
353
+ ...
354
+
355
+ def num_appendages(self) -> int:
356
+ """Returns the number of appendages."""
357
+ ...
358
+
359
+ def clear_appendages(self) -> None:
360
+ """Removes all appendages."""
361
+ ...
362
+
363
+ def get_appendages(self) -> List["Appendage"]:
364
+ """Get all appendages."""
365
+ ...
366
+
367
+ def get_total_appendage_volume(self) -> float:
368
+ """Returns the total appendage volume in m³."""
369
+ ...
370
+
371
+ def get_total_appendage_wetted_surface(self) -> float:
372
+ """Returns the total appendage wetted surface in m²."""
373
+ ...
374
+
375
+ # Deck Edge methods
376
+
377
+ def add_deck_edge(self, deck_edge: "DeckEdge") -> None:
378
+ """Add a deck edge to the vessel."""
379
+ ...
380
+
381
+ def num_deck_edges(self) -> int:
382
+ """Returns the number of deck edges."""
383
+ ...
384
+
385
+ def has_deck_edges(self) -> bool:
386
+ """Returns true if any deck edges are defined."""
387
+ ...
388
+
389
+ def clear_deck_edges(self) -> None:
390
+ """Removes all deck edges."""
391
+ ...
392
+
393
+ def get_deck_edges(self) -> List["DeckEdge"]:
394
+ """Get all deck edges."""
395
+ ...
396
+
397
+ def get_min_freeboard(self, heel: float, trim: float, waterline_z: float) -> float | None:
398
+ """Calculate minimum freeboard across all deck edges."""
399
+ ...
345
400
 
346
401
 
347
402
  class Silhouette:
@@ -426,6 +481,137 @@ class Silhouette:
426
481
  ...
427
482
 
428
483
 
484
+
485
+ class Appendage:
486
+ """An appendage (additional volume element) attached to the vessel.
487
+
488
+ Appendages represent volume contributions from items like keels, rudders,
489
+ bulbous bows, etc. that are not part of the main hull geometry.
490
+ """
491
+
492
+ @staticmethod
493
+ def from_point(name: str, center: Tuple[float, float, float], volume: float) -> "Appendage":
494
+ """Create an appendage from a point (fixed volume at position)."""
495
+ ...
496
+
497
+ @staticmethod
498
+ def from_file(name: str, file_path: str) -> "Appendage":
499
+ """Create an appendage from an STL or VTK file."""
500
+ ...
501
+
502
+ @staticmethod
503
+ def from_box(name: str, xmin: float, xmax: float, ymin: float, ymax: float, zmin: float, zmax: float) -> "Appendage":
504
+ """Create an appendage from a box (parallelepiped)."""
505
+ ...
506
+
507
+ @staticmethod
508
+ def from_cube(name: str, center: Tuple[float, float, float], volume: float) -> "Appendage":
509
+ """Create an appendage from a cube (center and volume)."""
510
+ ...
511
+
512
+ @staticmethod
513
+ def from_sphere(name: str, center: Tuple[float, float, float], volume: float) -> "Appendage":
514
+ """Create an appendage from a sphere (center and volume)."""
515
+ ...
516
+
517
+ @property
518
+ def name(self) -> str:
519
+ """The appendage name."""
520
+ ...
521
+
522
+ @name.setter
523
+ def name(self, value: str) -> None:
524
+ ...
525
+
526
+ @property
527
+ def volume(self) -> float:
528
+ """Volume in m³."""
529
+ ...
530
+
531
+ @property
532
+ def center(self) -> Tuple[float, float, float]:
533
+ """Center of volume (x, y, z) in meters."""
534
+ ...
535
+
536
+ @property
537
+ def wetted_surface(self) -> float | None:
538
+ """Wetted surface area in m², or None if not set."""
539
+ ...
540
+
541
+ @wetted_surface.setter
542
+ def wetted_surface(self, value: float | None) -> None:
543
+ ...
544
+
545
+ def geometry_type(self) -> str:
546
+ """Returns the geometry type (Point, Mesh, Box, etc.)."""
547
+ ...
548
+
549
+ def get_mesh_data(self) -> Tuple[List[Tuple[float, float, float]], List[Tuple[int, int, int]]] | None:
550
+ """Returns mesh data (vertices, faces) if geometry is a mesh.
551
+
552
+ Returns:
553
+ Tuple of (vertices, faces), or None if not a mesh.
554
+ """
555
+ ...
556
+
557
+ @property
558
+ def bounds(self) -> Tuple[float, float, float, float, float, float] | None:
559
+ """Returns bounds (xmin, xmax, ymin, ymax, zmin, zmax)."""
560
+ ...
561
+
562
+
563
+ class DeckEdgeSide:
564
+ """Side of the deck edge (Port, Starboard, or Both)."""
565
+
566
+ @staticmethod
567
+ def port() -> "DeckEdgeSide": ...
568
+
569
+ @staticmethod
570
+ def starboard() -> "DeckEdgeSide": ...
571
+
572
+ @staticmethod
573
+ def both() -> "DeckEdgeSide": ...
574
+
575
+
576
+ class DeckEdge:
577
+ """A deck edge contour (livet) for freeboard calculation."""
578
+
579
+ @staticmethod
580
+ def from_points(name: str, points: List[Tuple[float, float, float]], side: DeckEdgeSide) -> "DeckEdge":
581
+ """Create a deck edge from a list of 3D points."""
582
+ ...
583
+
584
+ @staticmethod
585
+ def from_file(name: str, file_path: str) -> "DeckEdge":
586
+ """Load a deck edge from a DXF or VTK file."""
587
+ ...
588
+
589
+ @property
590
+ def name(self) -> str:
591
+ """The deck edge name."""
592
+ ...
593
+
594
+ @name.setter
595
+ def name(self, value: str) -> None:
596
+ ...
597
+
598
+ def num_points(self) -> int:
599
+ """Returns the number of points."""
600
+ ...
601
+
602
+ def get_points(self) -> List[Tuple[float, float, float]]:
603
+ """Returns points as list of (x, y, z) tuples."""
604
+ ...
605
+
606
+ def get_side(self) -> str:
607
+ """Returns the side as a string."""
608
+ ...
609
+
610
+ def get_freeboard(self, heel: float, trim: float, pivot: Tuple[float, float, float], waterline_z: float) -> float:
611
+ """Calculate freeboard at given conditions."""
612
+ ...
613
+
614
+
429
615
  class OpeningType:
430
616
  """Type of opening that can cause downflooding.
431
617
 
@@ -635,6 +821,8 @@ class HydrostaticState:
635
821
  free_surface_correction_t: Transverse FSC in meters.
636
822
  free_surface_correction_l: Longitudinal FSC in meters.
637
823
  stiffness_matrix: 6x6 hydrostatic stiffness matrix (flattened).
824
+ sectional_areas: List of (x, area) pairs representing the sectional area curve.
825
+ freeboard: Minimum freeboard in meters, or None.
638
826
  """
639
827
 
640
828
  draft: float
@@ -646,6 +834,9 @@ class HydrostaticState:
646
834
  volume: float
647
835
  displacement: float
648
836
 
837
+ sectional_areas: List[Tuple[float, float]]
838
+ freeboard: float | None
839
+
649
840
  @property
650
841
  def cob(self) -> Tuple[float, float, float]:
651
842
  """Center of buoyancy (lcb, tcb, vcb) in meters."""
@@ -753,6 +944,7 @@ class HydrostaticsCalculator:
753
944
  trim: float = 0.0,
754
945
  heel: float = 0.0,
755
946
  vcg: float | None = None,
947
+ num_stations: int | None = None,
756
948
  ) -> HydrostaticState:
757
949
  """Calculate hydrostatics at a given draft, trim, and heel.
758
950
 
@@ -1274,7 +1466,53 @@ class CriteriaResult:
1274
1466
 
1275
1467
 
1276
1468
  class CriteriaContext:
1277
- """Context for Rhai scripts, wrapping stability results."""
1469
+ """Context for Rhai scripts, wrapping stability results.
1470
+
1471
+ This context provides access to stability data within Rhai scripts.
1472
+ The following methods are available when writing Rhai scripts:
1473
+
1474
+ **GZ Curve Methods:**
1475
+ - `get_heels()` -> list of heel angles
1476
+ - `get_gz_values()` -> list of GZ values
1477
+ - `area_under_curve(from, to)` -> area in m·rad
1478
+ - `gz_at_angle(angle)` -> GZ value at angle
1479
+ - `find_max_gz()` -> map with `angle` and `value`
1480
+ - `find_angle_of_vanishing_stability()` -> angle or ()
1481
+ - `get_first_flooding_angle()` -> angle or ()
1482
+ - `find_equilibrium_angle(heeling_arm)` -> angle or ()
1483
+ - `find_second_intercept(heeling_arm)` -> angle or ()
1484
+ - `get_limiting_angle(default)` -> limiting angle
1485
+
1486
+ **Hydrostatic Properties:**
1487
+ - `get_gm0()` -> initial GM in meters
1488
+ - `get_gm0_dry()` -> GM without FSC
1489
+ - `get_draft()` -> draft in meters
1490
+ - `get_trim()` -> trim angle in degrees
1491
+ - `get_displacement()` -> displacement in kg
1492
+ - `get_cog()` -> [x, y, z] center of gravity
1493
+
1494
+ **Form Coefficients:**
1495
+ - `get_cb()` -> block coefficient
1496
+ - `get_cm()` -> midship coefficient
1497
+ - `get_cp()` -> prismatic coefficient
1498
+ - `get_lwl()` -> waterline length
1499
+ - `get_bwl()` -> waterline breadth
1500
+ - `get_vcb()` -> vertical center of buoyancy
1501
+
1502
+ **Wind Data:**
1503
+ - `has_wind_data()` -> bool
1504
+ - `get_emerged_area()` -> area in m²
1505
+ - `get_wind_lever_arm()` -> arm in meters
1506
+ - `calculate_wind_heeling_lever(pressure)` -> lever at given pressure
1507
+
1508
+ **Parameters:**
1509
+ - `get_param(key)` -> value or ()
1510
+ - `has_param(key)` -> bool
1511
+
1512
+ **Metadata:**
1513
+ - `get_vessel_name()` -> string
1514
+ - `get_loading_condition()` -> string
1515
+ """
1278
1516
 
1279
1517
  @staticmethod
1280
1518
  def from_result(
@@ -24,8 +24,11 @@ def plot_vessel_3d(
24
24
  show_tanks: bool = True,
25
25
  show_silhouettes: bool = True,
26
26
  show_openings: bool = True,
27
+ show_appendages: bool = True,
28
+ show_deck_edges: bool = True,
27
29
  opacity_hull: float = 0.5,
28
30
  opacity_tank: float = 0.3,
31
+ opacity_appendage: float = 0.5,
29
32
  title: str = "Vessel Visualization",
30
33
  enable_opacity_slider: bool = True,
31
34
  show_axes: bool = True,
@@ -39,8 +42,11 @@ def plot_vessel_3d(
39
42
  show_tanks: Whether to show tank meshes.
40
43
  show_silhouettes: Whether to show silhouette profiles.
41
44
  show_openings: Whether to show downflooding openings.
45
+ show_appendages: Whether to show appendages.
46
+ show_deck_edges: Whether to show deck edges.
42
47
  opacity_hull: Opacity for hull meshes (0.0 to 1.0).
43
48
  opacity_tank: Opacity for tank meshes (0.0 to 1.0).
49
+ opacity_appendage: Opacity for appendage meshes (0.0 to 1.0).
44
50
  title: Title of the plot.
45
51
  enable_opacity_slider: Whether to add a slider to control hull opacity.
46
52
  show_axes: Whether to show axes, grid, and background.
@@ -183,6 +189,40 @@ def plot_vessel_3d(
183
189
  )
184
190
  )
185
191
 
192
+ # 5. Appendages
193
+ if show_appendages:
194
+ for app in vessel.get_appendages():
195
+ _add_appendage_trace(fig, app, opacity_appendage)
196
+
197
+ # 6. Deck Edges
198
+ if show_deck_edges:
199
+ for edge in vessel.get_deck_edges():
200
+ points = edge.get_points()
201
+ if not points:
202
+ continue
203
+
204
+ x, y, z = zip(*points)
205
+ side = edge.get_side()
206
+
207
+ # Determine color based on side
208
+ color = "orange" # Default/Both
209
+ if "Port" in side:
210
+ color = "red"
211
+ elif "Starboard" in side:
212
+ color = "green"
213
+
214
+ fig.add_trace(
215
+ go.Scatter3d(
216
+ x=x,
217
+ y=y,
218
+ z=z,
219
+ mode="lines+markers",
220
+ line=dict(color=color, width=4),
221
+ marker=dict(size=3, color=color),
222
+ name=f"Deck Edge: {edge.name} ({side})",
223
+ )
224
+ )
225
+
186
226
  # 5. Reference Points (AP/FP)
187
227
  # Using Y=0, Z=0 for reference
188
228
  ap_x = vessel.ap
@@ -326,8 +366,11 @@ def plot_hydrostatic_condition(
326
366
  show_tanks: bool = True,
327
367
  show_silhouettes: bool = True,
328
368
  show_openings: bool = True,
369
+ show_appendages: bool = True,
370
+ show_deck_edges: bool = True,
329
371
  opacity_hull: float = 0.5,
330
372
  opacity_tank: float = 0.3,
373
+ opacity_appendage: float = 0.5,
331
374
  title: str = "Hydrostatic Condition",
332
375
  enable_opacity_slider: bool = True,
333
376
  cog: Optional[Tuple[float, float, float]] = None,
@@ -348,8 +391,11 @@ def plot_hydrostatic_condition(
348
391
  show_tanks: Whether to show tank meshes.
349
392
  show_silhouettes: Whether to show silhouette profiles.
350
393
  show_openings: Whether to show downflooding openings.
394
+ show_appendages: Whether to show appendages.
395
+ show_deck_edges: Whether to show deck edges.
351
396
  opacity_hull: Opacity for hull meshes (0.0 to 1.0).
352
397
  opacity_tank: Opacity for tank meshes (0.0 to 1.0).
398
+ opacity_appendage: Opacity for appendage meshes (0.0 to 1.0).
353
399
  title: Title of the plot.
354
400
  enable_opacity_slider: Whether to add a slider to control hull opacity.
355
401
  cog: Optional Center of Gravity (x, y, z) to display.
@@ -599,7 +645,103 @@ def plot_hydrostatic_condition(
599
645
  )
600
646
  )
601
647
 
602
- # 5. Reference Points
648
+ # 5. Appendages
649
+ if show_appendages:
650
+ for app in vessel.get_appendages():
651
+ geo_type = app.geometry_type()
652
+
653
+ if geo_type == "Mesh":
654
+ mesh_data = app.get_mesh_data()
655
+ if mesh_data:
656
+ verts, faces = mesh_data
657
+ if verts and faces:
658
+ t_verts = transform_points(verts)
659
+ x, y, z = t_verts[:, 0], t_verts[:, 1], t_verts[:, 2]
660
+ i_idx, j_idx, k_idx = zip(*faces)
661
+
662
+ fig.add_trace(
663
+ go.Mesh3d(
664
+ x=x, y=y, z=z, i=i_idx, j=j_idx, k=k_idx,
665
+ color="darkkhaki",
666
+ opacity=opacity_appendage,
667
+ name=f"{app.name} (Appendage)",
668
+ showscale=False,
669
+ flatshading=True,
670
+ )
671
+ )
672
+ elif geo_type == "Box" or geo_type == "Cube":
673
+ bounds = app.bounds
674
+ if bounds:
675
+ xmin, xmax, ymin, ymax, zmin, zmax = bounds
676
+ # Create a box mesh
677
+ box_verts = [
678
+ (xmin, ymin, zmin), (xmax, ymin, zmin),
679
+ (xmax, ymax, zmin), (xmin, ymax, zmin),
680
+ (xmin, ymin, zmax), (xmax, ymin, zmax),
681
+ (xmax, ymax, zmax), (xmin, ymax, zmax)
682
+ ]
683
+ t_verts = transform_points(box_verts)
684
+ x, y, z = t_verts[:, 0], t_verts[:, 1], t_verts[:, 2]
685
+
686
+ i = [7, 0, 0, 0, 4, 4, 6, 6, 4, 0, 3, 2]
687
+ j = [3, 4, 1, 2, 5, 6, 5, 2, 0, 1, 6, 3]
688
+ k = [0, 7, 2, 3, 6, 7, 1, 1, 5, 5, 7, 6]
689
+
690
+ fig.add_trace(
691
+ go.Mesh3d(
692
+ x=x, y=y, z=z, i=i, j=j, k=k,
693
+ color="goldenrod",
694
+ opacity=opacity_appendage,
695
+ name=f"{app.name} (Appendage)",
696
+ showscale=False,
697
+ )
698
+ )
699
+ elif geo_type == "Sphere" or geo_type == "Point":
700
+ center = app.center
701
+ volume = app.volume
702
+ t_center = transform_points([center])[0]
703
+
704
+ fig.add_trace(
705
+ go.Scatter3d(
706
+ x=[t_center[0]],
707
+ y=[t_center[1]],
708
+ z=[t_center[2]],
709
+ mode="markers",
710
+ marker=dict(
711
+ size=10, color="goldenrod", symbol="circle"),
712
+ name=f"{app.name} (Appendage)",
713
+ text=f"Vol: {volume:.3f}m³",
714
+ )
715
+ )
716
+
717
+ # 6. Deck Edges
718
+ if show_deck_edges:
719
+ for edge in vessel.get_deck_edges():
720
+ points = edge.get_points()
721
+ if not points:
722
+ continue
723
+
724
+ t_points = transform_points(points)
725
+ x, y, z = t_points[:, 0], t_points[:, 1], t_points[:, 2]
726
+ side = edge.get_side()
727
+
728
+ color = "orange"
729
+ if "Port" in side:
730
+ color = "red"
731
+ elif "Starboard" in side:
732
+ color = "green"
733
+
734
+ fig.add_trace(
735
+ go.Scatter3d(
736
+ x=x, y=y, z=z,
737
+ mode="lines+markers",
738
+ line=dict(color=color, width=4),
739
+ marker=dict(size=3, color=color),
740
+ name=f"Deck Edge: {edge.name} ({side})",
741
+ )
742
+ )
743
+
744
+ # 7. Reference Points
603
745
  ap_pt = transform_points([(vessel.ap, 0, 0)])[0]
604
746
  fp_pt = transform_points([(vessel.fp, 0, 0)])[0]
605
747
 
@@ -615,7 +757,7 @@ def plot_hydrostatic_condition(
615
757
  )
616
758
  )
617
759
 
618
- # 6. Center of Gravity
760
+ # 8. Center of Gravity
619
761
  if cog:
620
762
  cog_pt = transform_points([cog])[0]
621
763
  fig.add_trace(
@@ -801,25 +943,91 @@ def _add_hull_trace(fig: go.Figure, hull: Hull, name: str, opacity: float):
801
943
  if hasattr(hull, "get_vertices") and hasattr(hull, "get_faces"):
802
944
  verts = hull.get_vertices()
803
945
  faces = hull.get_faces()
946
+ if verts and faces:
947
+ x, y, z = zip(*verts)
948
+ i_idx, j_idx, k_idx = zip(*faces)
804
949
 
805
- if not verts or not faces:
806
- return
950
+ fig.add_trace(
951
+ go.Mesh3d(
952
+ x=x,
953
+ y=y,
954
+ z=z,
955
+ i=i_idx,
956
+ j=j_idx,
957
+ k=k_idx,
958
+ color="gray",
959
+ opacity=opacity,
960
+ name=name,
961
+ showscale=False,
962
+ flatshading=True,
963
+ )
964
+ )
965
+
966
+
967
+ def _add_appendage_trace(fig: go.Figure, app: Any, opacity: float):
968
+ """Helper to add appendage to figure."""
969
+ geo_type = app.geometry_type()
970
+
971
+ if geo_type == "Mesh":
972
+ mesh_data = app.get_mesh_data()
973
+ if mesh_data:
974
+ verts, faces = mesh_data
975
+ if verts and faces:
976
+ x, y, z = zip(*verts)
977
+ i_idx, j_idx, k_idx = zip(*faces)
978
+
979
+ fig.add_trace(
980
+ go.Mesh3d(
981
+ x=x,
982
+ y=y,
983
+ z=z,
984
+ i=i_idx,
985
+ j=j_idx,
986
+ k=k_idx,
987
+ color="darkkhaki",
988
+ opacity=opacity,
989
+ name=f"{app.name} (Appendage)",
990
+ showscale=False,
991
+ flatshading=True,
992
+ )
993
+ )
994
+ elif geo_type == "Box" or geo_type == "Cube":
995
+ bounds = app.bounds
996
+ if bounds:
997
+ xmin, xmax, ymin, ymax, zmin, zmax = bounds
807
998
 
808
- x, y, z = zip(*verts)
809
- i, j, k = zip(*faces)
999
+ # Create a box mesh
1000
+ x = [xmin, xmin, xmax, xmax, xmin, xmin, xmax, xmax]
1001
+ y = [ymin, ymax, ymax, ymin, ymin, ymax, ymax, ymin]
1002
+ z = [zmin, zmin, zmin, zmin, zmax, zmax, zmax, zmax]
810
1003
 
1004
+ i = [7, 0, 0, 0, 4, 4, 6, 6, 4, 0, 3, 2]
1005
+ j = [3, 4, 1, 2, 5, 6, 5, 2, 0, 1, 6, 3]
1006
+ k = [0, 7, 2, 3, 6, 7, 1, 1, 5, 5, 7, 6]
1007
+
1008
+ fig.add_trace(
1009
+ go.Mesh3d(
1010
+ x=x, y=y, z=z, i=i, j=j, k=k,
1011
+ color="goldenrod",
1012
+ opacity=opacity,
1013
+ name=f"{app.name} (Appendage)",
1014
+ showscale=False,
1015
+ )
1016
+ )
1017
+ elif geo_type == "Sphere" or geo_type == "Point":
1018
+ # Draw as a marker or simple sphere if we want
1019
+ center = app.center
1020
+ volume = app.volume
1021
+
1022
+ # Simple marker for now
811
1023
  fig.add_trace(
812
- go.Mesh3d(
813
- x=x,
814
- y=y,
815
- z=z,
816
- i=i,
817
- j=j,
818
- k=k,
819
- color="gray",
820
- opacity=opacity,
821
- name=name,
822
- showscale=False,
823
- flatshading=True,
1024
+ go.Scatter3d(
1025
+ x=[center[0]],
1026
+ y=[center[1]],
1027
+ z=[center[2]],
1028
+ mode="markers",
1029
+ marker=dict(size=10, color="goldenrod", symbol="circle"),
1030
+ name=f"{app.name} (Appendage)",
1031
+ text=f"Vol: {volume:.3f}m³",
824
1032
  )
825
1033
  )
@@ -655,7 +655,7 @@ dependencies = [
655
655
 
656
656
  [[package]]
657
657
  name = "navaltoolbox"
658
- version = "0.3.0"
658
+ version = "0.4.0"
659
659
  dependencies = [
660
660
  "approx",
661
661
  "dxf",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "navaltoolbox"
3
- version = "0.3.0"
3
+ version = "0.4.0"
4
4
  edition = "2021"
5
5
  description = "High-performance naval architecture library for hydrostatics, stability, and tank calculations"
6
6
  authors = ["Antoine ANCEAU"]
@@ -23,8 +23,6 @@ parry3d-f64 = "0.18"
23
23
  nalgebra = "0.33"
24
24
  # STL file loading
25
25
  stl_io = "0.8"
26
- # VTK file loading (optional, for later)
27
- # vtkio = "0.6"
28
26
  # Fast polygon triangulation for waterline caps
29
27
  earcutr = "0.4"
30
28
  # Ordered floats for HashMap keys
@@ -38,7 +38,7 @@ Add to your `Cargo.toml`:
38
38
 
39
39
  ```toml
40
40
  [dependencies]
41
- navaltoolbox = "0.2"
41
+ navaltoolbox = "0.4"
42
42
  ```
43
43
 
44
44
  ## Quick Start