feat: functional gridfinity calculator app
This commit is contained in:
190
.gitignore
vendored
Normal file
190
.gitignore
vendored
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
# General
|
||||||
|
.DS_Store
|
||||||
|
__MACOSX/
|
||||||
|
.AppleDouble
|
||||||
|
.LSOverride
|
||||||
|
Icon[
|
||||||
|
]
|
||||||
|
|
||||||
|
# Thumbnails
|
||||||
|
._*
|
||||||
|
|
||||||
|
# Files that might appear in the root of a volume
|
||||||
|
.DocumentRevisions-V100
|
||||||
|
.fseventsd
|
||||||
|
.Spotlight-V100
|
||||||
|
.TemporaryItems
|
||||||
|
.Trashes
|
||||||
|
.VolumeIcon.icns
|
||||||
|
.com.apple.timemachine.donotpresent
|
||||||
|
|
||||||
|
# Directories potentially created on remote AFP share
|
||||||
|
.AppleDB
|
||||||
|
.AppleDesktop
|
||||||
|
Network Trash Folder
|
||||||
|
Temporary Items
|
||||||
|
.apdisk
|
||||||
|
|
||||||
|
|
||||||
|
# Swap
|
||||||
|
[._]*.s[a-v][a-z]
|
||||||
|
# comment out the next line if you don't need vector files
|
||||||
|
!*.svg
|
||||||
|
[._]*.sw[a-p]
|
||||||
|
[._]s[a-rt-v][a-z]
|
||||||
|
[._]ss[a-gi-z]
|
||||||
|
[._]sw[a-p]
|
||||||
|
|
||||||
|
# Session
|
||||||
|
Session.vim
|
||||||
|
Sessionx.vim
|
||||||
|
|
||||||
|
# Temporary
|
||||||
|
.netrwhist
|
||||||
|
*~
|
||||||
|
# Auto-generated tag files
|
||||||
|
tags
|
||||||
|
# Persistent undo
|
||||||
|
[._]*.un~
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||||
|
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
*.pid.lock
|
||||||
|
|
||||||
|
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||||
|
lib-cov
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
coverage
|
||||||
|
*.lcov
|
||||||
|
|
||||||
|
# nyc test coverage
|
||||||
|
.nyc_output
|
||||||
|
|
||||||
|
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||||
|
.grunt
|
||||||
|
|
||||||
|
# Bower dependency directory (https://bower.io/)
|
||||||
|
bower_components
|
||||||
|
|
||||||
|
# node-waf configuration
|
||||||
|
.lock-wscript
|
||||||
|
|
||||||
|
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||||
|
build/Release
|
||||||
|
|
||||||
|
# Dependency directories
|
||||||
|
node_modules/
|
||||||
|
jspm_packages/
|
||||||
|
|
||||||
|
# Snowpack dependency directory (https://snowpack.dev/)
|
||||||
|
web_modules/
|
||||||
|
|
||||||
|
# TypeScript cache
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# Optional npm cache directory
|
||||||
|
.npm
|
||||||
|
|
||||||
|
# Optional eslint cache
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# Optional stylelint cache
|
||||||
|
.stylelintcache
|
||||||
|
|
||||||
|
# Optional REPL history
|
||||||
|
.node_repl_history
|
||||||
|
|
||||||
|
# Output of 'npm pack'
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# Yarn Integrity file
|
||||||
|
.yarn-integrity
|
||||||
|
|
||||||
|
# dotenv environment variable files
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
|
||||||
|
# parcel-bundler cache (https://parceljs.org/)
|
||||||
|
.cache
|
||||||
|
.parcel-cache
|
||||||
|
|
||||||
|
# Next.js build output
|
||||||
|
.next
|
||||||
|
out
|
||||||
|
|
||||||
|
# Nuxt.js build / generate output
|
||||||
|
.nuxt
|
||||||
|
dist
|
||||||
|
.output
|
||||||
|
|
||||||
|
# Gatsby files
|
||||||
|
.cache/
|
||||||
|
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||||
|
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||||
|
# public
|
||||||
|
|
||||||
|
# vuepress build output
|
||||||
|
.vuepress/dist
|
||||||
|
|
||||||
|
# vuepress v2.x temp and cache directory
|
||||||
|
.temp
|
||||||
|
.cache
|
||||||
|
|
||||||
|
# Sveltekit cache directory
|
||||||
|
.svelte-kit/
|
||||||
|
|
||||||
|
# vitepress build output
|
||||||
|
**/.vitepress/dist
|
||||||
|
|
||||||
|
# vitepress cache directory
|
||||||
|
**/.vitepress/cache
|
||||||
|
|
||||||
|
# Docusaurus cache and generated files
|
||||||
|
.docusaurus
|
||||||
|
|
||||||
|
# Serverless directories
|
||||||
|
.serverless/
|
||||||
|
|
||||||
|
# FuseBox cache
|
||||||
|
.fusebox/
|
||||||
|
|
||||||
|
# DynamoDB Local files
|
||||||
|
.dynamodb/
|
||||||
|
|
||||||
|
# Firebase cache directory
|
||||||
|
.firebase/
|
||||||
|
|
||||||
|
# TernJS port file
|
||||||
|
.tern-port
|
||||||
|
|
||||||
|
# Stores VSCode versions used for testing VSCode extensions
|
||||||
|
.vscode-test
|
||||||
|
|
||||||
|
# yarn v3
|
||||||
|
.pnp.*
|
||||||
|
.yarn/*
|
||||||
|
!.yarn/patches
|
||||||
|
!.yarn/plugins
|
||||||
|
!.yarn/releases
|
||||||
|
!.yarn/sdks
|
||||||
|
!.yarn/versions
|
||||||
|
|
||||||
|
# Vite files
|
||||||
|
vite.config.js.timestamp-*
|
||||||
|
vite.config.ts.timestamp-*
|
||||||
|
.vite/
|
||||||
78
AGENTS.md
Normal file
78
AGENTS.md
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
# Core Workflow & My Behavior
|
||||||
|
|
||||||
|
## 1. My Core Interaction Style
|
||||||
|
|
||||||
|
* **I do not "yap":** I will be concise and eliminate conversational filler. I focus on technical precision over politeness.
|
||||||
|
* **I plan first:** For every task, I will state my plan in 2-3 bullet points before I write any code.
|
||||||
|
* **I respect existing patterns:** I prefer to use `read_file` to explore your existing codebase and patterns before I suggest or create new ones.
|
||||||
|
|
||||||
|
## 2. My Engineering Standards
|
||||||
|
|
||||||
|
* **I am test-driven:** If the environment allows, I will verify my changes by running a test or starting a dev server. I never assume code works just because it looks correct.
|
||||||
|
* **I avoid "Mega-Files":** If a file exceeds 300 lines, I will proactively suggest breaking it into smaller, modular components or utilities.
|
||||||
|
* **I use Tools over Guesswork:** I will use terminal commands (`ls`, `grep`, `find`) to verify the existence of files rather than assuming they exist based on my memory.
|
||||||
|
|
||||||
|
## 3. My "Self-Improving" Protocol
|
||||||
|
|
||||||
|
* **I evolve my rules:** If I encounter a recurring bug, a tricky error, or a pattern we both agree on, **I MUST** ask: "Should I update the `AGENTS.md` file to remember this pattern?"
|
||||||
|
* **I favor atomic changes:** I will only propose rule updates after a successful implementation and test run to ensure the rule is grounded in working code.
|
||||||
|
|
||||||
|
## 4. Git & Version Control Protocol
|
||||||
|
- **Atomic Commits:** I will create one commit per logical change. I will never bundle multiple unrelated features into a single commit.
|
||||||
|
- **Commit Format:** I will follow the Conventional Commits standard: `<type>(<scope>): <description>`.
|
||||||
|
- **Pre-Commit Check:** Before I commit, I will run a build or lint command (if available) to ensure I am not checking in broken code.
|
||||||
|
- **Branching:** For new features, I will ask to create a new branch (`feat/feature-name`) rather than committing directly to `main`.
|
||||||
|
- **Staging:** I prefer to stage files individually.
|
||||||
|
|
||||||
|
## 5. My Error Handling
|
||||||
|
|
||||||
|
* **I am honest about failures:** If a tool call fails or I am confused by a requirement, I will stop immediately and ask for clarification rather than "hallucinating" a fix.
|
||||||
|
* **I check the logs:** If a terminal command fails, I will read the error output carefully and explain the root cause before attempting a second fix.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Project Management & Context Rules
|
||||||
|
|
||||||
|
## 1. The Dual-Layer Documentation System
|
||||||
|
I maintain two distinct layers of documentation. I must keep them in sync but never mix their purposes.
|
||||||
|
|
||||||
|
### Layer 1: The Blueprint (/docs)
|
||||||
|
|
||||||
|
*Role: Permanent reference for humans and AI. The "Source of Truth".*
|
||||||
|
|
||||||
|
- **PRD.md**: Core features, target audience, and business logic.
|
||||||
|
- **Architecture.md**: Tech stack, folder structure, and data flow diagrams.
|
||||||
|
- **API.md**: Endpoint definitions, request/response schemas.
|
||||||
|
- **Schema.md**: Database tables, relationships, and types.
|
||||||
|
|
||||||
|
### Layer 2: The Memory Bank (/memory-bank)
|
||||||
|
|
||||||
|
*Role: Operational state for the AI agent. Updated every session.*
|
||||||
|
|
||||||
|
- **projectBrief.md**: The foundation. High-level overview of requirements.
|
||||||
|
- **productContext.md**: Why the project exists and how it should feel/work.
|
||||||
|
- **activeContext.md**: **CRITICAL.** Current work focus, recent changes, and active decisions.
|
||||||
|
- **systemPatterns.md**: Technical decisions and recurring code patterns discovered.
|
||||||
|
- **progress.md**: Milestone tracking. What is done, in-progress, and pending.
|
||||||
|
|
||||||
|
## 2. Required Workflow Protocol
|
||||||
|
|
||||||
|
### Phase A: Initialization (New Session)
|
||||||
|
At the start of every session, I MUST:
|
||||||
|
1. Read `memory-bank/activeContext.md` and `memory-bank/progress.md` to regain context.
|
||||||
|
2. Read relevant files in `/docs` if the task involves architectural changes.
|
||||||
|
3. Verbally confirm the "Current Focus" to the user.
|
||||||
|
|
||||||
|
### Phase B: Execution (Act Mode)
|
||||||
|
- I only perform tasks listed in `memory-bank/progress.md` or as requested by the user.
|
||||||
|
- If I discover a new pattern or make a technical decision (e.g., "we use absolute imports"), I must immediately update `systemPatterns.md`.
|
||||||
|
|
||||||
|
### Phase C: Task Completion
|
||||||
|
Before calling `attempt_completion`, I MUST:
|
||||||
|
1. Update `memory-bank/activeContext.md` with a summary of changes.
|
||||||
|
2. Update `memory-bank/progress.md` by checking off completed items.
|
||||||
|
3. **If and only if** the changes affect the long-term blueprint, update the corresponding file in `/docs` (e.g., updating `API.md` after adding a route).
|
||||||
|
|
||||||
|
## 3. Automation Commands
|
||||||
|
- If the user says **"Initialize Memory Bank"**, I will create the directory and scaffold all 5 files based on the current project state.
|
||||||
|
- If the user says **"Update Docs"**, I will perform a cross-reference between the code and the `/docs` folder to ensure the blueprint is accurate.
|
||||||
32
docs/Architecture.md
Normal file
32
docs/Architecture.md
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
# Architecture - Gridfinity Calculator
|
||||||
|
|
||||||
|
## 1. System Design
|
||||||
|
The application is a client-side only web tool.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
A[index.html] --> B[main.ts]
|
||||||
|
B --> C[calculator.ts]
|
||||||
|
B --> D[DOM Update]
|
||||||
|
E[User Input] --> B
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. Components
|
||||||
|
- **Input Form:** Collects drawer dimensions and unit preferences.
|
||||||
|
- **Spec Controls:** Accordion or settings panel for overriding Gridfinity defaults.
|
||||||
|
- **Calculation Engine:** Pure TS module for unit conversion and Gridfinity math.
|
||||||
|
- **Results Display:** Visual cards/readouts showing calculated values.
|
||||||
|
|
||||||
|
## 3. Data Flow
|
||||||
|
1. User changes an input.
|
||||||
|
2. `main.ts` catches the event.
|
||||||
|
3. `main.ts` gathers all current values from the DOM.
|
||||||
|
4. `main.ts` calls `calculator.ts` functions.
|
||||||
|
5. `calculator.ts` returns a results object.
|
||||||
|
6. `main.ts` updates the results section of the DOM.
|
||||||
|
|
||||||
|
## 4. Technical Stack
|
||||||
|
- **Vite:** Asset bundling and dev server.
|
||||||
|
- **TypeScript:** Type-safe logic.
|
||||||
|
- **Tailwind CSS:** Utility-first styling for speed and responsiveness.
|
||||||
|
- **Vitest:** Unit testing logic in isolation.
|
||||||
31
docs/PRD.md
Normal file
31
docs/PRD.md
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
# PRD - Gridfinity Calculator
|
||||||
|
|
||||||
|
## 1. Overview
|
||||||
|
The Gridfinity Calculator is a web tool designed to help makers plan their Gridfinity layouts. It calculates the number of baseplates that fit in a specific container and the maximum bin height allowed.
|
||||||
|
|
||||||
|
## 2. Target Audience
|
||||||
|
- 3D printing enthusiasts using the Gridfinity system.
|
||||||
|
- Workshop organizers and hobbyists.
|
||||||
|
|
||||||
|
## 3. Key Features
|
||||||
|
- **Dimension Input:** Width, Length (Depth), and Height of the target drawer/container.
|
||||||
|
- **Unit Toggle:** Support for Inches (decimal/fractional) and Millimeters.
|
||||||
|
- **Spec Overrides:** Modify standard 42mm grid and 7mm height unit.
|
||||||
|
- **Dynamic Results:**
|
||||||
|
- Total full grid squares (e.g., "5x4 grids").
|
||||||
|
- Fit percentage/fractional grids (e.g., "5.2 x 4.1 grids").
|
||||||
|
- Vertical Unit Height (U) for bins (e.g., "6U").
|
||||||
|
- Remaining space (gap) in mm/inches.
|
||||||
|
|
||||||
|
## 4. Technical Constraints
|
||||||
|
- Must be fast and lightweight.
|
||||||
|
- Must work offline once loaded (single-page build).
|
||||||
|
- Mobile-responsive design.
|
||||||
|
|
||||||
|
## 5. Calculation Logic
|
||||||
|
- `Grid Count = Floor(Drawer Dimension / Grid Spec)`
|
||||||
|
- `Exact Fit = Drawer Dimension / Grid Spec`
|
||||||
|
- `Bin Unit Height (U) = Floor((Drawer Height - Base Thickness) / Height Unit)`
|
||||||
|
- *Default Base Thickness:* 4.8mm
|
||||||
|
- *Default Grid Spec:* 42mm
|
||||||
|
- *Default Height Unit:* 7mm
|
||||||
111
index.html
Normal file
111
index.html
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Gridfinity Calculator</title>
|
||||||
|
</head>
|
||||||
|
<body class="bg-slate-50 text-slate-900 min-h-screen">
|
||||||
|
<div id="app" class="max-w-2xl mx-auto p-4 md:p-8">
|
||||||
|
<header class="mb-8 text-center">
|
||||||
|
<h1 class="text-3xl font-bold text-slate-800">Gridfinity Calculator</h1>
|
||||||
|
<p class="text-slate-600">Plan your modular storage layout</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="grid gap-8">
|
||||||
|
<!-- Inputs Section -->
|
||||||
|
<section class="bg-white p-6 rounded-xl shadow-sm border border-slate-200">
|
||||||
|
<h2 class="text-xl font-semibold mb-4 flex items-center gap-2">
|
||||||
|
Drawer Dimensions
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-slate-700 mb-1">Width</label>
|
||||||
|
<input type="number" id="input-width" class="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500" placeholder="0" step="any" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-slate-700 mb-1">Length / Depth</label>
|
||||||
|
<input type="number" id="input-length" class="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500" placeholder="0" step="any" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-slate-700 mb-1">Height</label>
|
||||||
|
<input type="number" id="input-height" class="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500" placeholder="0" step="any" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<label class="text-sm font-medium text-slate-700">Units:</label>
|
||||||
|
<div class="flex bg-slate-100 p-1 rounded-lg">
|
||||||
|
<button id="unit-mm" class="px-4 py-1 rounded-md text-sm font-medium bg-white shadow-sm">mm</button>
|
||||||
|
<button id="unit-in" class="px-4 py-1 rounded-md text-sm font-medium text-slate-600 hover:text-slate-900">inches</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Results Section -->
|
||||||
|
<section id="results" class="bg-blue-600 text-white p-6 rounded-xl shadow-md hidden">
|
||||||
|
<h2 class="text-xl font-semibold mb-6">Calculation Results</h2>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<p class="text-blue-100 text-sm uppercase tracking-wider font-semibold">Grid Count</p>
|
||||||
|
<p class="text-4xl font-bold" id="result-grid-count">0 x 0</p>
|
||||||
|
<p class="text-blue-100 text-sm mt-1" id="result-grid-fractional">0.00 x 0.00 grids</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-blue-100 text-sm uppercase tracking-wider font-semibold">Max Bin Height</p>
|
||||||
|
<p class="text-4xl font-bold" id="result-bin-height">0U</p>
|
||||||
|
<p class="text-blue-100 text-sm mt-1">Vertical Units (7mm each)</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-blue-700/50 p-4 rounded-lg space-y-3">
|
||||||
|
<h3 class="font-semibold border-b border-blue-500/50 pb-2">Remaining Space</h3>
|
||||||
|
<div class="grid grid-cols-2 gap-2 text-sm">
|
||||||
|
<span>Width Gap:</span>
|
||||||
|
<span id="gap-width" class="font-mono text-right">0.0 mm</span>
|
||||||
|
<span>Length Gap:</span>
|
||||||
|
<span id="gap-length" class="font-mono text-right">0.0 mm</span>
|
||||||
|
<span>Height Gap:</span>
|
||||||
|
<span id="gap-height" class="font-mono text-right">0.0 mm</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Spec Overrides -->
|
||||||
|
<details class="bg-white border border-slate-200 rounded-xl overflow-hidden shadow-sm">
|
||||||
|
<summary class="px-6 py-4 font-medium cursor-pointer hover:bg-slate-50 flex items-center justify-between">
|
||||||
|
<span>Gridfinity Spec Overrides</span>
|
||||||
|
<span class="text-slate-400 text-sm font-normal">Standard: 42x42x7mm</span>
|
||||||
|
</summary>
|
||||||
|
<div class="p-6 border-t border-slate-100 space-y-4 bg-slate-50/50">
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-semibold text-slate-500 uppercase mb-1">Grid Size (mm)</label>
|
||||||
|
<input type="number" id="spec-grid-size" value="42" class="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-semibold text-slate-500 uppercase mb-1">Unit Height (mm)</label>
|
||||||
|
<input type="number" id="spec-height-unit" value="7" class="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-semibold text-slate-500 uppercase mb-1">Base Thickness (mm)</label>
|
||||||
|
<input type="number" id="spec-base-thickness" value="4.8" class="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="text-xs text-slate-400 italic">Adjust these to scale the system for an exact fit in your drawer.</p>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer class="mt-12 text-center text-slate-400 text-sm">
|
||||||
|
<p>Follows the Gridfinity spec by Zach Freedman</p>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
<script type="module" src="/src/main.ts"></script>
|
||||||
|
<link rel="stylesheet" href="/src/style.css">
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
20
memory-bank/activeContext.md
Normal file
20
memory-bank/activeContext.md
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# Active Context
|
||||||
|
|
||||||
|
## Current Status
|
||||||
|
- Initializing the project environment.
|
||||||
|
- Documentation phase (Memory Bank & Blueprint).
|
||||||
|
|
||||||
|
## Recent Changes
|
||||||
|
- Created `memory-bank/projectBrief.md`.
|
||||||
|
- Created `memory-bank/productContext.md`.
|
||||||
|
|
||||||
|
## Current Focus
|
||||||
|
- Completing Memory Bank initialization.
|
||||||
|
- Drafting PRD and Architecture.
|
||||||
|
- Setting up the Vite/TypeScript/Tailwind environment.
|
||||||
|
|
||||||
|
## Active Decisions
|
||||||
|
- Using **Vite** for the build tool.
|
||||||
|
- Using **Vanilla TypeScript** to avoid framework overhead.
|
||||||
|
- Using **Vitest** for unit testing calculation logic.
|
||||||
|
- Target: A single-page application that can be built into a self-contained output.
|
||||||
18
memory-bank/productContext.md
Normal file
18
memory-bank/productContext.md
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Product Context
|
||||||
|
|
||||||
|
## Why this project exists?
|
||||||
|
Gridfinity is a popular modular storage system. While the standard is well-defined (42x42x7mm), users often need to:
|
||||||
|
1. Figure out how many baseplates fit in a drawer.
|
||||||
|
2. Determine the maximum height of bins for a specific drawer depth.
|
||||||
|
3. Scale the system slightly to perfectly fill a drawer that doesn't fit a whole number of 42mm grids.
|
||||||
|
|
||||||
|
## User Experience
|
||||||
|
- **Input-First:** Users should immediately see where to enter their dimensions.
|
||||||
|
- **Instant Feedback:** Results should update as they type.
|
||||||
|
- **Flexibility:** Toggle between mm and inches easily.
|
||||||
|
- **Precision:** Clear output showing "Full Grids" and "Vertical Units".
|
||||||
|
|
||||||
|
## Success Criteria
|
||||||
|
- Accurately converts between units.
|
||||||
|
- Correctly calculates bin height while accounting for baseplate/bin-bottom thickness.
|
||||||
|
- Provides clear instructions for "fractional" fits (e.g., if a drawer is 100mm, it fits 2.38 grids).
|
||||||
18
memory-bank/progress.md
Normal file
18
memory-bank/progress.md
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Progress Tracking
|
||||||
|
|
||||||
|
## Milestones
|
||||||
|
- [ ] Initial Environment & Docs Setup
|
||||||
|
- [ ] Core Calculation Engine
|
||||||
|
- [ ] Web UI & Integration
|
||||||
|
- [ ] Final Build & Polish
|
||||||
|
|
||||||
|
## Todo List
|
||||||
|
- [x] Create project structure
|
||||||
|
- [x] Initialize Memory Bank documents
|
||||||
|
- [ ] Create Blueprint documents (PRD, Architecture)
|
||||||
|
- [ ] Project setup (Vite, TS, Tailwind, Vitest)
|
||||||
|
- [ ] Implement `src/calculator.ts`
|
||||||
|
- [ ] Write tests in `src/calculator.test.ts`
|
||||||
|
- [ ] Implement `index.html` UI
|
||||||
|
- [ ] Implement `src/main.ts` glue code
|
||||||
|
- [ ] Verify build output
|
||||||
17
memory-bank/projectBrief.md
Normal file
17
memory-bank/projectBrief.md
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# Project Brief: Gridfinity Calculator
|
||||||
|
|
||||||
|
A simple, lightweight web-based calculator for Gridfinity baseplates and bins.
|
||||||
|
|
||||||
|
## Core Requirements
|
||||||
|
- Enter drawer/container dimensions (width, height, depth).
|
||||||
|
- Support for both inches and millimeters.
|
||||||
|
- Custom overrides for Gridfinity specifications (default 42x42x7mm).
|
||||||
|
- Calculate required baseplates (full grids).
|
||||||
|
- Calculate bin vertical unit height (7mm increments), accounting for base thickness.
|
||||||
|
- No heavy frameworks (Vanilla TS/Vite).
|
||||||
|
- Single-page optimized build.
|
||||||
|
|
||||||
|
## Goals
|
||||||
|
- Provide an accurate and easy-to-use tool for Gridfinity makers.
|
||||||
|
- Allow for non-standard scaling to fit specific drawer sizes perfectly.
|
||||||
|
- Clean, responsive UI with Tailwind CSS.
|
||||||
28
memory-bank/systemPatterns.md
Normal file
28
memory-bank/systemPatterns.md
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# System Patterns
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
- **Build Tool:** Vite
|
||||||
|
- **Language:** TypeScript
|
||||||
|
- **Styling:** Tailwind CSS
|
||||||
|
- **Testing:** Vitest
|
||||||
|
|
||||||
|
## Design Patterns
|
||||||
|
- **Modular Logic:** Calculation logic is isolated in a pure TypeScript module (`src/calculator.ts`) for easy testing.
|
||||||
|
- **Reactive UI (Vanilla):** Use DOM event listeners and a central `render()` or `update()` function to sync state to the UI.
|
||||||
|
- **Unit Normalization:** All internal calculations should be performed in **millimeters**. Inputs in inches are converted immediately upon ingestion.
|
||||||
|
|
||||||
|
## Directory Structure
|
||||||
|
- `src/`: Source code.
|
||||||
|
- `calculator.ts`: Core business logic.
|
||||||
|
- `main.ts`: Entry point and DOM manipulation.
|
||||||
|
- `style.css`: Tailwind entry point.
|
||||||
|
- `docs/`: Permanent documentation (PRD, Architecture).
|
||||||
|
- `memory-bank/`: AI operational context.
|
||||||
|
- `tests/`: Unit tests.
|
||||||
|
|
||||||
|
## Calculation Constants (Defaults)
|
||||||
|
- Grid Width/Depth: 42mm
|
||||||
|
- Grid Height Unit: 7mm
|
||||||
|
- Base Thickness: 4.8mm (typical baseplate height above surface)
|
||||||
|
- Bin Lip/Bottom: 0.7mm (typical clearance/thickness to account for in vertical units)
|
||||||
|
- *Note: These defaults will be refined during implementation based on official specs.*
|
||||||
2725
package-lock.json
generated
Normal file
2725
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
20
package.json
Normal file
20
package.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"name": "gridfinity-calc",
|
||||||
|
"private": true,
|
||||||
|
"version": "1.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc && vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"test": "vitest"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"autoprefixer": "^10.4.23",
|
||||||
|
"postcss": "^8.5.6",
|
||||||
|
"tailwindcss": "^3.4.19",
|
||||||
|
"typescript": "^5.2.2",
|
||||||
|
"vite": "^5.0.0",
|
||||||
|
"vitest": "^0.34.6"
|
||||||
|
}
|
||||||
|
}
|
||||||
6
postcss.config.js
Normal file
6
postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
78
src/calculator.test.ts
Normal file
78
src/calculator.test.ts
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { calculateGridfinity, DEFAULT_SPEC } from './calculator';
|
||||||
|
|
||||||
|
describe('Gridfinity Calculator', () => {
|
||||||
|
it('calculates standard 42mm grids correctly in mm', () => {
|
||||||
|
const results = calculateGridfinity({
|
||||||
|
width: 84, // 2 grids
|
||||||
|
length: 126, // 3 grids
|
||||||
|
height: 50,
|
||||||
|
unit: 'mm',
|
||||||
|
spec: DEFAULT_SPEC
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(results.fullGridsX).toBe(2);
|
||||||
|
expect(results.fullGridsY).toBe(3);
|
||||||
|
expect(results.widthGrids).toBe(2);
|
||||||
|
expect(results.lengthGrids).toBe(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calculates fractional grids correctly', () => {
|
||||||
|
const results = calculateGridfinity({
|
||||||
|
width: 100, // 100 / 42 = 2.38
|
||||||
|
length: 42,
|
||||||
|
height: 50,
|
||||||
|
unit: 'mm',
|
||||||
|
spec: DEFAULT_SPEC
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(results.fullGridsX).toBe(2);
|
||||||
|
expect(results.widthGrids).toBeCloseTo(2.38, 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles inches correctly', () => {
|
||||||
|
const results = calculateGridfinity({
|
||||||
|
width: 1.6536, // Exactly 42.00144mm
|
||||||
|
length: 3.3071, // 84.00034mm
|
||||||
|
height: 2,
|
||||||
|
unit: 'in',
|
||||||
|
spec: DEFAULT_SPEC
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(results.fullGridsX).toBe(1);
|
||||||
|
expect(results.fullGridsY).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calculates max bin units correctly', () => {
|
||||||
|
// (50mm drawer - 4.8mm base) / 7mm unit = 6.45 -> 6U
|
||||||
|
const results = calculateGridfinity({
|
||||||
|
width: 42,
|
||||||
|
length: 42,
|
||||||
|
height: 50,
|
||||||
|
unit: 'mm',
|
||||||
|
spec: DEFAULT_SPEC
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(results.maxBinUnits).toBe(6);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('respects custom spec overrides', () => {
|
||||||
|
const customSpec = {
|
||||||
|
gridSize: 50,
|
||||||
|
heightUnit: 10,
|
||||||
|
baseThickness: 5
|
||||||
|
};
|
||||||
|
|
||||||
|
const results = calculateGridfinity({
|
||||||
|
width: 100,
|
||||||
|
length: 50,
|
||||||
|
height: 35,
|
||||||
|
unit: 'mm',
|
||||||
|
spec: customSpec
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(results.fullGridsX).toBe(2);
|
||||||
|
expect(results.fullGridsY).toBe(1);
|
||||||
|
expect(results.maxBinUnits).toBe(3); // (35 - 5) / 10 = 3
|
||||||
|
});
|
||||||
|
});
|
||||||
66
src/calculator.ts
Normal file
66
src/calculator.ts
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
export interface GridfinitySpec {
|
||||||
|
gridSize: number; // e.g., 42mm
|
||||||
|
heightUnit: number; // e.g., 7mm
|
||||||
|
baseThickness: number; // Height of baseplate above drawer surface
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DEFAULT_SPEC: GridfinitySpec = {
|
||||||
|
gridSize: 42,
|
||||||
|
heightUnit: 7,
|
||||||
|
baseThickness: 4.8, // Standard baseplate height is 5mm, but usually 4.8mm-5mm
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface CalculatorInputs {
|
||||||
|
width: number;
|
||||||
|
length: number;
|
||||||
|
height: number;
|
||||||
|
unit: 'mm' | 'in';
|
||||||
|
spec: GridfinitySpec;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CalculatorResults {
|
||||||
|
widthGrids: number;
|
||||||
|
lengthGrids: number;
|
||||||
|
fullGridsX: number;
|
||||||
|
fullGridsY: number;
|
||||||
|
maxBinUnits: number;
|
||||||
|
remainingWidth: number;
|
||||||
|
remainingLength: number;
|
||||||
|
remainingHeight: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const INCH_TO_MM = 25.4;
|
||||||
|
|
||||||
|
export function calculateGridfinity(inputs: CalculatorInputs): CalculatorResults {
|
||||||
|
const { width, length, height, unit, spec } = inputs;
|
||||||
|
|
||||||
|
// Convert all to mm for internal calculation
|
||||||
|
const w = unit === 'in' ? width * INCH_TO_MM : width;
|
||||||
|
const l = unit === 'in' ? length * INCH_TO_MM : length;
|
||||||
|
const h = unit === 'in' ? height * INCH_TO_MM : height;
|
||||||
|
|
||||||
|
const widthGrids = w / spec.gridSize;
|
||||||
|
const lengthGrids = l / spec.gridSize;
|
||||||
|
|
||||||
|
const fullGridsX = Math.floor(widthGrids);
|
||||||
|
const fullGridsY = Math.floor(lengthGrids);
|
||||||
|
|
||||||
|
// Height calculation: (Available Height - Baseplate Thickness) / 7mm
|
||||||
|
// We floor this because bins come in whole unit increments
|
||||||
|
const maxBinUnits = Math.max(0, Math.floor((h - spec.baseThickness) / spec.heightUnit));
|
||||||
|
|
||||||
|
const remainingWidth = w % spec.gridSize;
|
||||||
|
const remainingLength = l % spec.gridSize;
|
||||||
|
const remainingHeight = (h - spec.baseThickness) % spec.heightUnit;
|
||||||
|
|
||||||
|
return {
|
||||||
|
widthGrids,
|
||||||
|
lengthGrids,
|
||||||
|
fullGridsX,
|
||||||
|
fullGridsY,
|
||||||
|
maxBinUnits,
|
||||||
|
remainingWidth,
|
||||||
|
remainingLength,
|
||||||
|
remainingHeight
|
||||||
|
};
|
||||||
|
}
|
||||||
90
src/main.ts
Normal file
90
src/main.ts
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
import { calculateGridfinity, GridfinitySpec } from './calculator';
|
||||||
|
|
||||||
|
// DOM Elements
|
||||||
|
const inputWidth = document.getElementById('input-width') as HTMLInputElement;
|
||||||
|
const inputLength = document.getElementById('input-length') as HTMLInputElement;
|
||||||
|
const inputHeight = document.getElementById('input-height') as HTMLInputElement;
|
||||||
|
|
||||||
|
const unitMmBtn = document.getElementById('unit-mm') as HTMLButtonElement;
|
||||||
|
const unitInBtn = document.getElementById('unit-in') as HTMLButtonElement;
|
||||||
|
|
||||||
|
const specGridSize = document.getElementById('spec-grid-size') as HTMLInputElement;
|
||||||
|
const specHeightUnit = document.getElementById('spec-height-unit') as HTMLInputElement;
|
||||||
|
const specBaseThickness = document.getElementById('spec-base-thickness') as HTMLInputElement;
|
||||||
|
|
||||||
|
const resultsSection = document.getElementById('results') as HTMLElement;
|
||||||
|
const resultGridCount = document.getElementById('result-grid-count') as HTMLElement;
|
||||||
|
const resultGridFractional = document.getElementById('result-grid-fractional') as HTMLElement;
|
||||||
|
const resultBinHeight = document.getElementById('result-bin-height') as HTMLElement;
|
||||||
|
|
||||||
|
const gapWidth = document.getElementById('gap-width') as HTMLElement;
|
||||||
|
const gapLength = document.getElementById('gap-length') as HTMLElement;
|
||||||
|
const gapHeight = document.getElementById('gap-height') as HTMLElement;
|
||||||
|
|
||||||
|
// State
|
||||||
|
let currentUnit: 'mm' | 'in' = 'mm';
|
||||||
|
|
||||||
|
function updateResults() {
|
||||||
|
const width = parseFloat(inputWidth.value) || 0;
|
||||||
|
const length = parseFloat(inputLength.value) || 0;
|
||||||
|
const height = parseFloat(inputHeight.value) || 0;
|
||||||
|
|
||||||
|
const spec: GridfinitySpec = {
|
||||||
|
gridSize: parseFloat(specGridSize.value) || 42,
|
||||||
|
heightUnit: parseFloat(specHeightUnit.value) || 7,
|
||||||
|
baseThickness: parseFloat(specBaseThickness.value) || 4.8,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (width > 0 && length > 0 && height > 0) {
|
||||||
|
const results = calculateGridfinity({
|
||||||
|
width,
|
||||||
|
length,
|
||||||
|
height,
|
||||||
|
unit: currentUnit,
|
||||||
|
spec,
|
||||||
|
});
|
||||||
|
|
||||||
|
resultsSection.classList.remove('hidden');
|
||||||
|
|
||||||
|
resultGridCount.textContent = `${results.fullGridsX} x ${results.fullGridsY}`;
|
||||||
|
resultGridFractional.textContent = `${results.widthGrids.toFixed(2)} x ${results.lengthGrids.toFixed(2)} grids`;
|
||||||
|
resultBinHeight.textContent = `${results.maxBinUnits}U`;
|
||||||
|
|
||||||
|
const unitLabel = currentUnit === 'in' ? 'in' : 'mm';
|
||||||
|
const displayGapWidth = currentUnit === 'in' ? results.remainingWidth / 25.4 : results.remainingWidth;
|
||||||
|
const displayGapLength = currentUnit === 'in' ? results.remainingLength / 25.4 : results.remainingLength;
|
||||||
|
const displayGapHeight = currentUnit === 'in' ? results.remainingHeight / 25.4 : results.remainingHeight;
|
||||||
|
|
||||||
|
gapWidth.textContent = `${displayGapWidth.toFixed(1)} ${unitLabel}`;
|
||||||
|
gapLength.textContent = `${displayGapLength.toFixed(1)} ${unitLabel}`;
|
||||||
|
gapHeight.textContent = `${displayGapHeight.toFixed(1)} ${unitLabel}`;
|
||||||
|
} else {
|
||||||
|
resultsSection.classList.add('hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event Listeners
|
||||||
|
[inputWidth, inputLength, inputHeight, specGridSize, specHeightUnit, specBaseThickness].forEach(el => {
|
||||||
|
el.addEventListener('input', updateResults);
|
||||||
|
});
|
||||||
|
|
||||||
|
unitMmBtn.addEventListener('click', () => {
|
||||||
|
currentUnit = 'mm';
|
||||||
|
unitMmBtn.classList.add('bg-white', 'shadow-sm');
|
||||||
|
unitMmBtn.classList.remove('text-slate-600');
|
||||||
|
unitInBtn.classList.remove('bg-white', 'shadow-sm');
|
||||||
|
unitInBtn.classList.add('text-slate-600');
|
||||||
|
updateResults();
|
||||||
|
});
|
||||||
|
|
||||||
|
unitInBtn.addEventListener('click', () => {
|
||||||
|
currentUnit = 'in';
|
||||||
|
unitInBtn.classList.add('bg-white', 'shadow-sm');
|
||||||
|
unitInBtn.classList.remove('text-slate-600');
|
||||||
|
unitMmBtn.classList.remove('bg-white', 'shadow-sm');
|
||||||
|
unitMmBtn.classList.add('text-slate-600');
|
||||||
|
updateResults();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initial call
|
||||||
|
updateResults();
|
||||||
3
src/style.css
Normal file
3
src/style.css
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
11
tailwind.config.js
Normal file
11
tailwind.config.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
export default {
|
||||||
|
content: [
|
||||||
|
"./index.html",
|
||||||
|
"./src/**/*.{js,ts,jsx,tsx}",
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
extend: {},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
}
|
||||||
19
tsconfig.json
Normal file
19
tsconfig.json
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ESNext",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"lib": ["ESNext", "DOM"],
|
||||||
|
"moduleResolution": "Node",
|
||||||
|
"strict": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noImplicitReturns": true,
|
||||||
|
"skipLibCheck": true
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user