subsurface-terra 2025.1.0rc6__tar.gz → 2025.1.0rc8__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 (119) hide show
  1. {subsurface_terra-2025.1.0rc6/subsurface_terra.egg-info → subsurface_terra-2025.1.0rc8}/PKG-INFO +1 -1
  2. {subsurface_terra-2025.1.0rc6 → subsurface_terra-2025.1.0rc8}/subsurface/_version.py +1 -1
  3. {subsurface_terra-2025.1.0rc6 → subsurface_terra-2025.1.0rc8}/subsurface/api/__init__.py +2 -0
  4. {subsurface_terra-2025.1.0rc6 → subsurface_terra-2025.1.0rc8}/subsurface/api/interfaces/stream.py +19 -1
  5. {subsurface_terra-2025.1.0rc6 → subsurface_terra-2025.1.0rc8}/subsurface/modules/reader/__init__.py +2 -0
  6. subsurface_terra-2025.1.0rc8/subsurface/modules/reader/mesh/_trimesh_reader.py +433 -0
  7. {subsurface_terra-2025.1.0rc6 → subsurface_terra-2025.1.0rc8}/subsurface/modules/reader/mesh/dxf_reader.py +10 -3
  8. {subsurface_terra-2025.1.0rc6 → subsurface_terra-2025.1.0rc8}/subsurface/modules/reader/mesh/glb_reader.py +7 -4
  9. {subsurface_terra-2025.1.0rc6 → subsurface_terra-2025.1.0rc8}/subsurface/modules/reader/mesh/obj_reader.py +19 -5
  10. {subsurface_terra-2025.1.0rc6 → subsurface_terra-2025.1.0rc8}/subsurface/modules/reader/wells/read_borehole_interface.py +12 -6
  11. {subsurface_terra-2025.1.0rc6 → subsurface_terra-2025.1.0rc8/subsurface_terra.egg-info}/PKG-INFO +1 -1
  12. subsurface_terra-2025.1.0rc6/subsurface/modules/reader/mesh/_trimesh_reader.py +0 -229
  13. {subsurface_terra-2025.1.0rc6 → subsurface_terra-2025.1.0rc8}/.env.example +0 -0
  14. {subsurface_terra-2025.1.0rc6 → subsurface_terra-2025.1.0rc8}/LICENSE +0 -0
  15. {subsurface_terra-2025.1.0rc6 → subsurface_terra-2025.1.0rc8}/README.rst +0 -0
  16. {subsurface_terra-2025.1.0rc6 → subsurface_terra-2025.1.0rc8}/requirements/requirements.txt +0 -0
  17. {subsurface_terra-2025.1.0rc6 → subsurface_terra-2025.1.0rc8}/requirements/requirements_all.txt +0 -0
  18. {subsurface_terra-2025.1.0rc6 → subsurface_terra-2025.1.0rc8}/requirements/requirements_dev.txt +0 -0
  19. {subsurface_terra-2025.1.0rc6 → subsurface_terra-2025.1.0rc8}/requirements/requirements_geospatial.txt +0 -0
  20. {subsurface_terra-2025.1.0rc6 → subsurface_terra-2025.1.0rc8}/requirements/requirements_mesh.txt +0 -0
  21. {subsurface_terra-2025.1.0rc6 → subsurface_terra-2025.1.0rc8}/requirements/requirements_opt.txt +0 -0
  22. {subsurface_terra-2025.1.0rc6 → subsurface_terra-2025.1.0rc8}/requirements/requirements_plot.txt +0 -0
  23. {subsurface_terra-2025.1.0rc6 → subsurface_terra-2025.1.0rc8}/requirements/requirements_traces.txt +0 -0
  24. {subsurface_terra-2025.1.0rc6 → subsurface_terra-2025.1.0rc8}/requirements/requirements_volume.txt +0 -0
  25. {subsurface_terra-2025.1.0rc6 → subsurface_terra-2025.1.0rc8}/requirements/requirements_wells.txt +0 -0
  26. {subsurface_terra-2025.1.0rc6 → subsurface_terra-2025.1.0rc8}/setup.cfg +0 -0
  27. {subsurface_terra-2025.1.0rc6 → subsurface_terra-2025.1.0rc8}/setup.py +0 -0
  28. {subsurface_terra-2025.1.0rc6 → subsurface_terra-2025.1.0rc8}/subsurface/__init__.py +0 -0
  29. {subsurface_terra-2025.1.0rc6 → subsurface_terra-2025.1.0rc8}/subsurface/api/interfaces/README.rst +0 -0
  30. {subsurface_terra-2025.1.0rc6 → subsurface_terra-2025.1.0rc8}/subsurface/api/interfaces/__init__.py +0 -0
  31. {subsurface_terra-2025.1.0rc6 → subsurface_terra-2025.1.0rc8}/subsurface/api/reader/__init__.py +0 -0
  32. {subsurface_terra-2025.1.0rc6 → subsurface_terra-2025.1.0rc8}/subsurface/api/reader/read_wells.py +0 -0
  33. {subsurface_terra-2025.1.0rc6 → subsurface_terra-2025.1.0rc8}/subsurface/core/__init__.py +0 -0
  34. {subsurface_terra-2025.1.0rc6 → subsurface_terra-2025.1.0rc8}/subsurface/core/geological_formats/__init__.py +0 -0
  35. {subsurface_terra-2025.1.0rc6 → subsurface_terra-2025.1.0rc8}/subsurface/core/geological_formats/boreholes/__init__.py +0 -0
  36. {subsurface_terra-2025.1.0rc6 → subsurface_terra-2025.1.0rc8}/subsurface/core/geological_formats/boreholes/_combine_trajectories.py +0 -0
  37. {subsurface_terra-2025.1.0rc6 → subsurface_terra-2025.1.0rc8}/subsurface/core/geological_formats/boreholes/boreholes.py +0 -0
  38. {subsurface_terra-2025.1.0rc6 → subsurface_terra-2025.1.0rc8}/subsurface/core/geological_formats/boreholes/collars.py +0 -0
  39. {subsurface_terra-2025.1.0rc6 → subsurface_terra-2025.1.0rc8}/subsurface/core/geological_formats/boreholes/survey.py +0 -0
  40. {subsurface_terra-2025.1.0rc6 → subsurface_terra-2025.1.0rc8}/subsurface/core/geological_formats/fault.py +0 -0
  41. {subsurface_terra-2025.1.0rc6 → subsurface_terra-2025.1.0rc8}/subsurface/core/reader_helpers/__init__.py +0 -0
  42. {subsurface_terra-2025.1.0rc6 → subsurface_terra-2025.1.0rc8}/subsurface/core/reader_helpers/reader_unstruct.py +0 -0
  43. {subsurface_terra-2025.1.0rc6 → subsurface_terra-2025.1.0rc8}/subsurface/core/reader_helpers/readers_data.py +0 -0
  44. {subsurface_terra-2025.1.0rc6 → subsurface_terra-2025.1.0rc8}/subsurface/core/reader_helpers/readers_wells.py +0 -0
  45. {subsurface_terra-2025.1.0rc6 → subsurface_terra-2025.1.0rc8}/subsurface/core/structs/README.rst +0 -0
  46. {subsurface_terra-2025.1.0rc6 → subsurface_terra-2025.1.0rc8}/subsurface/core/structs/__init__.py +0 -0
  47. {subsurface_terra-2025.1.0rc6 → subsurface_terra-2025.1.0rc8}/subsurface/core/structs/base_structures/__init__.py +0 -0
  48. {subsurface_terra-2025.1.0rc6 → subsurface_terra-2025.1.0rc8}/subsurface/core/structs/base_structures/_liquid_earth_mesh.py +0 -0
  49. {subsurface_terra-2025.1.0rc6 → subsurface_terra-2025.1.0rc8}/subsurface/core/structs/base_structures/_unstructured_data_constructor.py +0 -0
  50. {subsurface_terra-2025.1.0rc6 → subsurface_terra-2025.1.0rc8}/subsurface/core/structs/base_structures/base_structures_enum.py +0 -0
  51. {subsurface_terra-2025.1.0rc6 → subsurface_terra-2025.1.0rc8}/subsurface/core/structs/base_structures/structured_data.py +0 -0
  52. {subsurface_terra-2025.1.0rc6 → subsurface_terra-2025.1.0rc8}/subsurface/core/structs/base_structures/unstructured_data.py +0 -0
  53. {subsurface_terra-2025.1.0rc6 → subsurface_terra-2025.1.0rc8}/subsurface/core/structs/structured_elements/__init__.py +0 -0
  54. {subsurface_terra-2025.1.0rc6 → subsurface_terra-2025.1.0rc8}/subsurface/core/structs/structured_elements/octree_mesh.py +0 -0
  55. {subsurface_terra-2025.1.0rc6 → subsurface_terra-2025.1.0rc8}/subsurface/core/structs/structured_elements/structured_grid.py +0 -0
  56. {subsurface_terra-2025.1.0rc6 → subsurface_terra-2025.1.0rc8}/subsurface/core/structs/structured_elements/structured_mesh.py +0 -0
  57. {subsurface_terra-2025.1.0rc6 → subsurface_terra-2025.1.0rc8}/subsurface/core/structs/unstructured_elements/__init__.py +0 -0
  58. {subsurface_terra-2025.1.0rc6 → subsurface_terra-2025.1.0rc8}/subsurface/core/structs/unstructured_elements/line_set.py +0 -0
  59. {subsurface_terra-2025.1.0rc6 → subsurface_terra-2025.1.0rc8}/subsurface/core/structs/unstructured_elements/point_set.py +0 -0
  60. {subsurface_terra-2025.1.0rc6 → subsurface_terra-2025.1.0rc8}/subsurface/core/structs/unstructured_elements/tetrahedron_mesh.py +0 -0
  61. {subsurface_terra-2025.1.0rc6 → subsurface_terra-2025.1.0rc8}/subsurface/core/structs/unstructured_elements/triangular_surface.py +0 -0
  62. {subsurface_terra-2025.1.0rc6 → subsurface_terra-2025.1.0rc8}/subsurface/core/utils/__init__.py +0 -0
  63. {subsurface_terra-2025.1.0rc6 → subsurface_terra-2025.1.0rc8}/subsurface/core/utils/utils_core.py +0 -0
  64. {subsurface_terra-2025.1.0rc6 → subsurface_terra-2025.1.0rc8}/subsurface/modules/__init__.py +0 -0
  65. {subsurface_terra-2025.1.0rc6 → subsurface_terra-2025.1.0rc8}/subsurface/modules/reader/README.rst +0 -0
  66. {subsurface_terra-2025.1.0rc6 → subsurface_terra-2025.1.0rc8}/subsurface/modules/reader/faults/__init__.py +0 -0
  67. {subsurface_terra-2025.1.0rc6 → subsurface_terra-2025.1.0rc8}/subsurface/modules/reader/faults/faults.py +0 -0
  68. {subsurface_terra-2025.1.0rc6 → subsurface_terra-2025.1.0rc8}/subsurface/modules/reader/from_binary.py +0 -0
  69. {subsurface_terra-2025.1.0rc6 → subsurface_terra-2025.1.0rc8}/subsurface/modules/reader/geo_object/__init__.py +0 -0
  70. {subsurface_terra-2025.1.0rc6 → subsurface_terra-2025.1.0rc8}/subsurface/modules/reader/mesh/NOTES.md +0 -0
  71. {subsurface_terra-2025.1.0rc6 → subsurface_terra-2025.1.0rc8}/subsurface/modules/reader/mesh/_GOCAD_mesh.py +0 -0
  72. {subsurface_terra-2025.1.0rc6 → subsurface_terra-2025.1.0rc8}/subsurface/modules/reader/mesh/__init__.py +0 -0
  73. {subsurface_terra-2025.1.0rc6 → subsurface_terra-2025.1.0rc8}/subsurface/modules/reader/mesh/csv_mesh_reader.py +0 -0
  74. {subsurface_terra-2025.1.0rc6 → subsurface_terra-2025.1.0rc8}/subsurface/modules/reader/mesh/mx_reader.py +0 -0
  75. {subsurface_terra-2025.1.0rc6 → subsurface_terra-2025.1.0rc8}/subsurface/modules/reader/mesh/omf_mesh_reader.py +0 -0
  76. {subsurface_terra-2025.1.0rc6 → subsurface_terra-2025.1.0rc8}/subsurface/modules/reader/mesh/surface_reader.py +0 -0
  77. {subsurface_terra-2025.1.0rc6 → subsurface_terra-2025.1.0rc8}/subsurface/modules/reader/mesh/surfaces_api.py +0 -0
  78. {subsurface_terra-2025.1.0rc6 → subsurface_terra-2025.1.0rc8}/subsurface/modules/reader/petrel/__init__.py +0 -0
  79. {subsurface_terra-2025.1.0rc6 → subsurface_terra-2025.1.0rc8}/subsurface/modules/reader/profiles/__init__.py +0 -0
  80. {subsurface_terra-2025.1.0rc6 → subsurface_terra-2025.1.0rc8}/subsurface/modules/reader/profiles/profiles_core.py +0 -0
  81. {subsurface_terra-2025.1.0rc6 → subsurface_terra-2025.1.0rc8}/subsurface/modules/reader/read_netcdf.py +0 -0
  82. {subsurface_terra-2025.1.0rc6 → subsurface_terra-2025.1.0rc8}/subsurface/modules/reader/topography/__init__.py +0 -0
  83. {subsurface_terra-2025.1.0rc6 → subsurface_terra-2025.1.0rc8}/subsurface/modules/reader/topography/topo_core.py +0 -0
  84. {subsurface_terra-2025.1.0rc6 → subsurface_terra-2025.1.0rc8}/subsurface/modules/reader/volume/__init__.py +0 -0
  85. {subsurface_terra-2025.1.0rc6 → subsurface_terra-2025.1.0rc8}/subsurface/modules/reader/volume/read_volume.py +0 -0
  86. {subsurface_terra-2025.1.0rc6 → subsurface_terra-2025.1.0rc8}/subsurface/modules/reader/volume/segy_reader.py +0 -0
  87. {subsurface_terra-2025.1.0rc6 → subsurface_terra-2025.1.0rc8}/subsurface/modules/reader/volume/seismic.py +0 -0
  88. {subsurface_terra-2025.1.0rc6 → subsurface_terra-2025.1.0rc8}/subsurface/modules/reader/volume/volume_utils.py +0 -0
  89. {subsurface_terra-2025.1.0rc6 → subsurface_terra-2025.1.0rc8}/subsurface/modules/reader/wells/DEP/__init__.py +0 -0
  90. {subsurface_terra-2025.1.0rc6 → subsurface_terra-2025.1.0rc8}/subsurface/modules/reader/wells/DEP/_well_files_reader.py +0 -0
  91. {subsurface_terra-2025.1.0rc6 → subsurface_terra-2025.1.0rc8}/subsurface/modules/reader/wells/DEP/_wells_api.py +0 -0
  92. {subsurface_terra-2025.1.0rc6 → subsurface_terra-2025.1.0rc8}/subsurface/modules/reader/wells/DEP/_welly_reader.py +0 -0
  93. {subsurface_terra-2025.1.0rc6 → subsurface_terra-2025.1.0rc8}/subsurface/modules/reader/wells/DEP/pandas_to_welly.py +0 -0
  94. {subsurface_terra-2025.1.0rc6 → subsurface_terra-2025.1.0rc8}/subsurface/modules/reader/wells/README.rst +0 -0
  95. {subsurface_terra-2025.1.0rc6 → subsurface_terra-2025.1.0rc8}/subsurface/modules/reader/wells/__init__.py +0 -0
  96. {subsurface_terra-2025.1.0rc6 → subsurface_terra-2025.1.0rc8}/subsurface/modules/reader/wells/_read_to_df.py +0 -0
  97. {subsurface_terra-2025.1.0rc6 → subsurface_terra-2025.1.0rc8}/subsurface/modules/reader/wells/wells_utils.py +0 -0
  98. {subsurface_terra-2025.1.0rc6 → subsurface_terra-2025.1.0rc8}/subsurface/modules/visualization/__init__.py +0 -0
  99. {subsurface_terra-2025.1.0rc6 → subsurface_terra-2025.1.0rc8}/subsurface/modules/visualization/to_pyvista.py +0 -0
  100. {subsurface_terra-2025.1.0rc6 → subsurface_terra-2025.1.0rc8}/subsurface/modules/writer/__init__.py +0 -0
  101. {subsurface_terra-2025.1.0rc6 → subsurface_terra-2025.1.0rc8}/subsurface/modules/writer/to_binary.py +0 -0
  102. {subsurface_terra-2025.1.0rc6 → subsurface_terra-2025.1.0rc8}/subsurface/modules/writer/to_liquid_earth/__init__.py +0 -0
  103. {subsurface_terra-2025.1.0rc6 → subsurface_terra-2025.1.0rc8}/subsurface/modules/writer/to_rex/__init__.py +0 -0
  104. {subsurface_terra-2025.1.0rc6 → subsurface_terra-2025.1.0rc8}/subsurface/modules/writer/to_rex/common.py +0 -0
  105. {subsurface_terra-2025.1.0rc6 → subsurface_terra-2025.1.0rc8}/subsurface/modules/writer/to_rex/data_struct.py +0 -0
  106. {subsurface_terra-2025.1.0rc6 → subsurface_terra-2025.1.0rc8}/subsurface/modules/writer/to_rex/doc/rex-spec-v1.md +0 -0
  107. {subsurface_terra-2025.1.0rc6 → subsurface_terra-2025.1.0rc8}/subsurface/modules/writer/to_rex/doc/right-handed.png +0 -0
  108. {subsurface_terra-2025.1.0rc6 → subsurface_terra-2025.1.0rc8}/subsurface/modules/writer/to_rex/doc/sketchup_example.jpg +0 -0
  109. {subsurface_terra-2025.1.0rc6 → subsurface_terra-2025.1.0rc8}/subsurface/modules/writer/to_rex/gempy_to_rexfile.py +0 -0
  110. {subsurface_terra-2025.1.0rc6 → subsurface_terra-2025.1.0rc8}/subsurface/modules/writer/to_rex/material_encoder.py +0 -0
  111. {subsurface_terra-2025.1.0rc6 → subsurface_terra-2025.1.0rc8}/subsurface/modules/writer/to_rex/mesh_encoder.py +0 -0
  112. {subsurface_terra-2025.1.0rc6 → subsurface_terra-2025.1.0rc8}/subsurface/modules/writer/to_rex/to_rex.py +0 -0
  113. {subsurface_terra-2025.1.0rc6 → subsurface_terra-2025.1.0rc8}/subsurface/modules/writer/to_rex/utils.py +0 -0
  114. {subsurface_terra-2025.1.0rc6 → subsurface_terra-2025.1.0rc8}/subsurface/optional_requirements.py +0 -0
  115. {subsurface_terra-2025.1.0rc6 → subsurface_terra-2025.1.0rc8}/subsurface_terra.egg-info/SOURCES.txt +0 -0
  116. {subsurface_terra-2025.1.0rc6 → subsurface_terra-2025.1.0rc8}/subsurface_terra.egg-info/dependency_links.txt +0 -0
  117. {subsurface_terra-2025.1.0rc6 → subsurface_terra-2025.1.0rc8}/subsurface_terra.egg-info/not-zip-safe +0 -0
  118. {subsurface_terra-2025.1.0rc6 → subsurface_terra-2025.1.0rc8}/subsurface_terra.egg-info/requires.txt +0 -0
  119. {subsurface_terra-2025.1.0rc6 → subsurface_terra-2025.1.0rc8}/subsurface_terra.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: subsurface_terra
