Status: Pending Implementation Priority: Low-Medium (stable, quality improvements needed) Date: January 2026
The crystal-renderer package is production-ready (8/10 quality). It successfully converts crystal geometries to SVG, STL, and glTF formats with proper type annotations and clean architecture. Key areas for improvement are test coverage expansion and performance optimization.
The package has no blocking issues. All core functionality works correctly.
Current Coverage: ~60% (STL, glTF, projection fully tested; visualization untested)
Missing Test Categories:
| Module | Coverage | Gap |
|---|---|---|
visualization.py |
0% | High-level SVG generation |
conversion.py |
0% | SVG→raster (requires cairosvg) |
rendering.py drawing functions |
~30% | matplotlib-dependent functions |
Proposed Tests for visualization.py:
# tests/test_visualization.py
import pytest
from pathlib import Path
import tempfile
class TestVisualization:
def test_generate_cdl_svg_basic(self):
"""Test basic CDL to SVG generation."""
from crystal_renderer import generate_cdl_svg
with tempfile.NamedTemporaryFile(suffix='.svg', delete=False) as f:
output_path = Path(f.name)
result = generate_cdl_svg("cubic[m3m]:{111}", output_path)
assert result.exists()
assert result.stat().st_size > 0
content = result.read_text()
assert '<svg' in content
assert '</svg>' in content
output_path.unlink()
def test_generate_cdl_svg_with_axes(self):
"""Test SVG with crystallographic axes."""
from crystal_renderer import generate_cdl_svg
with tempfile.NamedTemporaryFile(suffix='.svg', delete=False) as f:
output_path = Path(f.name)
result = generate_cdl_svg(
"cubic[m3m]:{111}",
output_path,
show_axes=True
)
content = result.read_text()
# Axes should add arrow elements
assert 'a' in content or 'quiver' in content.lower()
output_path.unlink()
def test_generate_geometry_svg(self):
"""Test raw geometry SVG generation."""
import numpy as np
from crystal_renderer import generate_geometry_svg
# Simple tetrahedron
vertices = np.array([
[1, 1, 1], [-1, -1, 1], [-1, 1, -1], [1, -1, -1]
], dtype=float)
faces = [[0, 1, 2], [0, 1, 3], [0, 2, 3], [1, 2, 3]]
with tempfile.NamedTemporaryFile(suffix='.svg', delete=False) as f:
output_path = Path(f.name)
result = generate_geometry_svg(vertices, faces, output_path)
assert result.exists()
output_path.unlink()Issue: GEMCAD export mentioned in docstrings but not implemented.
Location: Referenced but absent from formats/ directory.
Proposed Implementation:
# formats/gemcad.py
"""GEMCAD format export for crystal geometry."""
from pathlib import Path
from typing import List
import numpy as np
def geometry_to_gemcad(
vertices: np.ndarray,
faces: List[List[int]],
name: str = "crystal"
) -> str:
"""Convert geometry to GEMCAD ASC format.
GEMCAD is a gemstone faceting design program.
ASC format is its native ASCII format.
Format structure:
- Header with gem name
- Vertex count and vertices
- Face count and face definitions
"""
lines = []
lines.append(f"# GEMCAD ASC file generated by crystal-renderer")
lines.append(f"# Name: {name}")
lines.append("")
lines.append(f"VERTICES {len(vertices)}")
for i, v in enumerate(vertices):
lines.append(f" {i+1} {v[0]:.6f} {v[1]:.6f} {v[2]:.6f}")
lines.append("")
lines.append(f"FACETS {len(faces)}")
for i, face in enumerate(faces):
vertex_refs = " ".join(str(v+1) for v in face)
lines.append(f" {i+1} {vertex_refs}")
return "\n".join(lines)
def export_gemcad(
vertices: np.ndarray,
faces: List[List[int]],
output_path: Path | str,
name: str = "crystal"
) -> Path:
"""Export geometry to GEMCAD ASC file."""
output_path = Path(output_path)
content = geometry_to_gemcad(vertices, faces, name)
output_path.write_text(content)
return output_pathIssue: glTF only supports single material per mesh.
Impact: Multi-form crystals lose color differentiation in 3D exports.
Proposed Solution: Split into multiple meshes (one per form):
def geometry_to_gltf_multicolor(
vertices: np.ndarray,
faces: List[List[int]],
face_colors: List[str] | None = None,
name: str = "crystal"
) -> dict:
"""Generate glTF with per-form coloring via multiple meshes."""
if face_colors is None:
return geometry_to_gltf(vertices, faces, name=name)
# Group faces by color
from collections import defaultdict
color_groups = defaultdict(list)
for i, face in enumerate(faces):
color = face_colors[i] if i < len(face_colors) else "#808080"
color_groups[color].append(face)
# Create mesh per color group
nodes = []
meshes = []
materials = []
for color_idx, (color, group_faces) in enumerate(color_groups.items()):
# Extract vertices used by this group
# Build submesh...
pass # Implementation details
# Combine into single glTF
return gltf_dictIssue: Vertex visibility recalculated on every render.
Impact: Slow for animations or multiple views.
Proposed Fix:
# projection.py
from functools import lru_cache
@lru_cache(maxsize=128)
def calculate_vertex_visibility_cached(
vertices_hash: int, # hash of vertices.tobytes()
faces_hash: int, # hash of tuple(tuple(f) for f in faces)
elev: float,
azim: float,
threshold: float = 0.1
) -> tuple:
"""Cached visibility calculation."""
# Reconstruct from cache key...
pass
def calculate_vertex_visibility(
vertices: np.ndarray,
faces: List[List[int]],
elev: float,
azim: float,
threshold: float = 0.1
) -> np.ndarray:
"""Calculate with caching for repeated calls."""
v_hash = hash(vertices.tobytes())
f_hash = hash(tuple(tuple(f) for f in faces))
return np.array(calculate_vertex_visibility_cached(
v_hash, f_hash, elev, azim, threshold
))Issue: No Wavefront OBJ export (simpler than glTF, widely compatible).
Proposed Implementation:
# formats/obj.py
"""Wavefront OBJ format export."""
def geometry_to_obj(
vertices: np.ndarray,
faces: List[List[int]],
name: str = "crystal"
) -> str:
"""Convert to OBJ format string."""
lines = [f"# {name}", f"o {name}", ""]
# Vertices
for v in vertices:
lines.append(f"v {v[0]:.6f} {v[1]:.6f} {v[2]:.6f}")
lines.append("")
# Faces (1-indexed)
for face in faces:
indices = " ".join(str(i+1) for i in face)
lines.append(f"f {indices}")
return "\n".join(lines)Issue: Only elevation/azimuth angles, no arbitrary rotation matrix.
Proposed Enhancement:
# projection.py
def rotation_matrix_from_angles(elev: float, azim: float) -> np.ndarray:
"""Convert elevation/azimuth to rotation matrix."""
elev_rad = np.radians(elev)
azim_rad = np.radians(azim)
# Rotation around z (azimuth), then around x' (elevation)
Rz = np.array([
[np.cos(azim_rad), -np.sin(azim_rad), 0],
[np.sin(azim_rad), np.cos(azim_rad), 0],
[0, 0, 1]
])
Rx = np.array([
[1, 0, 0],
[0, np.cos(elev_rad), -np.sin(elev_rad)],
[0, np.sin(elev_rad), np.cos(elev_rad)]
])
return Rx @ Rz
def apply_rotation(
vertices: np.ndarray,
rotation: np.ndarray | tuple[float, float]
) -> np.ndarray:
"""Apply rotation to vertices.
Args:
vertices: Nx3 vertex array
rotation: 3x3 matrix or (elev, azim) tuple
"""
if isinstance(rotation, tuple):
R = rotation_matrix_from_angles(*rotation)
else:
R = rotation
return vertices @ R.TIssue: Generic ValueError/ImportError used everywhere.
Proposed Custom Exceptions:
# exceptions.py
class CrystalRendererError(Exception):
"""Base exception for crystal-renderer."""
pass
class InvalidGeometryError(CrystalRendererError):
"""Raised when geometry is invalid."""
pass
class UnsupportedFormatError(CrystalRendererError):
"""Raised for unsupported export formats."""
pass
class DependencyMissingError(CrystalRendererError):
"""Raised when optional dependency is missing."""
def __init__(self, package: str, feature: str):
self.package = package
self.feature = feature
super().__init__(
f"Package '{package}' required for {feature}. "
f"Install with: pip install {package}"
)Issue: O(n²) brute-force distance check for bonds.
Location: rendering.py:draw_bonds()
Proposed Fix:
from scipy.spatial import cKDTree
def draw_bonds_optimized(ax, atoms, cutoff: float = 3.0):
"""Draw bonds using KD-tree for efficiency."""
positions = atoms.get_positions()
tree = cKDTree(positions)
pairs = tree.query_pairs(r=cutoff)
for i, j in pairs:
# Draw bond...
passIssue: No simplification for complex meshes.
Proposed Design:
def simplify_geometry(
vertices: np.ndarray,
faces: List[List[int]],
target_faces: int
) -> tuple[np.ndarray, List[List[int]]]:
"""Reduce mesh complexity for rendering.
Uses edge collapse algorithm to simplify
while preserving crystal form.
"""
# Would require implementing edge collapse
# or using external library (e.g., pymeshlab)
passIssue: No Sphinx documentation.
Proposed Structure:
docs/
├── conf.py
├── index.rst
├── api/
│ ├── visualization.rst
│ ├── projection.rst
│ ├── formats.rst
│ └── info_panel.rst
├── examples/
│ ├── basic_svg.rst
│ ├── 3d_export.rst
│ └── custom_colors.rst
└── performance.rst
For CDL v2 ~ operator (crystal aggregates):
def render_aggregate(
components: List[CrystalGeometry],
arrangement: str, # 'parallel', 'radial', 'cluster'
output_path: Path
) -> Path:
"""Render multiple crystal components as aggregate."""
# Combine all vertices with offset
# Render with unique colors per component
passFor CDL v2 | twin(law) modifier:
def render_twinned_crystal(
geometry: CrystalGeometry,
twin_law: str,
count: int = 2
) -> CrystalGeometry:
"""Generate twinned crystal geometry."""
from crystal_geometry.twinning import apply_twin
components = apply_twin(geometry, twin_law, count)
# Merge with distinct colors per individual
return merged_geometryFor CDL v2 features ([phantom:N], [smoky], etc.):
def render_with_features(
geometry: CrystalGeometry,
features: dict
) -> None:
"""Apply visual features to crystal rendering.
Features:
- phantom:N - Render N internal phantom layers
- smoky - Apply grey gradient
- colour:name - Apply specific coloring
- silk - Add needle inclusion texture
"""
pass| Priority | Task | Effort | Impact |
|---|---|---|---|
| P1 | Add visualization.py tests | 4 hours | Test coverage |
| P1 | Implement GEMCAD export | 2 hours | Feature completeness |
| P2 | Add OBJ export | 1 hour | Wider compatibility |
| P2 | Per-face glTF colors | 4 hours | Better 3D output |
| P2 | Visibility caching | 2 hours | Performance |
| P3 | Custom exceptions | 1 hour | Error clarity |
| P3 | Rotation matrix API | 2 hours | Flexibility |
| P3 | Sphinx documentation | 1 day | Docs quality |
| P3 | CDL v2 rendering | 1 week | Future features |
After implementing improvements:
- All existing tests pass
- visualization.py tests added and passing
- GEMCAD export works with sample crystals
- glTF multi-color works for diamond preset
- No performance regression (benchmark SVG generation)
- mypy passes with zero errors
- ruff check passes
Measure before optimization:
| Operation | Crystal | Time | Notes |
|---|---|---|---|
| CDL→SVG | Octahedron (8 faces) | TBD | Baseline |
| CDL→SVG | Diamond (14 faces) | TBD | Truncated |
| CDL→SVG | Garnet (36 faces) | TBD | Complex |
| SVG→PNG | 1000×1000 | TBD | With cairosvg |
| glTF export | 100 vertices | TBD | Baseline |
Document created: 2026-01-20