scigraphs-engine 0.1.1__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 (69) hide show
  1. scigraphs_engine-0.1.1/LICENSE +21 -0
  2. scigraphs_engine-0.1.1/PKG-INFO +103 -0
  3. scigraphs_engine-0.1.1/README.md +47 -0
  4. scigraphs_engine-0.1.1/pyproject.toml +53 -0
  5. scigraphs_engine-0.1.1/scigraphs_engine/__init__.py +50 -0
  6. scigraphs_engine-0.1.1/scigraphs_engine/_camera_math.py +85 -0
  7. scigraphs_engine-0.1.1/scigraphs_engine/adaptive.py +468 -0
  8. scigraphs_engine-0.1.1/scigraphs_engine/api.py +1284 -0
  9. scigraphs_engine-0.1.1/scigraphs_engine/backends/__init__.py +4 -0
  10. scigraphs_engine-0.1.1/scigraphs_engine/backends/wgpu/__init__.py +31 -0
  11. scigraphs_engine-0.1.1/scigraphs_engine/backends/wgpu/buffers.py +232 -0
  12. scigraphs_engine-0.1.1/scigraphs_engine/backends/wgpu/camera.py +148 -0
  13. scigraphs_engine-0.1.1/scigraphs_engine/backends/wgpu/device.py +108 -0
  14. scigraphs_engine-0.1.1/scigraphs_engine/backends/wgpu/pipelines.py +180 -0
  15. scigraphs_engine-0.1.1/scigraphs_engine/backends/wgpu/render.py +250 -0
  16. scigraphs_engine-0.1.1/scigraphs_engine/backends/wgpu/renderer.py +257 -0
  17. scigraphs_engine-0.1.1/scigraphs_engine/backends/wgpu/shaders.py +381 -0
  18. scigraphs_engine-0.1.1/scigraphs_engine/backends/wgpu/target.py +115 -0
  19. scigraphs_engine-0.1.1/scigraphs_engine/backends/wgpu/wgsl/arrow.json +78 -0
  20. scigraphs_engine-0.1.1/scigraphs_engine/backends/wgpu/wgsl/arrow.wgsl +76 -0
  21. scigraphs_engine-0.1.1/scigraphs_engine/backends/wgpu/wgsl/arrow_flat.json +48 -0
  22. scigraphs_engine-0.1.1/scigraphs_engine/backends/wgpu/wgsl/arrow_flat.wgsl +82 -0
  23. scigraphs_engine-0.1.1/scigraphs_engine/backends/wgpu/wgsl/heb_line.json +81 -0
  24. scigraphs_engine-0.1.1/scigraphs_engine/backends/wgpu/wgsl/heb_line.wgsl +137 -0
  25. scigraphs_engine-0.1.1/scigraphs_engine/backends/wgpu/wgsl/line_f.json +108 -0
  26. scigraphs_engine-0.1.1/scigraphs_engine/backends/wgpu/wgsl/line_f.wgsl +115 -0
  27. scigraphs_engine-0.1.1/scigraphs_engine/backends/wgpu/wgsl/ribbon.json +93 -0
  28. scigraphs_engine-0.1.1/scigraphs_engine/backends/wgpu/wgsl/ribbon.wgsl +131 -0
  29. scigraphs_engine-0.1.1/scigraphs_engine/backends/wgpu/wgsl/ribbon_f.json +162 -0
  30. scigraphs_engine-0.1.1/scigraphs_engine/backends/wgpu/wgsl/ribbon_f.wgsl +204 -0
  31. scigraphs_engine-0.1.1/scigraphs_engine/backends/wgpu/wgsl/ribbon_id.json +54 -0
  32. scigraphs_engine-0.1.1/scigraphs_engine/backends/wgpu/wgsl/ribbon_id.wgsl +103 -0
  33. scigraphs_engine-0.1.1/scigraphs_engine/backends/wgpu/wgsl/round_point.json +22 -0
  34. scigraphs_engine-0.1.1/scigraphs_engine/backends/wgpu/wgsl/round_point.wgsl +68 -0
  35. scigraphs_engine-0.1.1/scigraphs_engine/backends/wgpu/wgsl/round_point_f.json +118 -0
  36. scigraphs_engine-0.1.1/scigraphs_engine/backends/wgpu/wgsl/round_point_f.wgsl +141 -0
  37. scigraphs_engine-0.1.1/scigraphs_engine/backends/wgpu/wgsl/smooth_color.json +32 -0
  38. scigraphs_engine-0.1.1/scigraphs_engine/backends/wgpu/wgsl/smooth_color.wgsl +40 -0
  39. scigraphs_engine-0.1.1/scigraphs_engine/backends/wgpu/wgsl/sphere.json +83 -0
  40. scigraphs_engine-0.1.1/scigraphs_engine/backends/wgpu/wgsl/sphere.wgsl +115 -0
  41. scigraphs_engine-0.1.1/scigraphs_engine/backends/wgpu/wgsl/sphere_f.json +152 -0
  42. scigraphs_engine-0.1.1/scigraphs_engine/backends/wgpu/wgsl/sphere_f.wgsl +185 -0
  43. scigraphs_engine-0.1.1/scigraphs_engine/backends/wgpu/wgsl/sphere_id.json +44 -0
  44. scigraphs_engine-0.1.1/scigraphs_engine/backends/wgpu/wgsl/sphere_id.wgsl +80 -0
  45. scigraphs_engine-0.1.1/scigraphs_engine/backends/wgpu/wgsl/uniform_color.json +37 -0
  46. scigraphs_engine-0.1.1/scigraphs_engine/backends/wgpu/wgsl/uniform_color.wgsl +42 -0
  47. scigraphs_engine-0.1.1/scigraphs_engine/blocks.py +76 -0
  48. scigraphs_engine-0.1.1/scigraphs_engine/bundling/__init__.py +11 -0
  49. scigraphs_engine-0.1.1/scigraphs_engine/bundling/fdeb.py +310 -0
  50. scigraphs_engine-0.1.1/scigraphs_engine/bundling/mingle.py +480 -0
  51. scigraphs_engine-0.1.1/scigraphs_engine/bundling/routed.py +420 -0
  52. scigraphs_engine-0.1.1/scigraphs_engine/bundling/sbeb.py +292 -0
  53. scigraphs_engine-0.1.1/scigraphs_engine/channels.py +91 -0
  54. scigraphs_engine-0.1.1/scigraphs_engine/communities.py +125 -0
  55. scigraphs_engine-0.1.1/scigraphs_engine/edge_styles.py +566 -0
  56. scigraphs_engine-0.1.1/scigraphs_engine/filters.py +754 -0
  57. scigraphs_engine-0.1.1/scigraphs_engine/gpu_compute.py +17 -0
  58. scigraphs_engine-0.1.1/scigraphs_engine/lod.py +89 -0
  59. scigraphs_engine-0.1.1/scigraphs_engine/mesh.py +480 -0
  60. scigraphs_engine-0.1.1/scigraphs_engine/palette.py +68 -0
  61. scigraphs_engine-0.1.1/scigraphs_engine/settings.py +151 -0
  62. scigraphs_engine-0.1.1/scigraphs_engine/simplify.py +352 -0
  63. scigraphs_engine-0.1.1/scigraphs_engine/source.py +143 -0
  64. scigraphs_engine-0.1.1/scigraphs_engine.egg-info/PKG-INFO +103 -0
  65. scigraphs_engine-0.1.1/scigraphs_engine.egg-info/SOURCES.txt +67 -0
  66. scigraphs_engine-0.1.1/scigraphs_engine.egg-info/dependency_links.txt +1 -0
  67. scigraphs_engine-0.1.1/scigraphs_engine.egg-info/requires.txt +19 -0
  68. scigraphs_engine-0.1.1/scigraphs_engine.egg-info/top_level.txt +1 -0
  69. scigraphs_engine-0.1.1/setup.cfg +4 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 José Marín
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,103 @@
1
+ Metadata-Version: 2.4
2
+ Name: scigraphs-engine
3
+ Version: 0.1.1
4
+ Summary: Blender-free graph visualization engine: filters, backbones, bundling, PNG render.
5
+ Author: José Marín
6
+ License: MIT License
7
+
8
+ Copyright (c) 2026 José Marín
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Project-URL: Homepage, https://github.com/SciBlend/SciGraphs
29
+ Project-URL: Repository, https://github.com/SciBlend/SciGraphs
30
+ Project-URL: Issues, https://github.com/SciBlend/SciGraphs/issues
31
+ Project-URL: Documentation, https://github.com/SciBlend/SciGraphs/tree/main/docs
32
+ Keywords: graph,network,visualization,rendering,layout
33
+ Classifier: Development Status :: 3 - Alpha
34
+ Classifier: Intended Audience :: Science/Research
35
+ Classifier: License :: OSI Approved :: MIT License
36
+ Classifier: Programming Language :: Python :: 3
37
+ Classifier: Topic :: Scientific/Engineering :: Visualization
38
+ Requires-Python: >=3.10
39
+ Description-Content-Type: text/markdown
40
+ License-File: LICENSE
41
+ Requires-Dist: numpy>=1.23
42
+ Provides-Extra: igraph
43
+ Requires-Dist: python-igraph>=0.10; extra == "igraph"
44
+ Provides-Extra: scipy
45
+ Requires-Dist: scipy>=1.9; extra == "scipy"
46
+ Provides-Extra: networkx
47
+ Requires-Dist: networkx>=2.8; extra == "networkx"
48
+ Provides-Extra: all
49
+ Requires-Dist: python-igraph>=0.10; extra == "all"
50
+ Requires-Dist: scipy>=1.9; extra == "all"
51
+ Requires-Dist: networkx>=2.8; extra == "all"
52
+ Provides-Extra: wgpu
53
+ Requires-Dist: wgpu>=0.32; extra == "wgpu"
54
+ Requires-Dist: pillow>=9; extra == "wgpu"
55
+ Dynamic: license-file
56
+
57
+ # scigraphs-engine
58
+
59
+ Graph visualization as a plain Python package. Coordinates, an edge list and
60
+ whatever attributes they carry go in; geometry or pixels come out, with 26
61
+ filter channels, structural sparsification, edge-style tessellation and
62
+ hierarchical bundling in between.
63
+
64
+ ```python
65
+ from scigraphs_engine import Graph
66
+
67
+ g = Graph.from_arrays(coords, edges)
68
+ g = g.filter(core__gte=0.5).simplify(backbone="disparity", alpha=0.05)
69
+ g.style(edges="bundled").render(size=(1920, 1080)).save("out.png")
70
+ ```
71
+
72
+ Nothing in this package imports `bpy`, `gpu`, `bmesh` or `mathutils`. The
73
+ compute half needs only `numpy`, and `Graph.geometry()` runs with no graphics
74
+ library installed at all, returning `MeshSpec`s that name their shader as a
75
+ string, so any renderer can draw them. The GPU half is an optional extra.
76
+
77
+ ## Install
78
+
79
+ ```bash
80
+ pip install scigraphs-engine # numpy only
81
+ pip install "scigraphs-engine[igraph]" # coreness, PageRank, betweenness, bridges
82
+ pip install "scigraphs-engine[scipy]" # components, hop distance, crowding
83
+ pip install "scigraphs-engine[wgpu]" # render() to pixels
84
+ pip install "scigraphs-engine[all]" # everything
85
+ ```
86
+
87
+ Requires Python 3.10 or newer. A missing extra makes individual channels
88
+ unavailable rather than breaking anything: `available_channels()` returns all 26
89
+ with a reason for each one that cannot run.
90
+
91
+ ## Documentation
92
+
93
+ https://github.com/SciBlend/SciGraphs/tree/main/docs
94
+
95
+ ```bash
96
+ python engine/scigraphs_engine/tests/run.py # -v for full output
97
+ ```
98
+
99
+ ## License
100
+
101
+ MIT, see `LICENSE`. The SciGraphs Blender add-on is GPLv3 and is one caller of
102
+ this package, not its owner. It reaches the package through a shim that prefers
103
+ an installed `scigraphs_engine` over any copy shipped alongside it.
@@ -0,0 +1,47 @@
1
+ # scigraphs-engine
2
+
3
+ Graph visualization as a plain Python package. Coordinates, an edge list and
4
+ whatever attributes they carry go in; geometry or pixels come out, with 26
5
+ filter channels, structural sparsification, edge-style tessellation and
6
+ hierarchical bundling in between.
7
+
8
+ ```python
9
+ from scigraphs_engine import Graph
10
+
11
+ g = Graph.from_arrays(coords, edges)
12
+ g = g.filter(core__gte=0.5).simplify(backbone="disparity", alpha=0.05)
13
+ g.style(edges="bundled").render(size=(1920, 1080)).save("out.png")
14
+ ```
15
+
16
+ Nothing in this package imports `bpy`, `gpu`, `bmesh` or `mathutils`. The
17
+ compute half needs only `numpy`, and `Graph.geometry()` runs with no graphics
18
+ library installed at all, returning `MeshSpec`s that name their shader as a
19
+ string, so any renderer can draw them. The GPU half is an optional extra.
20
+
21
+ ## Install
22
+
23
+ ```bash
24
+ pip install scigraphs-engine # numpy only
25
+ pip install "scigraphs-engine[igraph]" # coreness, PageRank, betweenness, bridges
26
+ pip install "scigraphs-engine[scipy]" # components, hop distance, crowding
27
+ pip install "scigraphs-engine[wgpu]" # render() to pixels
28
+ pip install "scigraphs-engine[all]" # everything
29
+ ```
30
+
31
+ Requires Python 3.10 or newer. A missing extra makes individual channels
32
+ unavailable rather than breaking anything: `available_channels()` returns all 26
33
+ with a reason for each one that cannot run.
34
+
35
+ ## Documentation
36
+
37
+ https://github.com/SciBlend/SciGraphs/tree/main/docs
38
+
39
+ ```bash
40
+ python engine/scigraphs_engine/tests/run.py # -v for full output
41
+ ```
42
+
43
+ ## License
44
+
45
+ MIT, see `LICENSE`. The SciGraphs Blender add-on is GPLv3 and is one caller of
46
+ this package, not its owner. It reaches the package through a shim that prefers
47
+ an installed `scigraphs_engine` over any copy shipped alongside it.
@@ -0,0 +1,53 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "scigraphs-engine"
7
+ version = "0.1.1"
8
+ description = "Blender-free graph visualization engine: filters, backbones, bundling, PNG render."
9
+ readme = "README.md"
10
+ # Floor matches Blender 4.1+ / 5.x Python.
11
+ requires-python = ">=3.10"
12
+ license = { file = "LICENSE" }
13
+ authors = [{ name = "José Marín" }]
14
+ keywords = ["graph", "network", "visualization", "rendering", "layout"]
15
+ classifiers = [
16
+ "Development Status :: 3 - Alpha",
17
+ "Intended Audience :: Science/Research",
18
+ "License :: OSI Approved :: MIT License",
19
+ "Programming Language :: Python :: 3",
20
+ "Topic :: Scientific/Engineering :: Visualization",
21
+ ]
22
+
23
+ # Only hard dep. igraph/scipy/networkx are optional and degrade to None.
24
+ dependencies = ["numpy>=1.23"]
25
+
26
+ [project.urls]
27
+ Homepage = "https://github.com/SciBlend/SciGraphs"
28
+ Repository = "https://github.com/SciBlend/SciGraphs"
29
+ Issues = "https://github.com/SciBlend/SciGraphs/issues"
30
+ Documentation = "https://github.com/SciBlend/SciGraphs/tree/main/docs"
31
+
32
+ [project.optional-dependencies]
33
+ igraph = ["python-igraph>=0.10"]
34
+ scipy = ["scipy>=1.9"]
35
+ networkx = ["networkx>=2.8"]
36
+ all = ["python-igraph>=0.10", "scipy>=1.9", "networkx>=2.8"]
37
+ # Only needed for Graph.render(); import stays lazy.
38
+ wgpu = ["wgpu>=0.32", "pillow>=9"]
39
+
40
+ [tool.setuptools]
41
+ # Listed one by one rather than discovered, so a new subpackage has to be added
42
+ # here on purpose. Leaving one out ships a wheel that imports and then fails on
43
+ # the first call into the missing module.
44
+ packages = [
45
+ "scigraphs_engine",
46
+ "scigraphs_engine.backends",
47
+ "scigraphs_engine.backends.wgpu",
48
+ "scigraphs_engine.bundling",
49
+ ]
50
+ license-files = ["LICENSE"]
51
+
52
+ [tool.setuptools.package-data]
53
+ "scigraphs_engine.backends.wgpu" = ["wgsl/*.wgsl", "wgsl/*.json"]
@@ -0,0 +1,50 @@
1
+ # Nothing in this package may import bpy, gpu, bmesh or mathutils; tests in the
2
+ # SciGraphs repository parse this directory for those names. Everything below
3
+ # numpy is lazy, so a MeshSpec caller never pays for igraph and scipy.
4
+
5
+ import importlib as _importlib
6
+
7
+ __version__ = "0.1.1"
8
+
9
+ from .mesh import ( # noqa: F401
10
+ DrawState, MeshGroup, MeshSpec, ShaderRef, LINES, POINTS, TRIS,
11
+ )
12
+ from .settings import Clause, Settings, PREFIX # noqa: F401
13
+ from .source import ArraySource, GraphSource, EDGE, POINT # noqa: F401
14
+
15
+ _LAZY = (
16
+ "adaptive", "api", "blocks", "channels", "communities", "edge_styles",
17
+ "filters", "gpu_compute", "lod", "mesh", "palette", "settings", "simplify",
18
+ "source",
19
+ )
20
+
21
+ # Lets `from scigraphs_engine import Graph` resolve without importing api.
22
+ _API_NAMES = (
23
+ "BackendUnavailable", "Camera", "ChannelResult", "ChannelSpec",
24
+ "ChannelUnavailable", "Condition", "EngineError", "Geometry", "Graph",
25
+ "Image", "Report", "UnknownChannel", "channel", "channel_names",
26
+ "channel_specs", "colormap_names", "gpu_available",
27
+ )
28
+
29
+ __all__ = [
30
+ "ArraySource", "Clause", "DrawState", "EDGE", "GraphSource", "LINES",
31
+ "MeshGroup", "MeshSpec", "POINT", "POINTS", "PREFIX", "Settings",
32
+ "ShaderRef", "TRIS", "__version__",
33
+ ] + list(_API_NAMES) + list(_LAZY)
34
+
35
+
36
+ def __getattr__(name):
37
+ """Import a submodule or a public API name on first access (PEP 562)."""
38
+ if name in _LAZY:
39
+ module = _importlib.import_module("." + name, __name__)
40
+ globals()[name] = module
41
+ return module
42
+ if name in _API_NAMES:
43
+ value = getattr(_importlib.import_module(".api", __name__), name)
44
+ globals()[name] = value
45
+ return value
46
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
47
+
48
+
49
+ def __dir__():
50
+ return sorted(set(globals()) | set(_LAZY) | set(_API_NAMES))
@@ -0,0 +1,85 @@
1
+ # Transcribed from backends/wgpu/camera.py, held float32-exact against it by
2
+ # tests/test_camera.py. Depth is z in [0, 1] (WebGPU), not OpenGL's.
3
+
4
+ import numpy as np
5
+
6
+
7
+ def _normalize(v):
8
+ v = np.asarray(v, dtype=np.float64)
9
+ n = np.linalg.norm(v)
10
+ return v / n if n > 1e-12 else v
11
+
12
+
13
+ def look_at(eye, target, up=(0.0, 0.0, 1.0)):
14
+ """World -> view, right-handed, looking down -z. +Z up like Blender."""
15
+ eye = np.asarray(eye, dtype=np.float64)
16
+ f = _normalize(np.asarray(target, dtype=np.float64) - eye)
17
+ up = _normalize(up)
18
+ if abs(float(np.dot(f, up))) > 0.9999:
19
+ up = np.array([0.0, 1.0, 0.0]) if abs(f[1]) < 0.9 else \
20
+ np.array([1.0, 0.0, 0.0])
21
+ s = _normalize(np.cross(f, up))
22
+ u = np.cross(s, f)
23
+ m = np.eye(4, dtype=np.float64)
24
+ m[0, :3], m[1, :3], m[2, :3] = s, u, -f
25
+ m[0, 3] = -float(np.dot(s, eye))
26
+ m[1, 3] = -float(np.dot(u, eye))
27
+ m[2, 3] = float(np.dot(f, eye))
28
+ return m.astype(np.float32)
29
+
30
+
31
+ def perspective(fov_y_deg, aspect, near, far):
32
+ """View -> clip with z in [0, 1]."""
33
+ f = 1.0 / np.tan(np.radians(float(fov_y_deg)) * 0.5)
34
+ m = np.zeros((4, 4), dtype=np.float64)
35
+ m[0, 0] = f / float(aspect)
36
+ m[1, 1] = f
37
+ m[2, 2] = far / (near - far)
38
+ m[2, 3] = (far * near) / (near - far)
39
+ m[3, 2] = -1.0
40
+ return m.astype(np.float32)
41
+
42
+
43
+ def bounding_sphere(coords):
44
+ """(center, radius) of the box's circumsphere. Over-frames a flat layout."""
45
+ coords = np.asarray(coords, dtype=np.float32).reshape(-1, 3)
46
+ if coords.size == 0:
47
+ return np.zeros(3, dtype=np.float32), 1.0
48
+ lo = coords.min(axis=0)
49
+ hi = coords.max(axis=0)
50
+ center = (lo + hi) * 0.5
51
+ radius = float(np.linalg.norm(hi - lo)) * 0.5
52
+ return center, (radius if radius > 1e-6 else 1.0)
53
+
54
+
55
+ def fit_view(coords, size, fov_y_deg=45.0, margin=1.15,
56
+ direction=(0.0, -1.0, 0.35), up=(0.0, 0.0, 1.0)):
57
+ """A resolved api.Camera framing all of ``coords`` at ``size``."""
58
+ from .api import Camera
59
+
60
+ width, height = size
61
+ center, radius = bounding_sphere(coords)
62
+ aspect = float(width) / float(height)
63
+ # Fit the tighter axis: vertical-only fitting crops a portrait render.
64
+ half_v = np.radians(float(fov_y_deg)) * 0.5
65
+ half_h = np.arctan(np.tan(half_v) * aspect)
66
+ dist = radius * float(margin) / np.sin(min(half_v, half_h))
67
+
68
+ eye = center - _normalize(direction).astype(np.float32) * dist
69
+ near = max(dist - radius * 2.0, dist * 1e-3)
70
+ far = dist + radius * 4.0
71
+ return Camera(look_at(eye, center, up),
72
+ perspective(fov_y_deg, aspect, near, far), near, far)
73
+
74
+
75
+ def look_at_camera(eye, target, up, fov_y_deg, size, near=None, far=None):
76
+ from .api import Camera
77
+
78
+ width, height = size
79
+ eye = np.asarray(eye, dtype=np.float64)
80
+ span = float(np.linalg.norm(np.asarray(target, dtype=np.float64) - eye))
81
+ near = float(near) if near is not None else max(span * 1e-3, 1e-4)
82
+ far = float(far) if far is not None else max(span * 10.0, near * 100.0)
83
+ return Camera(look_at(eye, target, up),
84
+ perspective(fov_y_deg, float(width) / float(height),
85
+ near, far), near, far)