Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
name: Docs

on:
push:
branches: [main]
paths: [docs/**, scrollcase/examples/**, mkdocs.yml, .github/workflows/docs.yml]
pull_request:
paths: [docs/**, scrollcase/examples/**, mkdocs.yml, .github/workflows/docs.yml]
workflow_dispatch:

permissions:
contents: read

concurrency:
group: pages
cancel-in-progress: false

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v6
- run: uvx --with mkdocs-material --with 'mkdocs<2' mkdocs build --strict
- uses: actions/upload-pages-artifact@v3
with:
path: site

deploy:
if: github.event_name != 'pull_request'
needs: build
runs-on: ubuntu-latest
permissions:
pages: write
id-token: write
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- id: deployment
uses: actions/deploy-pages@v4
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ cmake-build*/
.venv/
__pycache__/
.pytest_cache/
site/
49 changes: 49 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Overview

3D-printable sample mounts (scroll cases, interface plates, spindles, trays) for scanner/beamline data acquisition. Most of the repo is OpenSCAD source in `OpenSCAD/`, with exported STLs committed to `Models/`. Scroll cases are generated by the Python package in `scrollcase/`, which is the only part with tests.

## Commands

Render an OpenSCAD model to STL (parameters are top-level variables, overridable with `-D`):

```bash
openscad -o out.stl -D rod_type=2 "OpenSCAD/Spindle Base.scad"
```

Scroll cases (run from `scrollcase/`; uv manages the environment):

```bash
uv run scrollcase build path/to/scroll.ply -c case.toml -o out/
uv run scrollcase defaults # print every config option as TOML
uv run pytest # ~30s; builds real cases
uv run pytest tests/test_lining.py::test_scroll_slides_out_toward_split
```

The hanging-frame case is rendered through `OpenSCAD/scripts/HangingCase-Generic.sh`; set `UTILS_DIR` to the absolute path of this clone first.

Build the C++ utilities (libigl is fetched automatically; `spiral-length` only builds if GSL is found):

```bash
cmake -S utils -B build && cmake --build build
```

C++ formatting follows `.clang-format` (Google-based, 4-space indent).

## Architecture

- **Shared base geometry.** `Generic Mount Disc.scad` / `Generic Mount Disc 65mm.scad` define the standard mount base and expose its dimensions as functions (`GenericMountDisc_Diameter()`, `GenericMountDisc_Thickness()`, etc.). Interface plates and spindle bases `use <>` these, so changing a disc dimension affects every dependent model. `scrollcase/src/scrollcase/mount_disc.py` mirrors both discs for the scroll case and must be updated by hand to match. `Generic Mount Ring*` is the mating ring for the disc.
- **`cylinder_outer.scad`** is used almost everywhere instead of `cylinder()`. It scales the radius so the polygon's flats (not vertices) sit at the requested radius, which matters for press fits and clearances.
- **`use <>` semantics.** Files are pulled in with `use`, so only modules/functions are imported. Library files still call their own module at the top level (e.g. `GenericMountDisc();`) so they render standalone.
- **`scrollcase/`.** Python (build123d + meshlib) port of the former OpenSCAD scroll case generator; its mesh pipeline is adapted from ScrollPrize/villa's `scrollcase`. Mesh stage (`lining.py`): align the scroll, offset it to a cavity, split at Y=0, `FixUndercuts` toward the split, offset again for the lining wall. B-rep stage (`case.py`): shell, divider, disc, nubs, escape holes, labels, stand; `scad.py` gives OpenSCAD-style primitives (extrinsic XYZ rotation, `center=` anchoring) so it reads like the original. Halves are assembled as `(body ∪ wall) − cavity`.
- **Never load meshlib and build123d in one process.** meshlib bundles its own OpenCASCADE, and the two segfault or abort together. `pipeline.py` runs `brep_worker` in subprocesses; tests that need build123d use `conftest.run_brep`.
- **OCCT is fragile with coincident or tangent curved faces.** The shell is built by clipping the honeycomb cutter, not by unioning caps or ring bands back over the cylinder; tools overshoot faces; tangent escape holes are nudged. `lining.load_brep_export` fails on any real hole in a B-rep STL, so a new feature that leaves faces coincident shows up as a build error.
- **Variants by copy.** Size/scanner variants (e.g. `SI Interface Plate - 147mm` vs `- 200mm`, `SpindleBase-*`) are separate files or separate `-D` renders, and the resulting STLs in `Models/` are named after the parameter values. When adding a variant, export its STL to `Models/` too.
- **`utils/`.** `SpiralLength.cpp` computes Archimedean spiral length via GSL, for use with `Spiral Frame.scad`.

## Known gaps

`Fragment Trays.scad` and `Hanging Frame Case.scad` `use <Diamond Interface Plate.scad>`, which is not in the repo, so they won't render as-is.
66 changes: 58 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,21 +1,71 @@
# Sample Mounts

A collection of scripts and models used to construct safe sample mounts for
data acquisition.
3D-printable sample mounts (scroll cases, interface plates, spindles, and
trays) for scanner and beamline data acquisition.

User docs: https://educelab.github.io/sample-mounts/

## Models

Predesigned models.
Ready-to-print STLs. Variants are named after the parameters they were
rendered with (e.g. `SpindleBase-6.35mm.stl`, `SI Interface Plate - 200mm.stl`).

- **Mount discs and rings.** The standard base (`Generic Mount Disc`, and a
65 mm version) and the ring it mates with.
- **Interface plates.** Adapt the mount disc to a specific scanner or
beamline stage (SkyScan 1273, I12.EH1, SI).
- **Spindles.** Spindle bases and stoppers for several rod diameters.

## OpenSCAD

OpenSCAD scripts for generating many of the models.
Parametric sources for the models. Parameters are top-level variables and can
be overridden on the command line:

```bash
openscad -o out.stl -D rod_type=2 "OpenSCAD/Spindle Base.scad"
```

Interface plates and spindle bases are built on `Generic Mount Disc*.scad`,
so changing the disc changes all of them.

`Fragment Trays.scad` and `Hanging Frame Case.scad` depend on
`Diamond Interface Plate.scad`, which isn't in this repo, so they won't render
as-is.

## scrollcase

Python generator for split scroll cases fit to a scroll mesh. See
[`docs/creating-scroll-case.md`](docs/creating-scroll-case.md).
Python generator for split scroll cases fit to a scroll mesh. Requires
[uv](https://docs.astral.sh/uv/):

```bash
cd scrollcase
uv run scrollcase defaults --style educelab.v1 > my-scroll.toml
uv run scrollcase build -c my-scroll.toml -o out/
```

See [Creating a scroll case](docs/creating-scroll-case.md) and the
[config reference](docs/scroll-case-config.md).

## utils
Miscellaneous utilities. `SpiralLength.cpp` computes the length of an
Archimedean spiral for use with `Spiral Frame.scad`.

`SpiralLength.cpp` computes the length of an Archimedean spiral for use with
`Spiral Frame.scad`. It needs [GSL](https://www.gnu.org/software/gsl/):

```bash
cmake -S utils -B build && cmake --build build
```

## Docs

The user docs in `docs/` are built with MkDocs and published to GitHub Pages
on every push to `main`. To preview locally:

```bash
uvx --with mkdocs-material --with 'mkdocs<2' mkdocs serve
```

## License

AGPL-3.0. The scroll case's alignment and mesh pipeline are adapted from
[ScrollPrize/villa](https://github.com/ScrollPrize/villa/tree/main/foundation/scrollcase)
(MIT); see `scrollcase/src/scrollcase/LICENSE.villa`.
17 changes: 12 additions & 5 deletions docs/creating-scroll-case.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Creating a scroll case

Scroll cases are generated by the `scrollcase` Python package in
[`scrollcase/`](../scrollcase). Given a scroll mesh, it aligns the scroll,
[`scrollcase/`](https://github.com/educelab/sample-mounts/tree/main/scrollcase). Given a scroll mesh, it aligns the scroll,
builds a lining that follows its surface, and wraps the lining in the split
honeycomb case: left half (with mount disc), right half, and assembly stand.

Expand All @@ -16,15 +16,22 @@ honeycomb case: left half (with mount disc), right half, and assembly stand.
be a single watertight surface. Orientation and position don't matter. If
it isn't in millimeters, set `scroll.scale`.

2. **Write a config.** Start from the defaults or the example:
2. **Write a config.** Copy
[`scrollcase/examples/template.toml`](https://github.com/educelab/sample-mounts/blob/main/scrollcase/examples/template.toml)
next to your mesh:

```bash
cd scrollcase
uv run scrollcase defaults > my-scroll.toml
```toml
--8<-- "template.toml"
```

Set `name`, `scroll.mesh` (relative to the config file), and the label.
Every option is described in [the config reference](scroll-case-config.md).
To start from every option instead, print a style's full config:

```bash
cd scrollcase
uv run scrollcase defaults --style educelab.v1 > my-scroll.toml
```

3. **Generate a draft.**

Expand Down
20 changes: 20 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Sample Mounts

3D-printable sample mounts (scroll cases, interface plates, spindles, and
trays) for scanner and beamline data acquisition. Source and models are on
[GitHub](https://github.com/educelab/sample-mounts).

- **Models.** Ready-to-print STLs are in
[`Models/`](https://github.com/educelab/sample-mounts/tree/main/Models).
- **OpenSCAD.** Parametric sources for most models are in
[`OpenSCAD/`](https://github.com/educelab/sample-mounts/tree/main/OpenSCAD).
Parameters are top-level variables and can be overridden with `-D`:

```bash
openscad -o out.stl -D rod_type=2 "OpenSCAD/Spindle Base.scad"
```

- **Scroll cases.** Split scroll cases fit to a scroll mesh are generated by
the `scrollcase` Python package. Start with
[Creating a scroll case](creating-scroll-case.md), then see the
[config reference](scroll-case-config.md) for every option.
Loading
Loading