Skip to content

Developing CifVis

Contributor guide: the lay of the land, how the layers depend on each other, and where to find the full API reference.

The lay of the land

All source lives under src/lib/. Here's what's responsible for what:

  • read-cif/CIF, CifBlock, CifLoop. Turns raw CIF text into a generic key/value + loop-table object model. Knows nothing about crystallography — no atoms, no cells, no symmetry. Depends on nothing else in the codebase.
  • structure/CrystalStructure, UnitCell, Atom, plus the supporting pieces (bonds.js, cell-symmetry.js, adp.js, position.js, fract-to-cart.js, bond-classification.js, applied-symmetry.js). The crystallographic domain model, built directly from a CifBlock. No DOM, no Three.js.
  • structure/structure-modifiers/ — the Filters (HydrogenFilter, DisorderFilter, SymmetryGrower, AtomLabelFilter, BondGenerator, IsolatedHydrogenFixer), all extending BaseFilter. Each transforms one CrystalStructure into another. Depends only on structure/.
  • structure/structure-modifiers/growing/ — the symmetry/fragment "growing" engine behind SymmetryGrower: growCell, growFragment, growExternalHBonds. Depends only on the parent structure/ folder.
  • density/ — the reflection-to-map pipeline: FCF/custom coefficient parsing, observed-reflection merging and systematic-absence filtering, IAM/Cromer–Mann structure factors, anomalous-dispersion and extinction corrections, Fourier grids, progressive workers, Marching Cubes, and symmetry-aware mesh reuse. Its numerical core depends on read-cif/ and structure/; only the surface modules depend on Three.js.
  • ortep3d/ORTEP3JsStructure (converts a CrystalStructure into Three.js geometry) and CrystalViewer (the orchestrator: CIF parsing → filters → ORTEP3D scene → camera/controls/lighting/selection/density). Along with the density surface modules, this is the rendering layer that imports Three.js. CrystalViewer joins the structure-modifier, repair, structure, and density pipelines.
  • fix-cif/tryToFixCifBlock — repairs malformed/nonstandard CIF blocks (reconciling atom labels, guessing missing symmetry operations) before they reach CrystalStructure.fromCIF. Depends only on read-cif/.
  • generated/svg-icons.js — auto-generated, gitignored, regenerated by npm run generate-svg-icons from src/svg-icons/. Required at runtime by widget.js.
  • svg-icons/ — hand-authored source SVGs, the input to the icon code-generation step.

Hierarchy of objects

Each layer only imports from the ones below it, so a change to read-cif/ can only ripple upward, never sideways or back down:

read-cif ─┬─► structure ──► structure-modifiers ──┐
           ├─► density ◄──────── structure ─────────┤
           └─► fix-cif ─────────────────────────────┴─► ortep3d ──► widget.js ──► index.js

ortep3d/crystal-viewer.js is the pipeline glue: it's the one file that imports from structure, structure-modifiers, fix-cif, read-cif, and density together to drive CIF parsing → filters → rendering end to end. When you change the public shape of any of those lower layers, check this file.

Two entry points

package.json's exports map exposes two entry points:

EntryPurpose
cifvissrc/index.jsBrowser entry. Exports everything, and registers <cifview-widget> as a custom element.
cifvis/nobrowsersrc/index.nobrowser.jsSame core exports, including CIF/structure APIs, Filters, map calculation, IAM, anomalous lookup, and reflection readers, but CrystalViewer/CifViewWidget are stubs that throw — safe to import in Node/SSR/test environments with no window.

The core (read-cif, structure, structure-modifiers, fix-cif, the numerical density modules, ORTEP3JsStructure, and formatValueEsd) is deliberately DOM-free by construction — only CrystalViewer (canvas/WebGL) and CifViewWidget (custom element) need a browser.

The playground

The playground under site/ is deliberately kept readable as reference code for driving CrystalViewer directly. site/src/main.js is a thin, linear boot file (viewer lifecycle, file loading, toolbar wiring); playground-cif-routing.js classifies uploaded CIF/FCF/Cube files; playground-settings.js is the pure logic behind the settings overlay (it consumes the same generated options metadata as this documentation's reference tables); settings-overlay.js is its DOM layer.

Working on the documentation

The docs you are reading are a VitePress site under docs/:

bash
npm run docs:dev      # live-reloading dev server
npm run docs:build    # production build to docs/.vitepress/dist
npm run gen:options   # regenerate the options-reference tables from the source defaults

The options-reference tables are generated by scripts/gen-options-reference.mjs from structure-settings.js and the DEFAULT_* density option objects; descriptions live in docs/.vitepress/data/option-descriptions.js. A test (scripts/gen-options-reference.test.js) fails when the committed tables drift from the code or a new option lacks a description — after adding an option, run npm run gen:options and add its description. Live demos in the docs import the library directly from src/, so they always reflect your working tree.

Performance benchmarking

benchmark/speed.mjs loads real CIF files against the built library in headless Chrome (via playwright-core) and reports parse/build time, render time, and WebGL draw-call count per structure — self-contained, no external server required.

bash
npm run bench -- path/to/structure.cif
npm run bench -- path/to/cif/directory --limit 200
npm run bench -- path/to/file-list.txt --out results.csv --render-mode constant

The target can be a single .cif file, a directory (searched recursively), or a text file listing one path per line (optionally path<TAB>size). It builds dist/cifvis.alldeps.js automatically if missing. Chrome's location is taken from --chrome or the CHROME_PATH environment variable if the system default isn't found.

Density work has three focused regression microbenchmarks:

bash
npm run bench:iam -- structure.cif 20
npm run bench:density-symmetry -- structure.cif reflections.fcf 10
npm run bench:contours -- structure.cif scalar-field.cube 7

bench:iam separates model construction, first calculation, and warmed repeated calculation; when calculated F-squared values are present it also reports agreement. The symmetry benchmark alternates direct and symmetry-aware surface generation and reports wall-clock, polygonization, stitching, and Marching Cubes timings so ordering and JIT warm-up do not favour one path consistently. The contour benchmark reports plane setup, linear or monotone-tricubic sampling, multi-level Marching Squares extraction, transferable packing, and main-thread thick-line geometry independently. This mirrors the production worker/rendering boundary without including worker startup. Population profilers, parameter sweeps, model fitting, and report generation are research-analysis tools and intentionally live outside the library's regression benchmark suite.

Deployment

The deployment script is the complete GitHub Pages workflow: it cleans stale output, builds the external-dependency library, the all-dependencies bundle, the playground site, and this documentation, copies them into one self-contained deployment/ tree, publishes with gh-pages, and cleans generated output even if publishing fails.

bash
npm run deploy

# Inspect the assembled tree without publishing:
npm run build:deployment

# Remove dist/ and deployment/:
npm run clean:deployment

Full JSDoc

Every exported class and function has a JSDoc comment, and eslint-plugin-jsdoc enforces their presence across the codebase, so the generated reference is unusually complete. These pages are the map; JSDoc is the territory — use it when you need the full method/parameter list rather than a narrative explanation.

bash
npm run docs

Generates a browsable HTML API reference at jsdoc-out/index.html, built from jsdoc.json at the repo root.