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 aCifBlock. No DOM, no Three.js.structure/structure-modifiers/— the Filters (HydrogenFilter,DisorderFilter,SymmetryGrower,AtomLabelFilter,BondGenerator,IsolatedHydrogenFixer), all extendingBaseFilter. Each transforms oneCrystalStructureinto another. Depends only onstructure/.structure/structure-modifiers/growing/— the symmetry/fragment "growing" engine behindSymmetryGrower:growCell,growFragment,growExternalHBonds. Depends only on the parentstructure/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 onread-cif/andstructure/; only the surface modules depend on Three.js.ortep3d/—ORTEP3JsStructure(converts aCrystalStructureinto Three.js geometry) andCrystalViewer(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.CrystalViewerjoins 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 reachCrystalStructure.fromCIF. Depends only onread-cif/.generated/—svg-icons.js— auto-generated, gitignored, regenerated bynpm run generate-svg-iconsfromsrc/svg-icons/. Required at runtime bywidget.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.jsortep3d/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:
| Entry | Purpose |
|---|---|
cifvis → src/index.js | Browser entry. Exports everything, and registers <cifview-widget> as a custom element. |
cifvis/nobrowser → src/index.nobrowser.js | Same 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/:
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 defaultsThe 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.
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 constantThe 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:
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 7bench: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.
npm run deploy
# Inspect the assembled tree without publishing:
npm run build:deployment
# Remove dist/ and deployment/:
npm run clean:deploymentFull 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.
npm run docsGenerates a browsable HTML API reference at jsdoc-out/index.html, built from jsdoc.json at the repo root.