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 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")