3
- Version: 2025.1.0rc6
3
+ Version: 2025.1.0rc8
4
4
  Summary: Subsurface data types and utilities. This version is the one used by Terranigma Solutions. Please feel free to take anything in this repository for the original one.
5
5
  Home-page: https://softwareunderground.github.io/subsurface
6
6
  Author: Software Underground
@@ -17,5 +17,5 @@ __version__: str
17
17
  __version_tuple__: VERSION_TUPLE
18
18
  version_tuple: VERSION_TUPLE
19
19
 
20
- __version__ = version = '2025.1.0rc6'
20
+ __version__ = version = '2025.1.0rc8'
21
21
  __version_tuple__ = version_tuple = (2025, 1, 0)
@@ -7,4 +7,6 @@ from .interfaces.stream import (
7
7
  CSV_volume_stream_to_struct,
8
8
  VTK_stream_to_struct,
9
9
  MX_stream_to_unstruc,
10
+ OBJ_stream_to_trisurf,
11
+ GLTF_stream_to_trisurf
10
12
  )
@@ -1,17 +1,20 @@
1
+ import io
1
2
  from io import BytesIO
2
3
  from typing import TextIO
3
4
 
4
5
  import pandas
5
- from subsurface.modules.reader.volume.volume_utils import interpolate_unstructured_data_to_structured_data
6
6
 
