Counterslayer is a Svelte/JSCad application for generating 3D-printable counter tray inserts for board games. It creates STL files for trays, boxes, and lids organized into stackable layers.
When making geometry changes, use this self-contained loop to iterate without user intervention:
1. Make code changes to geometry (lid.ts, counterTray.ts, box.ts)
2. Regenerate STLs: npx tsx scripts/generate-geometry.ts
3. Verify with renders: npx tsx scripts/capture-view.ts --angle iso
4. Read project.json for positions and layout data
5. Check multiple angles, zoom into problem areas
6. If not correct, go back to step 1
7. When satisfied, inform the user
In dev mode, project.json is automatically saved to mesh-analysis/ whenever the user clicks "Regenerate". This includes computed layout data with global positions for all items.
For full debug export (including STLs and screenshot), the user can click "Import / Export" → "Debug for Claude".
# Regenerate all STLs from project.json (box, lid, and all trays)
npx tsx scripts/generate-geometry.ts
# Optionally specify a box ID
npx tsx scripts/generate-geometry.ts <boxId>This reads mesh-analysis/project.json and regenerates STLs with the latest code changes.
| File | Purpose |
|---|---|
project.json |
Full project config + computed layout with positions |
app-screenshot.png |
Three.js render (from Debug for Claude) |
view.png |
Captured view from capture-view.ts |
*.stl |
Raw geometry files (from Debug for Claude) |
The file contains the full project configuration plus a computedLayout section with global positions:
{
"version": 2,
"layers": [...],
"counterShapes": [...],
"cardSizes": [...],
"computedLayout": {
"timestamp": "2026-03-12T...",
"gameContainer": { "width": 150, "depth": 200 },
"layers": [
{
"layerId": "abc123",
"layerName": "Bottom layer",
"layerHeight": 28.4,
"boxes": [
{
"boxId": "box1",
"boxName": "Counter box",
"x": 0,
"y": 100,
"width": 150,
"depth": 90,
"trays": [
{ "trayId": "t1", "name": "Tokens", "letter": "A", "x": 0, "y": 0, "width": 143, "depth": 45 }
]
}
],
"looseTrays": [
{ "trayId": "t2", "name": "Cup tray", "letter": "C", "x": 0, "y": 0, "width": 150, "depth": 100 }
]
}
]
}
}Game container: The bounding area (e.g., 150mm × 200mm) that all layers fit within.
Global positions (x, y): Where items sit within the game container.
y = 0→ front edge of containery = depth→ back edge of containerx = 0→ left edgex = width→ right edge
Box-relative positions (xInBox, yInBox): Where trays sit within their parent box.
# Which items touch the front (y=0)?
cat mesh-analysis/project.json | jq '.computedLayout.layers[] | {layer: .layerName, front: [.looseTrays[] | select(.y == 0) | .name]}'
# Get all loose tray positions
cat mesh-analysis/project.json | jq '.computedLayout.layers[].looseTrays[] | {name, x, y}'Use scripts/capture-view.ts (Playwright-based) to render views of the app:
# Ensure dev server is running (npm run dev)
# View from preset angles
npx tsx scripts/capture-view.ts --angle iso
npx tsx scripts/capture-view.ts --angle top
npx tsx scripts/capture-view.ts --angle front
# Zoom in
npx tsx scripts/capture-view.ts --angle left --zoom 3
# Custom camera position (Three.js Y-up coordinates)
npx tsx scripts/capture-view.ts --pos "100,80,150" --look-at "0,25,50"
# Output to specific file
npx tsx scripts/capture-view.ts --angle top --out mesh-analysis/view-top.png
# View specific items by ID (selects item and sets appropriate view mode)
npx tsx scripts/capture-view.ts --trayId nrme206 --angle bottom --zoom 2Use --trayId to view a specific tray in isolation (sets view mode to "tray"):
# View a specific tray's bottom (useful for checking text emboss)
npx tsx scripts/capture-view.ts --trayId <id> --angle bottom --zoom 2
# Get tray IDs from project.json
cat mesh-analysis/project.json | jq '.layers[].looseTrays[] | {id, name}'
cat mesh-analysis/project.json | jq '.layers[].boxes[].trays[] | {id, name}'The app also supports these URL parameters directly:
?trayId=<id>- Select tray and switch to tray view?boxId=<id>- Select box and switch to exploded view?layerId=<id>- Select layer and switch to layer view?view=<mode>- Set view mode (tray, layer, exploded, all, all-no-lid)
| Angle | View |
|---|---|
front |
Looking from +Z toward -Z |
back |
Looking from -Z toward +Z |
left |
Looking from -X toward +X |
right |
Looking from +X toward -X |
top |
Looking from +Y down |
bottom |
Looking from -Y up |
iso |
Isometric from front-right |
iso-back |
Isometric from back-left |
iso-left |
Isometric from front-left |
iso-right |
Isometric from back-right |
- X: Width (left/right)
- Y: Height (up/down) - Y is up in Three.js
- Z: Depth (front/back)
- Origin (0,0,0) is at front-left-bottom corner
The app includes a TinkerCAD-style ViewCube in the top-right corner:
- Click faces (FRONT, TOP, etc.) to snap to orthographic views
- Click corners to snap to isometric views
- Cube rotates to match current camera orientation
- Hidden on mobile and in debug/capture mode
src/lib/models/- Geometry generation (counterTray.ts, box.ts, lid.ts, layer.ts)src/lib/stores/project.svelte.ts- Project state managementsrc/lib/workers/geometry.worker.ts- Web worker for non-blocking geometry generationsrc/lib/components/ViewCube.svelte- Camera navigation cubescripts/capture-view.ts- Playwright-based view capturescripts/generate-geometry.ts- CLI for regenerating STLsmesh-analysis/- Generated debug files (gitignored)
Use BEM with camelCase:
.componentName__elementName--modifier
Examples: .cupCell, .cupCell__label, .cupCell--selected