vixar 1.0.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.
- vixar-1.0.0/.gitignore +38 -0
- vixar-1.0.0/PKG-INFO +75 -0
- vixar-1.0.0/README.md +36 -0
- vixar-1.0.0/pyproject.toml +68 -0
- vixar-1.0.0/src/vixar/__init__.py +17 -0
- vixar-1.0.0/src/vixar/cli.py +76 -0
- vixar-1.0.0/src/vixar/errors.py +32 -0
- vixar-1.0.0/src/vixar/html.py +50 -0
- vixar-1.0.0/src/vixar/io/__init__.py +21 -0
- vixar-1.0.0/src/vixar/io/block_model_reader.py +78 -0
- vixar-1.0.0/src/vixar/io/coords.py +84 -0
- vixar-1.0.0/src/vixar/io/csv_reader.py +160 -0
- vixar-1.0.0/src/vixar/io/encode.py +25 -0
- vixar-1.0.0/src/vixar/io/las_reader.py +108 -0
- vixar-1.0.0/src/vixar/io/mesh_reader.py +166 -0
- vixar-1.0.0/src/vixar/io/stats.py +43 -0
- vixar-1.0.0/src/vixar/io/volume_reader.py +141 -0
- vixar-1.0.0/src/vixar/layers.py +100 -0
- vixar-1.0.0/src/vixar/schema.py +170 -0
- vixar-1.0.0/src/vixar/server.py +127 -0
- vixar-1.0.0/src/vixar/static/viewer.js +4018 -0
- vixar-1.0.0/src/vixar/static/viewer.js.map +1 -0
- vixar-1.0.0/src/vixar/static.py +59 -0
- vixar-1.0.0/src/vixar/tiler.py +150 -0
- vixar-1.0.0/src/vixar/viewer.py +1015 -0
- vixar-1.0.0/src/vixar/widget.py +128 -0
vixar-1.0.0/.gitignore
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Node
|
|
2
|
+
node_modules/
|
|
3
|
+
dist/
|
|
4
|
+
*.tsbuildinfo
|
|
5
|
+
.pnpm-store/
|
|
6
|
+
|
|
7
|
+
# Vite
|
|
8
|
+
.vite/
|
|
9
|
+
|
|
10
|
+
# Python
|
|
11
|
+
__pycache__/
|
|
12
|
+
*.py[cod]
|
|
13
|
+
*.egg-info/
|
|
14
|
+
.eggs/
|
|
15
|
+
build/
|
|
16
|
+
.pytest_cache/
|
|
17
|
+
.venv/
|
|
18
|
+
venv/
|
|
19
|
+
.mypy_cache/
|
|
20
|
+
.ruff_cache/
|
|
21
|
+
|
|
22
|
+
# Built viewer bundle (produced by CI from packages/engine + apps/viewer)
|
|
23
|
+
python/src/vixar/static/viewer.js
|
|
24
|
+
python/src/vixar/static/viewer.js.map
|
|
25
|
+
|
|
26
|
+
# Notebooks
|
|
27
|
+
.ipynb_checkpoints/
|
|
28
|
+
|
|
29
|
+
# OS / editor
|
|
30
|
+
.DS_Store
|
|
31
|
+
Thumbs.db
|
|
32
|
+
.idea/
|
|
33
|
+
*.log
|
|
34
|
+
|
|
35
|
+
# Test artifacts
|
|
36
|
+
coverage/
|
|
37
|
+
playwright-report/
|
|
38
|
+
test-results/
|
vixar-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: vixar
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Proprietary geoscientific 3D/2D visualization engine, distributed as a Python library.
|
|
5
|
+
Author: Vixar
|
|
6
|
+
License: Proprietary
|
|
7
|
+
Keywords: geoscience,mining,point-cloud,visualization,webgl
|
|
8
|
+
Classifier: Intended Audience :: Science/Research
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Classifier: Topic :: Scientific/Engineering :: Visualization
|
|
13
|
+
Requires-Python: >=3.10
|
|
14
|
+
Requires-Dist: anywidget>=0.9
|
|
15
|
+
Requires-Dist: fastapi>=0.110
|
|
16
|
+
Requires-Dist: numpy>=1.24
|
|
17
|
+
Requires-Dist: pandas>=2.0
|
|
18
|
+
Requires-Dist: pydantic>=2.5
|
|
19
|
+
Requires-Dist: pyproj>=3.5
|
|
20
|
+
Requires-Dist: uvicorn>=0.27
|
|
21
|
+
Provides-Extra: all
|
|
22
|
+
Requires-Dist: laspy>=2.5; extra == 'all'
|
|
23
|
+
Requires-Dist: lazrs>=0.5; extra == 'all'
|
|
24
|
+
Requires-Dist: meshio>=5.3; extra == 'all'
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: httpx>=0.27; extra == 'dev'
|
|
27
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
28
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
29
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
30
|
+
Provides-Extra: docs
|
|
31
|
+
Requires-Dist: mkdocs-material>=9.5; extra == 'docs'
|
|
32
|
+
Requires-Dist: mkdocstrings[python]>=0.26; extra == 'docs'
|
|
33
|
+
Provides-Extra: las
|
|
34
|
+
Requires-Dist: laspy>=2.5; extra == 'las'
|
|
35
|
+
Requires-Dist: lazrs>=0.5; extra == 'las'
|
|
36
|
+
Provides-Extra: mesh
|
|
37
|
+
Requires-Dist: meshio>=5.3; extra == 'mesh'
|
|
38
|
+
Description-Content-Type: text/markdown
|
|
39
|
+
|
|
40
|
+
# Vixar
|
|
41
|
+
|
|
42
|
+
Proprietary geoscientific 3D/2D visualization engine, distributed as a Python library.
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
import vixar as vx
|
|
46
|
+
|
|
47
|
+
viewer = vx.Viewer(width=1200, height=800, theme="dark")
|
|
48
|
+
viewer.add_point_cloud("survey.csv", color_by="elevation", cmap="terrain")
|
|
49
|
+
viewer.add_boreholes(
|
|
50
|
+
"drillholes.csv",
|
|
51
|
+
id_col="HOLE_ID", x_col="X", y_col="Y", z_col="Z", from_col="FROM",
|
|
52
|
+
color_by="au_ppm", cmap="hot",
|
|
53
|
+
)
|
|
54
|
+
viewer.show() # inline Jupyter widget
|
|
55
|
+
viewer.serve(port=8050) # local web server (iframe-embeddable)
|
|
56
|
+
viewer.to_html("scene.html") # standalone HTML
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
`pip install vixar` — no Node.js required for end users. The WebGL2 engine ships
|
|
60
|
+
as a bundled `viewer.js` static asset.
|
|
61
|
+
|
|
62
|
+
## Development
|
|
63
|
+
|
|
64
|
+
The JS engine lives in the monorepo at the repo root. Build the bundle before
|
|
65
|
+
running the Python package from source:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
pnpm install
|
|
69
|
+
pnpm build:viewer # -> python/src/vixar/static/viewer.js
|
|
70
|
+
cd python && pip install -e ".[dev]"
|
|
71
|
+
pytest
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
This is a Phase 1 build (CSV point clouds + boreholes, colour legend, UTM
|
|
75
|
+
origin, Jupyter/serve/HTML output). See `../plan_v2.md` for the full roadmap.
|
vixar-1.0.0/README.md
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Vixar
|
|
2
|
+
|
|
3
|
+
Proprietary geoscientific 3D/2D visualization engine, distributed as a Python library.
|
|
4
|
+
|
|
5
|
+
```python
|
|
6
|
+
import vixar as vx
|
|
7
|
+
|
|
8
|
+
viewer = vx.Viewer(width=1200, height=800, theme="dark")
|
|
9
|
+
viewer.add_point_cloud("survey.csv", color_by="elevation", cmap="terrain")
|
|
10
|
+
viewer.add_boreholes(
|
|
11
|
+
"drillholes.csv",
|
|
12
|
+
id_col="HOLE_ID", x_col="X", y_col="Y", z_col="Z", from_col="FROM",
|
|
13
|
+
color_by="au_ppm", cmap="hot",
|
|
14
|
+
)
|
|
15
|
+
viewer.show() # inline Jupyter widget
|
|
16
|
+
viewer.serve(port=8050) # local web server (iframe-embeddable)
|
|
17
|
+
viewer.to_html("scene.html") # standalone HTML
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
`pip install vixar` — no Node.js required for end users. The WebGL2 engine ships
|
|
21
|
+
as a bundled `viewer.js` static asset.
|
|
22
|
+
|
|
23
|
+
## Development
|
|
24
|
+
|
|
25
|
+
The JS engine lives in the monorepo at the repo root. Build the bundle before
|
|
26
|
+
running the Python package from source:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pnpm install
|
|
30
|
+
pnpm build:viewer # -> python/src/vixar/static/viewer.js
|
|
31
|
+
cd python && pip install -e ".[dev]"
|
|
32
|
+
pytest
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
This is a Phase 1 build (CSV point clouds + boreholes, colour legend, UTM
|
|
36
|
+
origin, Jupyter/serve/HTML output). See `../plan_v2.md` for the full roadmap.
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "vixar"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Proprietary geoscientific 3D/2D visualization engine, distributed as a Python library."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = { text = "Proprietary" }
|
|
12
|
+
authors = [{ name = "Vixar" }]
|
|
13
|
+
keywords = ["geoscience", "visualization", "webgl", "point-cloud", "mining"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3.10",
|
|
16
|
+
"Programming Language :: Python :: 3.11",
|
|
17
|
+
"Programming Language :: Python :: 3.12",
|
|
18
|
+
"Intended Audience :: Science/Research",
|
|
19
|
+
"Topic :: Scientific/Engineering :: Visualization",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
dependencies = [
|
|
23
|
+
"numpy>=1.24",
|
|
24
|
+
"pandas>=2.0",
|
|
25
|
+
"pydantic>=2.5",
|
|
26
|
+
"pyproj>=3.5",
|
|
27
|
+
"anywidget>=0.9",
|
|
28
|
+
"fastapi>=0.110",
|
|
29
|
+
"uvicorn>=0.27",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[project.optional-dependencies]
|
|
33
|
+
# 'lazrs' is a pure-Rust LAZ backend with prebuilt wheels for all platforms
|
|
34
|
+
# (no system LASzip needed), so `vixar[las]` reads both .las and .laz.
|
|
35
|
+
las = ["laspy>=2.5", "lazrs>=0.5"]
|
|
36
|
+
mesh = ["meshio>=5.3"]
|
|
37
|
+
dev = [
|
|
38
|
+
"pytest>=8.0",
|
|
39
|
+
"pytest-asyncio>=0.23",
|
|
40
|
+
"httpx>=0.27",
|
|
41
|
+
"ruff>=0.6",
|
|
42
|
+
]
|
|
43
|
+
docs = [
|
|
44
|
+
"mkdocs-material>=9.5",
|
|
45
|
+
"mkdocstrings[python]>=0.26",
|
|
46
|
+
]
|
|
47
|
+
all = ["vixar[las,mesh]"]
|
|
48
|
+
|
|
49
|
+
[project.scripts]
|
|
50
|
+
vixar = "vixar.cli:main"
|
|
51
|
+
|
|
52
|
+
[tool.hatch.build.targets.wheel]
|
|
53
|
+
packages = ["src/vixar"]
|
|
54
|
+
artifacts = ["src/vixar/static/viewer.js", "src/vixar/static/viewer.js.map"]
|
|
55
|
+
|
|
56
|
+
[tool.hatch.build.targets.sdist]
|
|
57
|
+
include = ["src/vixar", "README.md"]
|
|
58
|
+
|
|
59
|
+
[tool.pytest.ini_options]
|
|
60
|
+
testpaths = ["tests"]
|
|
61
|
+
asyncio_mode = "auto"
|
|
62
|
+
|
|
63
|
+
[tool.ruff]
|
|
64
|
+
line-length = 100
|
|
65
|
+
target-version = "py310"
|
|
66
|
+
|
|
67
|
+
[tool.ruff.lint]
|
|
68
|
+
select = ["E", "F", "I", "UP", "B"]
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Vixar — proprietary geoscientific 3D/2D visualization engine (Python library).
|
|
2
|
+
|
|
3
|
+
import vixar as vx
|
|
4
|
+
viewer = vx.Viewer()
|
|
5
|
+
viewer.add_point_cloud("survey.csv", color_by="elevation")
|
|
6
|
+
viewer.show()
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
__version__ = "1.0.0"
|
|
12
|
+
|
|
13
|
+
from vixar.layers import LayerRef
|
|
14
|
+
from vixar.schema import SCHEMA_VERSION, SceneConfig
|
|
15
|
+
from vixar.viewer import Viewer
|
|
16
|
+
|
|
17
|
+
__all__ = ["Viewer", "LayerRef", "SceneConfig", "SCHEMA_VERSION", "__version__"]
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Vixar command-line interface (plan §6).
|
|
2
|
+
|
|
3
|
+
Provides ``vixar tile`` for spatial pre-tiling (Phase 3) and
|
|
4
|
+
``vixar --version``.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import argparse
|
|
10
|
+
import sys
|
|
11
|
+
|
|
12
|
+
from vixar import __version__
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def main(argv: list[str] | None = None) -> int:
|
|
16
|
+
parser = argparse.ArgumentParser(prog="vixar", description="Vixar geoscientific viewer.")
|
|
17
|
+
parser.add_argument("--version", action="version", version=f"vixar {__version__}")
|
|
18
|
+
sub = parser.add_subparsers(dest="command")
|
|
19
|
+
|
|
20
|
+
tile_p = sub.add_parser(
|
|
21
|
+
"tile",
|
|
22
|
+
help="Split a LAS file into spatial tiles for streaming (Phase 3).",
|
|
23
|
+
)
|
|
24
|
+
tile_p.add_argument("input", help="Path to the source LAS file.")
|
|
25
|
+
tile_p.add_argument(
|
|
26
|
+
"--output", "-o", default="./tiles/",
|
|
27
|
+
help="Output directory for tile files and meta.json (default: ./tiles/).",
|
|
28
|
+
)
|
|
29
|
+
tile_p.add_argument(
|
|
30
|
+
"--tile-size", type=float, default=100.0,
|
|
31
|
+
help="Spatial cell size in metres (default: 100).",
|
|
32
|
+
)
|
|
33
|
+
tile_p.add_argument(
|
|
34
|
+
"--color-by",
|
|
35
|
+
help="Point attribute to encode as per-point values (e.g. elevation, intensity).",
|
|
36
|
+
)
|
|
37
|
+
tile_p.add_argument(
|
|
38
|
+
"--lod-factor", type=int, default=4,
|
|
39
|
+
help="Every N-th point is written to the coarse LOD tile (default: 4).",
|
|
40
|
+
)
|
|
41
|
+
tile_p.add_argument(
|
|
42
|
+
"--quiet", "-q", action="store_true",
|
|
43
|
+
help="Suppress progress output.",
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
args = parser.parse_args(argv)
|
|
47
|
+
|
|
48
|
+
if args.command == "tile":
|
|
49
|
+
try:
|
|
50
|
+
from vixar.tiler import tile_point_cloud
|
|
51
|
+
tile_point_cloud(
|
|
52
|
+
args.input,
|
|
53
|
+
output_dir=args.output,
|
|
54
|
+
tile_size=args.tile_size,
|
|
55
|
+
color_by=args.color_by,
|
|
56
|
+
lod_factor=args.lod_factor,
|
|
57
|
+
verbose=not args.quiet,
|
|
58
|
+
)
|
|
59
|
+
return 0
|
|
60
|
+
except ImportError as exc:
|
|
61
|
+
print(
|
|
62
|
+
f"Error: {exc}\n"
|
|
63
|
+
"Install the LAS dependency with: pip install 'vixar[las]'",
|
|
64
|
+
file=sys.stderr,
|
|
65
|
+
)
|
|
66
|
+
return 1
|
|
67
|
+
except Exception as exc: # noqa: BLE001
|
|
68
|
+
print(f"Error: {exc}", file=sys.stderr)
|
|
69
|
+
return 1
|
|
70
|
+
|
|
71
|
+
parser.print_help()
|
|
72
|
+
return 0
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
if __name__ == "__main__": # pragma: no cover
|
|
76
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Vixar exception hierarchy with clear, user-facing messages (plan Phase 1/2)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class VixarError(Exception):
|
|
7
|
+
"""Base class for all Vixar errors."""
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class InputValidationError(VixarError):
|
|
11
|
+
"""Raised for bad user input: missing columns, wrong shapes, bad files."""
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class LAZBackendError(VixarError):
|
|
15
|
+
"""Raised when a ``.laz`` file is read but no decompression backend is installed.
|
|
16
|
+
|
|
17
|
+
LAZ (LASzip-compressed LAS) is decompressed by ``laspy`` using a pluggable
|
|
18
|
+
backend. ``vixar[las]`` ships the pure-Python-installable ``lazrs`` backend;
|
|
19
|
+
this error means none is importable at read time.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(self, path: str | None = None) -> None:
|
|
23
|
+
where = f" ({path})" if path else ""
|
|
24
|
+
super().__init__(
|
|
25
|
+
f"Reading LAZ files{where} requires a LAZ decompression backend. "
|
|
26
|
+
"Install one with: pip install 'vixar[las]' (bundles 'lazrs'), "
|
|
27
|
+
"or pip install lazrs (or laszip)."
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class UnsupportedEPSGError(VixarError):
|
|
32
|
+
"""Raised when a CRS/EPSG code cannot be resolved by pyproj."""
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Standalone HTML generation (plan Phase 1: ``viewer.to_html``).
|
|
2
|
+
|
|
3
|
+
Produces a self-contained page: the bundled viewer.js inlined as a <script>,
|
|
4
|
+
and the scene config injected as ``window.__VIXAR_SCENE__`` so the engine
|
|
5
|
+
renders immediately with no server. Also reused as the anywidget iframe srcdoc.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from vixar.schema import SceneConfig
|
|
13
|
+
from vixar.static import CSP_META, read_viewer_js
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def render_html(config: SceneConfig, width: int = 1200, height: int = 800) -> str:
|
|
17
|
+
"""Return a complete standalone HTML document string for ``config``."""
|
|
18
|
+
viewer_js = read_viewer_js()
|
|
19
|
+
scene_json = config.to_json()
|
|
20
|
+
# Guard against an accidental </script> inside JSON breaking the inline tag.
|
|
21
|
+
scene_json = scene_json.replace("</", "<\\/")
|
|
22
|
+
return f"""<!doctype html>
|
|
23
|
+
<html lang="en">
|
|
24
|
+
<head>
|
|
25
|
+
<meta charset="utf-8" />
|
|
26
|
+
<meta http-equiv="Content-Security-Policy" content="{CSP_META}" />
|
|
27
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
28
|
+
<title>Vixar</title>
|
|
29
|
+
<style>
|
|
30
|
+
html, body {{ margin: 0; height: 100%; background: #12141c; }}
|
|
31
|
+
#vixar-root {{
|
|
32
|
+
position: absolute; inset: 0;
|
|
33
|
+
width: {width}px; height: {height}px; max-width: 100%;
|
|
34
|
+
}}
|
|
35
|
+
</style>
|
|
36
|
+
</head>
|
|
37
|
+
<body>
|
|
38
|
+
<div id="vixar-root"></div>
|
|
39
|
+
<script>window.__VIXAR_SCENE__ = {scene_json};</script>
|
|
40
|
+
<script>{viewer_js}</script>
|
|
41
|
+
</body>
|
|
42
|
+
</html>
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def write_html(config: SceneConfig, path: str, width: int = 1200, height: int = 800) -> str:
|
|
47
|
+
"""Write the standalone HTML to ``path`` and return the absolute path."""
|
|
48
|
+
out = Path(path)
|
|
49
|
+
out.write_text(render_html(config, width=width, height=height), encoding="utf-8")
|
|
50
|
+
return str(out.resolve())
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Data ingestion and coordinate handling for Vixar (plan Phase 1)."""
|
|
2
|
+
|
|
3
|
+
from vixar.io.coords import Origin, compute_origin, reproject_to_utm, subtract_origin
|
|
4
|
+
from vixar.io.csv_reader import read_borehole_csv, read_point_csv
|
|
5
|
+
from vixar.io.encode import encode_f32, encode_u32
|
|
6
|
+
from vixar.io.las_reader import read_las
|
|
7
|
+
from vixar.io.stats import AttributeStats, compute_stats
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"Origin",
|
|
11
|
+
"compute_origin",
|
|
12
|
+
"reproject_to_utm",
|
|
13
|
+
"subtract_origin",
|
|
14
|
+
"read_point_csv",
|
|
15
|
+
"read_borehole_csv",
|
|
16
|
+
"read_las",
|
|
17
|
+
"encode_f32",
|
|
18
|
+
"encode_u32",
|
|
19
|
+
"AttributeStats",
|
|
20
|
+
"compute_stats",
|
|
21
|
+
]
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Block model reader for regular-grid CSV block models (plan Phase 3).
|
|
2
|
+
|
|
3
|
+
Reads a CSV with block-centre coordinates (X, Y, Z) and attribute columns,
|
|
4
|
+
detects the regular grid dimensions from the unique coordinate values, and
|
|
5
|
+
returns the data in the shape expected by ``add_block_model``.
|
|
6
|
+
|
|
7
|
+
The output ``centers`` array is in source-CRS (float64 UTM) and is NOT yet
|
|
8
|
+
origin-subtracted — that happens at ``build_config`` time in ``Viewer``.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
|
|
15
|
+
import numpy as np
|
|
16
|
+
import pandas as pd
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class BlockModelData:
|
|
21
|
+
"""Regular-grid block model in source CRS."""
|
|
22
|
+
|
|
23
|
+
centers: np.ndarray # (N, 3) float64 block-centre X, Y, Z
|
|
24
|
+
values: dict[str, np.ndarray] = field(default_factory=dict)
|
|
25
|
+
# Detected uniform block spacing [dx, dy, dz].
|
|
26
|
+
block_size: tuple[float, float, float] = (1.0, 1.0, 1.0)
|
|
27
|
+
# Grid dimensions [nx, ny, nz] — number of cells per axis.
|
|
28
|
+
grid_dims: tuple[int, int, int] = (1, 1, 1)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def read_block_model(
|
|
32
|
+
source,
|
|
33
|
+
x_col: str = "x",
|
|
34
|
+
y_col: str = "y",
|
|
35
|
+
z_col: str = "z",
|
|
36
|
+
attribute_cols: list[str] | None = None,
|
|
37
|
+
) -> BlockModelData:
|
|
38
|
+
"""Read a regular block model from *source* (CSV path or DataFrame).
|
|
39
|
+
|
|
40
|
+
Grid dimensions are auto-detected from the unique sorted X/Y/Z values.
|
|
41
|
+
``attribute_cols`` lists extra columns to extract as scalar attributes; if
|
|
42
|
+
*None*, all non-coordinate columns are included.
|
|
43
|
+
"""
|
|
44
|
+
if isinstance(source, pd.DataFrame):
|
|
45
|
+
df = source
|
|
46
|
+
else:
|
|
47
|
+
df = pd.read_csv(source)
|
|
48
|
+
|
|
49
|
+
x = df[x_col].to_numpy(dtype="float64")
|
|
50
|
+
y = df[y_col].to_numpy(dtype="float64")
|
|
51
|
+
z = df[z_col].to_numpy(dtype="float64")
|
|
52
|
+
|
|
53
|
+
ux = np.unique(x)
|
|
54
|
+
uy = np.unique(y)
|
|
55
|
+
uz = np.unique(z)
|
|
56
|
+
nx, ny, nz = int(len(ux)), int(len(uy)), int(len(uz))
|
|
57
|
+
|
|
58
|
+
dx = float(np.median(np.diff(ux))) if nx > 1 else 1.0
|
|
59
|
+
dy = float(np.median(np.diff(uy))) if ny > 1 else 1.0
|
|
60
|
+
dz = float(np.median(np.diff(uz))) if nz > 1 else 1.0
|
|
61
|
+
|
|
62
|
+
centers = np.stack([x, y, z], axis=1) # (N, 3) float64
|
|
63
|
+
|
|
64
|
+
attr_names = attribute_cols if attribute_cols is not None else [
|
|
65
|
+
c for c in df.columns if c not in (x_col, y_col, z_col)
|
|
66
|
+
]
|
|
67
|
+
values = {
|
|
68
|
+
name: df[name].to_numpy(dtype="float32")
|
|
69
|
+
for name in attr_names
|
|
70
|
+
if name in df.columns
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return BlockModelData(
|
|
74
|
+
centers=centers,
|
|
75
|
+
values=values,
|
|
76
|
+
block_size=(dx, dy, dz),
|
|
77
|
+
grid_dims=(nx, ny, nz),
|
|
78
|
+
)
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""UTM coordinate strategy (plan §4.3).
|
|
2
|
+
|
|
3
|
+
Large UTM coordinates (~500,000 m) lose precision when cast to float32 in
|
|
4
|
+
WebGL. Vixar keeps full float64 precision in Python, computes a scene origin
|
|
5
|
+
(the bounding-box centroid), and subtracts it so the JS engine only ever sees
|
|
6
|
+
small, float32-safe local offsets. The absolute origin travels in the scene
|
|
7
|
+
config so picks/exports can be mapped back to real-world UTM.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
|
|
14
|
+
import numpy as np
|
|
15
|
+
|
|
16
|
+
from vixar.errors import UnsupportedEPSGError
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True)
|
|
20
|
+
class Origin:
|
|
21
|
+
easting: float
|
|
22
|
+
northing: float
|
|
23
|
+
elevation: float
|
|
24
|
+
epsg: int | None = None
|
|
25
|
+
|
|
26
|
+
def as_array(self) -> np.ndarray:
|
|
27
|
+
return np.array([self.easting, self.northing, self.elevation], dtype="float64")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def reproject_to_utm(
|
|
31
|
+
points: np.ndarray,
|
|
32
|
+
src_epsg: int,
|
|
33
|
+
dst_epsg: int | None = None,
|
|
34
|
+
) -> np.ndarray:
|
|
35
|
+
"""Reproject (N, 3) points from ``src_epsg`` to ``dst_epsg``.
|
|
36
|
+
|
|
37
|
+
When ``dst_epsg`` is None or equals the source, points are returned
|
|
38
|
+
unchanged (already in the working projected CRS). Z is passed through.
|
|
39
|
+
"""
|
|
40
|
+
if dst_epsg is None or dst_epsg == src_epsg:
|
|
41
|
+
return np.asarray(points, dtype="float64")
|
|
42
|
+
|
|
43
|
+
try:
|
|
44
|
+
from pyproj import Transformer
|
|
45
|
+
except ImportError as exc: # pragma: no cover - import guard
|
|
46
|
+
raise UnsupportedEPSGError(
|
|
47
|
+
"pyproj is required for reprojection but is not installed."
|
|
48
|
+
) from exc
|
|
49
|
+
|
|
50
|
+
try:
|
|
51
|
+
transformer = Transformer.from_crs(src_epsg, dst_epsg, always_xy=True)
|
|
52
|
+
except Exception as exc: # noqa: BLE001 - surface a clear message
|
|
53
|
+
raise UnsupportedEPSGError(
|
|
54
|
+
f"Could not build a transform from EPSG:{src_epsg} to EPSG:{dst_epsg}: {exc}"
|
|
55
|
+
) from exc
|
|
56
|
+
|
|
57
|
+
pts = np.asarray(points, dtype="float64")
|
|
58
|
+
x, y = transformer.transform(pts[:, 0], pts[:, 1])
|
|
59
|
+
out = pts.copy()
|
|
60
|
+
out[:, 0] = x
|
|
61
|
+
out[:, 1] = y
|
|
62
|
+
return out
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def compute_origin(points_utm: np.ndarray, epsg: int | None = None) -> Origin:
|
|
66
|
+
"""Compute the scene origin as the bounding-box centroid of the points."""
|
|
67
|
+
pts = np.asarray(points_utm, dtype="float64")
|
|
68
|
+
if pts.ndim != 2 or pts.shape[1] != 3:
|
|
69
|
+
raise ValueError("compute_origin expects an (N, 3) array")
|
|
70
|
+
lo = pts.min(axis=0)
|
|
71
|
+
hi = pts.max(axis=0)
|
|
72
|
+
center = (lo + hi) * 0.5
|
|
73
|
+
return Origin(
|
|
74
|
+
easting=float(center[0]),
|
|
75
|
+
northing=float(center[1]),
|
|
76
|
+
elevation=float(center[2]),
|
|
77
|
+
epsg=epsg,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def subtract_origin(points_utm: np.ndarray, origin: Origin) -> np.ndarray:
|
|
82
|
+
"""Subtract the origin and downcast to float32 local coordinates."""
|
|
83
|
+
pts = np.asarray(points_utm, dtype="float64") - origin.as_array()
|
|
84
|
+
return pts.astype("float32")
|