7
+ from ...core.structs import TriSurf
7
8
  from ...core.reader_helpers.reader_unstruct import ReaderUnstructuredHelper
8
9
  from ...core.reader_helpers.readers_data import GenericReaderFilesHelper
9
10
  from ...core.geological_formats import BoreholeSet
10
11
  from ...core.structs.base_structures import UnstructuredData, StructuredData
11
12
 
12
13
  from ...modules import reader
14
+ from ...modules.reader.mesh._trimesh_reader import TriMeshTransformations
13
15
  from ...modules.reader.volume.read_volume import read_volumetric_mesh_to_subsurface, read_VTK_structured_grid
14
16
  from ...modules.reader.mesh.surfaces_api import read_2d_mesh_to_unstruct
17
+ from ...modules.reader.volume.volume_utils import interpolate_unstructured_data_to_structured_data
15
18
 
16
19
  from ..reader.read_wells import read_wells
17
20
 
@@ -39,6 +42,21 @@ def MX_stream_to_unstruc(stream: TextIO) -> list[UnstructuredData]:
39
42
  return list_unstruct
40
43
 
41
44
 
45
+ def OBJ_stream_to_trisurf(obj_stream: TextIO, mtl_stream: list[TextIO],
46
+ texture_stream: list[io.BytesIO], coordinate_system: TriMeshTransformations) -> TriSurf:
47
+ tri_mesh: TriSurf = reader.load_obj_with_trimesh_from_binary(
48
+ obj_stream=obj_stream,
49
+ mtl_stream=mtl_stream,
50
+ texture_stream=texture_stream,
51
+ coord_system=coordinate_system
52
+ )
53
+ return tri_mesh
54
+
55
+
56
+ def GLTF_stream_to_trisurf(gltf_stream: io.BytesIO, coordinate_system: TriMeshTransformations) -> TriSurf:
57
+ tri_mesh: TriSurf = reader.load_gltf_with_trimesh(gltf_stream, coordinate_system)
58
+ return tri_mesh
59
+
42
60
  def VTK_stream_to_struct(stream: BytesIO, attribute_name: str) -> list[StructuredData]:
43
61
  struct = read_VTK_structured_grid(stream, attribute_name)
44
62
  return [struct]
@@ -7,3 +7,5 @@ from .topography.topo_core import read_structured_topography, read_unstructured_
7
7
  from .mesh.omf_mesh_reader import omf_stream_to_unstructs
8
8
  from .mesh.dxf_reader import dxf_stream_to_unstruct_input, dxf_file_to_unstruct_input
9
9
  from .mesh.mx_reader import mx_to_unstruc_from_binary
10
+ from .mesh.obj_reader import load_obj_with_trimesh, load_obj_with_trimesh_from_binary
11
+ from .mesh.glb_reader import load_gltf_with_trimesh
@@ -0,0 +1,433 @@
1
+ import enum
2
+ from typing import Union, TextIO, Optional
3
+ import io
4
+ import os
5
+
6
+ import numpy as np
7
+ from ....core.structs import UnstructuredData
8
+ from .... import optional_requirements
9
+ from ....core.structs import TriSurf, StructuredData
10
+
11
+
12
+ class TriMeshTransformations(enum.Enum):
13
+ RIGHT_HANDED_Z_UP = "right_handed_z_up"
14
+ ORIGINAL = "original"
15
+
16
+
17
+ def load_with_trimesh(path_to_file_or_buffer, file_type: Optional[str] = None,
18
+ coordinate_system: TriMeshTransformations = TriMeshTransformations.RIGHT_HANDED_Z_UP, *, plot=False):
19
+ """
20
+ Load a mesh with trimesh and convert to the specified coordinate system.
21
+
22
+ """
23
+ trimesh = optional_requirements.require_trimesh()
24
+ scene_or_mesh = LoadWithTrimesh.load_with_trimesh(path_to_file_or_buffer, file_type, plot)
25
+
26
+ # Compute a -90° rotation around the X axis
27
+ angle_rad = np.deg2rad(-90)
28
+ transform = trimesh.transformations.rotation_matrix(angle_rad, [1, 0, 0])
29
+
30
+ match coordinate_system:
31
+ case TriMeshTransformations.ORIGINAL:
32
+ return scene_or_mesh
33
+ case TriMeshTransformations.RIGHT_HANDED_Z_UP:
34
+ # Transform from Y-up (modeling software) to Z-up (scientific)
35
+ # This rotates the model so that:
36
+ # Old Y axis → New Z axis (pointing up)
37
+ # Old Z axis → New -Y axis
38
+ # Old X axis → Remains as X axis
39
+ transform = np.array([
40
+ [1, 0, 0, 0], # X → X
41
+ [0, 0, 1, 0], # Y → Z
42
+ [0, 1, 0, 0], # Z → -Y
43
+ [0, 0, 0, 1]
44
+ ])
45
+
46
+ # Apply the coordinate transformation
47
+ if isinstance(scene_or_mesh, trimesh.Scene):
48
+ for geometry in scene_or_mesh.geometry.values():
49
+ geometry.apply_transform(transform)
50
+ else:
51
+ scene_or_mesh.apply_transform(transform)
52
+ case _:
53
+ raise ValueError(f"Invalid coordinate system: {coordinate_system}")
54
+
55
+ return scene_or_mesh
56
+
57
+
58
+ def trimesh_to_unstruct(scene_or_mesh: Union["trimesh.Trimesh", "trimesh.Scene"]) -> TriSurf:
59
+ return TrimeshToSubsurface.trimesh_to_unstruct(scene_or_mesh)
60
+
61
+
62
+ class LoadWithTrimesh:
63
+ @classmethod
64
+ def load_with_trimesh(cls, path_to_file_or_buffer, file_type: Optional[str] = None, plot=False):
65
+ trimesh = optional_requirements.require_trimesh()
66
+ # Load the OBJ with Trimesh using the specified options
67
+ scene_or_mesh = trimesh.load(
68
+ file_obj=path_to_file_or_buffer,
69
+ file_type=file_type,
70
+ force="mesh"
71
+ )
72
+ # Process single mesh vs. scene
73
+ if isinstance(scene_or_mesh, trimesh.Scene):
74
+ print("Loaded a Scene with multiple geometries.")
75
+ cls._process_scene(scene_or_mesh)
76
+ if plot:
77
+ scene_or_mesh.show()
78
+ else:
79
+ print("Loaded a single Trimesh object.")
80
+ print(f" - Vertices: {len(scene_or_mesh.vertices)}")
81
+ print(f" - Faces: {len(scene_or_mesh.faces)}")
82
+ cls.handle_material_info(scene_or_mesh)
83
+ if plot:
84
+ scene_or_mesh.show()
85
+
86
+ return scene_or_mesh
87
+
88
+ @classmethod
89
+ def handle_material_info(cls, geometry):
90
+ """
91
+ Handle and print material information for a single geometry,
92
+ explicitly injecting the PIL image if provided.
93
+ """
94
+ if geometry.visual and hasattr(geometry.visual, 'material'):
95
+ material = geometry.visual.material
96
+
97
+ print("Trimesh material:", material)
98
+
99
+ # If there's already an image reference in the material, let the user know
100
+ if hasattr(material, 'image') and material.image is not None:
101
+ print(" -> Material already has an image:", material.image)
102
+ else:
103
+ print("No material found or no 'material' attribute on this geometry.")
104
+
105
+ @classmethod
106
+ def _process_scene(cls, scene):
107
+ """Process a scene with multiple geometries."""
108
+ geometries = scene.geometry
109
+ assert len(geometries) > 0, "No geometries found in the scene."
110
+
111
+ print(f"Loaded a Scene with {len(scene.geometry)} geometry object(s).")
112
+ for geom_name, geom in geometries.items():
113
+ print(f" Submesh: {geom_name}")
114
+ print(f" - Vertices: {len(geom.vertices)}")
115
+ print(f" - Faces: {len(geom.faces)}")
116
+
117
+ print(f"Geometry '{geom_name}':")
118
+ cls.handle_material_info(geom)
119
+
120
+
121
+ class TrimeshToSubsurface:
122
+ @classmethod
123
+ def trimesh_to_unstruct(cls, scene_or_mesh: Union["trimesh.Trimesh", "trimesh.Scene"]) -> TriSurf:
124
+ """
125
+ Convert a Trimesh or Scene object to a subsurface TriSurf object.
126
+
127
+ This function takes either a `trimesh.Trimesh` object or a `trimesh.Scene`
128
+ object and converts it to a `subsurface.TriSurf` object. If the input is
129
+ a scene containing multiple geometries, it processes all geometries and
130
+ combines them into a single TriSurf object. If the input is a single
131
+ Trimesh object, it directly converts it to a TriSurf object. An error
132
+ is raised if the input is neither a `trimesh.Trimesh` nor a `trimesh.Scene`
133
+ object.
134
+
135
+ Parameters:
136
+ scene_or_mesh (Union[trimesh.Trimesh, trimesh.Scene]):
137
+ Input geometry data, either as a Trimesh object representing
138
+ a single mesh or a Scene object containing multiple geometries.
139
+
140
+ Note:
141
+ ! Multimesh with multiple materials will read the uvs but not the textures since in that case is better
142
+ ! to read directly the multiple images (compressed) whenever the user wants to work with them.
143
+
144
+ Returns:
145
+ subsurface.TriSurf: Converted subsurface representation of the
146
+ provided geometry data.
147
+
148
+ Raises:
149
+ ValueError: If the input is neither a `trimesh.Trimesh` object nor
150
+ a `trimesh.Scene` object.
151
+ """
152
+ trimesh = optional_requirements.require_trimesh()
153
+ if isinstance(scene_or_mesh, trimesh.Scene):
154
+ # Process scene with multiple geometries
155
+ ts = cls._trisurf_from_scene(scene_or_mesh, trimesh)
156
+
157
+ elif isinstance(scene_or_mesh, trimesh.Trimesh):
158
+ ts = cls._trisurf_from_trimesh(scene_or_mesh)
159
+
160
+
161
+ else:
162
+ raise ValueError("Input must be a Trimesh object or a Scene with multiple geometries.")
163
+
164
+ return ts
165
+
166
+ @classmethod
167
+ def _trisurf_from_trimesh(cls, scene_or_mesh):
168
+ # Process single mesh
169
+ tri = scene_or_mesh
170
+ pandas = optional_requirements.require_pandas()
171
+ frame = pandas.DataFrame(tri.face_attributes)
172
+ # Check frame has a valid shape for cells_attr if not make None
173
+ if frame.shape[0] != tri.faces.shape[0]:
174
+ frame = None
175
+ # Get UV coordinates if they exist
176
+ vertex_attr = None
177
+ if hasattr(tri.visual, 'uv') and tri.visual.uv is not None:
178
+ vertex_attr = pandas.DataFrame(
179
+ tri.visual.uv,
180
+ columns=['u', 'v']
181
+ )
182
+ unstruct = UnstructuredData.from_array(
183
+ np.array(tri.vertices),
184
+ np.array(tri.faces),
185
+ cells_attr=frame,
186
+ vertex_attr=vertex_attr,
187
+ xarray_attributes={
188
+ "bounds": tri.bounds.tolist(),
189
+ },
190
+ )
191
+
192
+ texture = cls._extract_texture_from_material(tri)
193
+
194
+ ts = TriSurf(
195
+ mesh=unstruct,
196
+ texture=texture,
197
+ )
198
+ return ts
199
+
200
+ @classmethod
201
+ def _trisurf_from_scene(cls, scene_or_mesh: 'Scene', trimesh: 'trimesh') -> TriSurf:
202
+ pandas = optional_requirements.require_pandas()
203
+ geometries = scene_or_mesh.geometry
204
+ assert len(geometries) > 0, "No geometries found in the scene."
205
+ all_vertex = []
206
+ all_cells = []
207
+ cell_attr = []
208
+ all_vertex_attr = []
209
+ _last_cell = 0
210
+ texture = None
211
+ for i, (geom_name, geom) in enumerate(geometries.items()):
212
+ geom: trimesh.Trimesh
213
+ LoadWithTrimesh.handle_material_info(geom)
214
+
215
+ # Append vertices
216
+ all_vertex.append(np.array(geom.vertices))
217
+
218
+ # Adjust cell indices and append
219
+ cells = np.array(geom.faces)
220
+ if len(all_cells) > 0:
221
+ cells = cells + _last_cell
222
+ all_cells.append(cells)
223
+
224
+ # Create attribute array for this geometry
225
+ cell_attr.append(np.ones(len(cells)) * i)
226
+
227
+ _last_cell = cells.max() + 1
228
+
229
+ # Get UV coordinates if they exist
230
+ if hasattr(geom.visual, 'uv') and geom.visual.uv is not None:
231
+ vertex_attr = pandas.DataFrame(
232
+ geom.visual.uv,
233
+ columns=['u', 'v']
234
+ )
235
+ all_vertex_attr.append(vertex_attr)
236
+
237
+ # Extract texture from material if it is only one geometry
238
+ if len(geometries) == 1:
239
+ texture = cls._extract_texture_from_material(geom)
240
+
241
+ # Create the combined UnstructuredData
242
+ unstruct = UnstructuredData.from_array(
243
+ vertex=np.vstack(all_vertex),
244
+ cells=np.vstack(all_cells),
245
+ vertex_attr=pandas.concat(all_vertex_attr, ignore_index=True) if len(all_vertex_attr) > 0 else None,
246
+ cells_attr=pandas.DataFrame(np.hstack(cell_attr), columns=["Geometry id"]),
247
+ xarray_attributes={
248
+ "bounds": scene_or_mesh.bounds.tolist(),
249
+ },
250
+ )
251
+
252
+ # If there is a texture
253
+ ts = TriSurf(
254
+ mesh=unstruct,
255
+ texture=texture,
256
+ )
257
+
258
+ return ts
259
+
260
+ @classmethod
261
+ def _extract_texture_from_material(cls, geom):
262
+ from PIL.JpegImagePlugin import JpegImageFile
263
+ from PIL.PngImagePlugin import PngImageFile
264
+ import trimesh
265
+
266
+ if geom.visual is None or getattr(geom.visual, 'material', None) is None:
267
+ return None
268
+
269
+ array = np.empty(0)
270
+ if isinstance(geom.visual.material, trimesh.visual.material.SimpleMaterial):
271
+ image: JpegImageFile = geom.visual.material.image
272
+ if image is None:
273
+ return None
274
+ array = np.array(image)
275
+ elif isinstance(geom.visual.material, trimesh.visual.material.PBRMaterial):
276
+ image: PngImageFile = geom.visual.material.baseColorTexture
277
+ array = np.array(image.convert('RGBA'))
278
+
279
+ if image is None:
280
+ return None
281
+ else:
282
+ raise ValueError(f"Unsupported material type: {type(geom.visual.material)}")
283
+
284
+ # Asser that image has 3 channels assert array.shape[2] == 3 from PIL.PngImagePlugin import PngImageFile
285
+ assert array.shape[2] == 3 or array.shape[2] == 4
286
+ texture = StructuredData.from_numpy(array)
287
+ return texture
288
+
289
+ @classmethod
290
+ def _validate_texture_path(cls, texture_path):
291
+ """Validate the texture file path."""
292
+ if texture_path and not texture_path.lower().endswith(('.png', '.jpg', '.jpeg')):
293
+ raise ValueError("Texture path must be a PNG or JPEG file")
294
+
295
+
296
+ class TriMeshReaderFromBlob:
297
+ @classmethod
298
+ def OBJ_stream_to_trisurf(cls, obj_stream: TextIO, mtl_stream: list[TextIO],
299
+ texture_stream: list[io.BytesIO], coord_system: TriMeshTransformations) -> TriSurf:
300
+ """
301
+ Load an OBJ file from a stream and convert it to a TriSurf object.
302
+
303
+ Parameters:
304
+ obj_stream: TextIO containing the OBJ file data (text format)
305
+ mtl_stream: TextIO containing the MTL file data (text format)
306
+ texture_stream: BytesIO containing the texture file data (binary format)
307
+
308
+ Returns:
309
+ TriSurf: The loaded mesh with textures if available
310
+ """
311
+ trimesh = optional_requirements.require_trimesh()
312
+ import tempfile
313
+
314
+ path_in = "file.obj"
315
+
316
+ # Create a temporary directory to store associated files
317
+ with tempfile.TemporaryDirectory() as temp_dir:
318
+ # Write the OBJ content to a temp file
319
+ obj_path = os.path.join(temp_dir, os.path.basename(path_in))
320
+ with open(obj_path, 'w') as f: # Use text mode 'w' for text files
321
+ obj_stream.seek(0)
322
+ f.write(obj_stream.read())
323
+ obj_stream.seek(0)
324
+
325
+ if mtl_stream is not None:
326
+ cls.write_material_files(
327
+ mtl_streams=mtl_stream,
328
+ obj_stream=obj_stream,
329
+ temp_dir=temp_dir,
330
+ texture_streams=texture_stream
331
+ )
332
+
333
+ # Now load the OBJ with all associated files available
334
+ scene_or_mesh = load_with_trimesh(
335
+ path_to_file_or_buffer=obj_path,
336
+ file_type="obj",
337
+ coordinate_system=coord_system
338
+ )
339
+
340
+ # Convert to a TriSurf object
341
+ tri_surf = TrimeshToSubsurface.trimesh_to_unstruct(scene_or_mesh)
342
+
343
+ return tri_surf
344
+
345
+ @classmethod
346
+ def write_material_files(cls, mtl_streams: list[TextIO], obj_stream: TextIO, temp_dir, texture_streams: list[io.BytesIO]):
347
+ # Extract mtl references from the OBJ file
348
+ mtl_files = cls._extract_mtl_references(obj_stream)
349
+ # Download and save MTL files
350
+ for e, mtl_file in enumerate(mtl_files):
351
+ mtl_path = f"{temp_dir}/{mtl_file}" if temp_dir else mtl_file
352
+ mtl_stream = mtl_streams[e] if mtl_streams else None
353
+ try:
354
+ # Save the MTL file to temp directory
355
+ mtl_temp_path = os.path.join(temp_dir, mtl_file)
356
+ with open(mtl_temp_path, 'w') as f: # Use text mode 'w' for text files
357
+ mtl_stream.seek(0)
358
+ f.write(mtl_stream.read())
359
+
360
+ # Extract texture references from MTL
361
+ mtl_stream.seek(0)
362
+ texture_files = cls._extract_texture_references(mtl_stream)
363
+
364
+ if texture_streams is None:
365
+ continue
366
+
367
+ # Download texture files
368
+ for ee, texture_file in enumerate(texture_files):
369
+ texture_path = f"{temp_dir}/{texture_file}" if temp_dir else texture_file
370
+ texture_stream = texture_streams[ee] if texture_streams else None
371
+ try:
372
+ # Save the texture file to temp directory
373
+ with open(os.path.join(temp_dir, texture_file), 'wb') as f: # Binary mode for textures
374
+ texture_stream.seek(0)
375
+ f.write(texture_stream.read())
376
+ except Exception as e:
377
+ print(f"Failed to load texture {texture_file}: {e}")
378
+ except Exception as e:
379
+ print(f"Failed to load MTL file {mtl_file}: {e}")
380
+
381
+ @classmethod
382
+ def _extract_mtl_references(cls, obj_stream):
383
+ """Extract MTL file references from an OBJ file."""
384
+ obj_stream.seek(0)
385
+ mtl_files = []
386
+
387
+ # TextIO stream already contains decoded text, so no need to decode
388
+ obj_text = obj_stream.read()
389
+ obj_stream.seek(0)
390
+
391
+ for line in obj_text.splitlines():
392
+ if line.startswith('mtllib '):
393
+ mtl_name = line.split(None, 1)[1].strip()
394
+ mtl_files.append(mtl_name)
395
+
396
+ return mtl_files
397
+
398
+ @classmethod
399
+ def _extract_texture_references(cls, mtl_stream):
400
+ """
401
+ Extract texture file references from an MTL file.
402
+ Works with both TextIO and BytesIO streams.
403
+
404
+ Parameters:
405
+ mtl_stream: TextIO or BytesIO containing the MTL file data
406
+
407
+ Returns:
408
+ list[str]: List of texture file names referenced in the MTL
409
+ """
410
+ mtl_stream.seek(0)
411
+ texture_files = []
412
+
413
+ # Handle both TextIO and BytesIO
414
+ if isinstance(mtl_stream, io.TextIOWrapper):
415
+ # TextIO stream already contains decoded text
416
+ mtl_text = mtl_stream.read()
417
+ else:
418
+ # BytesIO stream needs to be decoded
419
+ mtl_text = mtl_stream.read().decode('utf-8', errors='replace')
420
+
421
+ mtl_stream.seek(0)
422
+
423
+ for line in mtl_text.splitlines():
424
+ # Check for texture map definitions
425
+ for prefix in ['map_Kd ', 'map_Ka ', 'map_Ks ', 'map_Bump ', 'map_d ']:
426
+ if line.startswith(prefix):
427
+ parts = line.split(None, 1)
428
+ if len(parts) > 1:
429
+ texture_name = parts[1].strip()
430
+ texture_files.append(texture_name)
431
+ break
432
+
433
+ return texture_files
@@ -59,7 +59,7 @@ def dxf_file_to_unstruct_input(
59
59
  """
60
60
  ezdxf = optional_requirements.require_ezdxf()
61
61
  dataset = ezdxf.readfile(file)
62
- cell_attr_int, cell_attr_map, cells, vertex = _dxf_dataset_to_unstruct_input(dataset)
62
+ vertex, cells, cell_attr_int, cell_attr_map = _dxf_dataset_to_unstruct_input(dataset)
63
63
 
64
64
  if vertex.size == 0:
65
65
  raise ValueError("The DXF file does not contain any 3DFACE entities.")
@@ -124,7 +124,14 @@ def _extract_vertices_from_dataset(
124
124
  return np.unique(vertices, axis=0)
125
125
 
126
126
 
127
- def _dxf_dataset_to_unstruct_input(dataset):
127
+ def _dxf_dataset_to_unstruct_input(dataset: 'ezdxf.drawing.Drawing') -> tuple[np.ndarray, np.ndarray, np.ndarray, dict]:
128
+ """
129
+ Build unstructured-mesh-like data from 3DFACE entities in a dataset:
130
+ - vertex coordinates
131
+ - connectivity in 'cells' array
132
+ - cell attributes in both integer-coded and mapping (string->int) forms
133
+
134
+ """
128
135
  """
129
136
  Build unstructured-mesh-like data from 3DFACE entities in a dataset:
130
137
  - vertex coordinates
@@ -167,4 +174,4 @@ def _dxf_dataset_to_unstruct_input(dataset):
167
174
  cells = np.arange(0, vertices.shape[0]).reshape(-1, 3)
168
175
 
169
176
  cell_attr_int, cell_attr_map = _map_cell_attr_strings_to_integers(cell_attr)
170
- return cell_attr_int, cell_attr_map, cells, vertices
177
+ return vertices, cells, cell_attr_int, cell_attr_map
@@ -1,8 +1,11 @@
1
- import subsurface
2
- from subsurface.modules.reader.mesh._trimesh_reader import _load_with_trimesh, trimesh_to_unstruct
1
+ import io
2
+ from typing import Union
3
3
 
4
+ from ....core.structs import TriSurf
5
+ from ._trimesh_reader import load_with_trimesh, trimesh_to_unstruct, TriMeshTransformations
4
6
 
5
- def load_glb_with_trimesh(path_to_glb: str, plot: bool = False) -> subsurface.TriSurf:
7
+
8
+ def load_gltf_with_trimesh(path_to_glb: Union[str | io.BytesIO], coordinate_system: TriMeshTransformations) -> TriSurf:
6
9
  """
7
10
  load_obj_with_trimesh(path_to_glb, plot=False)
8
11
 
@@ -22,6 +25,6 @@ def load_glb_with_trimesh(path_to_glb: str, plot: bool = False) -> subsurface.Tr
22
25
  subsurface.TriSurf
23
26
  A TriSurf object representing the processed 3D surface geometry.
24
27
  """
25
- trimesh = _load_with_trimesh(path_to_glb, plot)
28
+ trimesh = load_with_trimesh(path_to_glb, file_type="glb", coordinate_system=coordinate_system, plot=False)
26
29
  trisurf = trimesh_to_unstruct(trimesh)
27
30
  return trisurf
@@ -1,10 +1,24 @@
1
- from typing import Union
1
+ from typing import Union, TextIO
2
+ import io
2
3
 
3
- import subsurface
4
- from subsurface.modules.reader.mesh._trimesh_reader import _load_with_trimesh, trimesh_to_unstruct
4
+ from ._trimesh_reader import load_with_trimesh, trimesh_to_unstruct, TriMeshReaderFromBlob, TriMeshTransformations
5
+ from ....core.structs import TriSurf
5
6
 
6
7
 
7
- def load_obj_with_trimesh(path_to_obj: str, plot: bool = False) -> subsurface.TriSurf:
8
+
9
+ def load_obj_with_trimesh_from_binary(obj_stream: TextIO, mtl_stream: list[TextIO],
10
+ texture_stream: list[io.BytesIO], coord_system: TriMeshTransformations) -> TriSurf:
11
+ tri_surf: TriSurf = TriMeshReaderFromBlob.OBJ_stream_to_trisurf(
12
+ obj_stream=obj_stream,
13
+ mtl_stream=mtl_stream,
14
+ texture_stream=texture_stream,
15
+ coord_system=coord_system
16
+ )
17
+
18
+ return tri_surf
19
+
20
+
21
+ def load_obj_with_trimesh(path_to_obj: str, plot: bool = False) -> TriSurf:
8
22
  """
9
23
  Load and process an OBJ file, returning trimesh-compatible objects.
10
24
 
@@ -34,6 +48,6 @@ def load_obj_with_trimesh(path_to_obj: str, plot: bool = False) -> subsurface.Tr
34
48
  `ValueError`: If the OBJ file could not be properly processed.
35
49
 
36
50
  """
37
- trimesh = _load_with_trimesh(path_to_obj, plot)
51
+ trimesh = load_with_trimesh(path_to_obj, file_type="obj", plot=plot)
38
52
  trisurf = trimesh_to_unstruct(trimesh)
39
53
  return trisurf
@@ -9,7 +9,6 @@ from subsurface.modules.reader.wells.wells_utils import add_tops_from_base_and_a
9
9
 
10
10
 
11
11
  def read_collar(reader_helper: GenericReaderFilesHelper) -> pd.DataFrame:
12
- if reader_helper.usecols is None: reader_helper.usecols = [0, 1, 2, 3]
13
12
  if reader_helper.index_col is False: reader_helper.index_col = 0
14
13
 
15
14
  # Check file_or_buffer type
@@ -22,13 +21,16 @@ def read_collar(reader_helper: GenericReaderFilesHelper) -> pd.DataFrame:
22
21
  return data_df
23
22
 
24
23
 
25
- def read_survey(reader_helper: GenericReaderFilesHelper):
24
+ def read_survey(reader_helper: GenericReaderFilesHelper, validate_survey: bool = True) -> pd.DataFrame:
26
25
  if reader_helper.index_col is False: reader_helper.index_col = 0
27
26
 
28
27
  d = check_format_and_read_to_df(reader_helper)
29
28
  _map_rows_and_cols_inplace(d, reader_helper)
30
29
 
31
- d_no_singles = _validate_survey_data(d)
30
+ if validate_survey:
31
+ d_no_singles = _validate_survey_data(d)
32
+ else:
33
+ d_no_singles = d
32
34
 
33
35
  return d_no_singles
34
36
 
@@ -37,12 +39,16 @@ def read_lith(reader_helper: GenericReaderFilesHelper) -> pd.DataFrame:
37
39
  return read_attributes(reader_helper, is_lith=True)
38
40
 
39
41
 
40
- def read_attributes(reader_helper: GenericReaderFilesHelper, is_lith: bool = False) -> pd.DataFrame:
41
- if reader_helper.index_col is False: reader_helper.index_col = 0
42
-
42
+ def read_attributes(reader_helper: GenericReaderFilesHelper, is_lith: bool = False, validate_attr: bool = True) -> pd.DataFrame:
43
+ if reader_helper.index_col is False:
44
+ reader_helper.index_col = 0
45
+
43
46
  d = check_format_and_read_to_df(reader_helper)
44
47
 
45
48
  _map_rows_and_cols_inplace(d, reader_helper)
49
+ if validate_attr is False:
50
+ return d
51
+
46
52
  if is_lith:
47
53
  d = _validate_lith_data(d, reader_helper)
48
54
  else:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: subsurface_terra
3
- Version: 2025.1.0rc6
3
+ Version: 2025.1.0rc8
4
4
  Summary: Subsurface data types and utilities. This version is the one used by Terranigma Solutions. Please feel free to take anything in this repository for the original one.
5
5
  Home-page: https://softwareunderground.github.io/subsurface
6
6
  Author: Software Underground