-
Notifications
You must be signed in to change notification settings - Fork 0
Developer Guide
Complete guide for developers contributing to the Decompression Calculator project.
- Development Setup
- Project Architecture
- Code Structure
- Development Workflow
- Component Development
- Algorithm Implementation
- State Management
- Testing
- Code Style
- Contributing
- Node.js 18+
- npm or yarn
- Git
- VS Code (recommended)
# Clone repository
git clone https://github.com/your-username/decompression-calculator.git
cd decompression-calculator
# Install dependencies
npm install
# Start development server
npm run dev{
"recommendations": [
"vue.volar",
"vue.vscode-typescript-vue-plugin",
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"bradlc.vscode-tailwindcss"
]
}Create .env.local for local development:
VITE_API_URL=http://localhost:3001
VITE_WEATHER_API_KEY=your_key_here
VITE_ENCRYPTION_KEY=dev_key_onlyFrontend:
- Vue 3 (Composition API)
- TypeScript
- Vite
- Tailwind CSS
- Carbon Design System
State Management:
- Pinia
Routing:
- Vue Router
Charts:
- Chart.js + vue-chartjs
Backend:
- Node.js
- Express
- File-based storage
┌─────────────────────────────────────┐
│ Presentation Layer │
│ (Vue Components, Views, Router) │
├─────────────────────────────────────┤
│ Application Layer │
│ (Stores, Composables, i18n) │
├─────────────────────────────────────┤
│ Business Logic │
│ (Algorithm Utils, Services) │
├─────────────────────────────────────┤
│ Data Layer │
│ (Types, Constants, API) │
└─────────────────────────────────────┘
src/
├── components/ # Vue components
│ ├── calculator/ # Calculator-specific
│ ├── common/ # Shared components
│ ├── analysis/ # Analysis tools
│ ├── comparison/ # Model comparison
│ ├── education/ # Educational content
│ ├── repetitive/ # Repetitive dives
│ └── visualizations/ # Charts and graphs
├── composables/ # Composition API hooks
├── i18n/ # Internationalization
│ └── locales/ # Translation files
├── router/ # Vue Router config
├── services/ # Business services
├── stores/ # Pinia stores
├── types/ # TypeScript definitions
├── utils/ # Utility functions
│ ├── buhlmann/ # Bühlmann algorithm
│ ├── rgbm/ # RGBM algorithm
│ ├── vpmb/ # VPM-B algorithm
│ └── tables/ # Dive tables
├── views/ # Page components
├── App.vue # Root component
└── main.ts # Entry point
Components:
- PascalCase:
DiveCalculatorInput.vue - Descriptive names:
CompartmentChart.vue
Utilities:
- camelCase:
tissueLoading.ts - Grouped by feature:
buhlmann/decompression.ts
Types:
- PascalCase interfaces:
DiveProfile - Descriptive:
DecompressionStop
main (production)
↓
develop (integration)
↓
feature/feature-name (your work)
# Create feature branch
git checkout -b feature/multi-gas-support
# Make changes
# ... code ...
# Commit with conventional commits
git commit -m "feat: add multi-gas support to calculator"
# Push and create PR
git push origin feature/multi-gas-supportfeat: new feature
fix: bug fix
docs: documentation
style: formatting
refactor: code restructuring
test: adding tests
chore: maintenance
- Create feature branch
- Implement changes
- Write/update tests
- Update documentation
- Create PR with description
- Address review comments
- Merge when approved
Component Template:
<script setup lang="ts">
import { ref, computed, watch } from 'vue';
import type { DiveParameters } from '@/types';
// Props
interface Props {
depth: number;
time: number;
}
const props = defineProps<Props>();
// Emits
interface Emits {
(e: 'calculate', params: DiveParameters): void;
}
const emit = defineEmits<Emits>();
// State
const gasMix = ref({ oxygen: 21, nitrogen: 79, helium: 0 });
// Computed
const isValid = computed(() => {
return props.depth > 0 && props.time > 0;
});
// Methods
const handleCalculate = () => {
if (isValid.value) {
emit('calculate', {
depth: props.depth,
time: props.time,
gasMix: gasMix.value
});
}
};
// Watchers
watch(() => props.depth, (newDepth) => {
console.log('Depth changed:', newDepth);
});
</script>
<template>
<div class="calculator-input">
<h2>Dive Parameters</h2>
<!-- Template content -->
<button @click="handleCalculate" :disabled="!isValid">
Calculate
</button>
</div>
</template>
<style scoped>
.calculator-input {
/* Component styles */
}
</style>1. Single Responsibility:
// ✅ Good - focused component
const DiveDepthInput = () => { /* depth input only */ };
// ❌ Bad - too many responsibilities
const DiveInputEverything = () => { /* depth, time, gas, etc. */ };2. Props Validation:
interface Props {
depth: number; // Required
time?: number; // Optional
gasMix: GasMix; // Required complex type
}
const props = withDefaults(defineProps<Props>(), {
time: 0
});3. Emit Type Safety:
interface Emits {
(e: 'update:depth', value: number): void;
(e: 'calculate', params: DiveParameters): void;
}
const emit = defineEmits<Emits>();4. Composables for Reusability:
// composables/useDiveCalculation.ts
export function useDiveCalculation() {
const calculate = (params: DiveParameters) => {
// Calculation logic
};
return { calculate };
}
// In component
const { calculate } = useDiveCalculation();Core Files:
-
utils/buhlmann/constants.ts- ZHL-16C constants -
utils/buhlmann/tissueLoading.ts- Tissue calculations -
utils/buhlmann/decompression.ts- Deco calculations -
utils/buhlmann/gradientFactors.ts- GF logic
Adding a New Algorithm Feature:
// 1. Define types
interface NewFeature {
parameter: number;
result: number;
}
// 2. Implement calculation
export function calculateNewFeature(
tissues: TissueCompartment[],
depth: number
): NewFeature {
// Implementation
return {
parameter: 0,
result: 0
};
}
// 3. Add tests
describe('calculateNewFeature', () => {
it('should calculate correctly', () => {
const result = calculateNewFeature(tissues, 30);
expect(result.parameter).toBe(expectedValue);
});
});
// 4. Integrate into main calculation
export function calculateDiveProfile(params: DiveParameters) {
// ... existing code ...
const newFeature = calculateNewFeature(tissues, depth);
// ... use result ...
}Schreiner Equation (constant depth):
export function calculateTissueLoading(
initialPressure: number,
inspiredPressure: number,
time: number,
halfTime: number
): number {
const k = Math.LN2 / halfTime;
const pressure = inspiredPressure +
(initialPressure - inspiredPressure) * Math.exp(-k * time);
return pressure;
}Haldane Equation (changing depth):
export function calculateTissueLoadingWithDepthChange(
initialPressure: number,
initialInspired: number,
rate: number,
time: number,
halfTime: number
): number {
const k = Math.LN2 / halfTime;
const R = rate;
const pressure = initialInspired + R * (time - 1/k) -
(initialInspired - initialPressure - R/k) * Math.exp(-k * time);
return pressure;
}// utils/gasMix.ts
export function calculateMOD(
oxygenFraction: number,
maxPPO2: number = 1.4
): number {
return ((maxPPO2 / oxygenFraction) - 1) * 10;
}
export function calculatePPO2(
oxygenFraction: number,
depth: number
): number {
const ambientPressure = (depth / 10) + 1;
return oxygenFraction * ambientPressure;
}
export function calculateEND(
depth: number,
nitrogenFraction: number
): number {
const ambientPressure = (depth / 10) + 1;
const nitrogenPressure = nitrogenFraction * ambientPressure;
return (nitrogenPressure / 0.79 - 1) * 10;
}// stores/diveStore.ts
import { defineStore } from 'pinia';
import type { DiveParameters, DiveProfile } from '@/types';
export const useDiveStore = defineStore('dive', {
state: () => ({
parameters: null as DiveParameters | null,
profile: null as DiveProfile | null,
isCalculating: false,
error: null as string | null
}),
getters: {
hasProfile: (state) => state.profile !== null,
totalDiveTime: (state) => state.profile?.totalDiveTime ?? 0
},
actions: {
async calculateProfile(params: DiveParameters) {
this.isCalculating = true;
this.error = null;
try {
const profile = await calculateDiveProfile(params);
this.parameters = params;
this.profile = profile;
} catch (error) {
this.error = error.message;
} finally {
this.isCalculating = false;
}
},
clearProfile() {
this.parameters = null;
this.profile = null;
this.error = null;
}
}
});<script setup lang="ts">
import { useDiveStore } from '@/stores/diveStore';
const diveStore = useDiveStore();
const handleCalculate = (params: DiveParameters) => {
diveStore.calculateProfile(params);
};
</script>
<template>
<div>
<div v-if="diveStore.isCalculating">Calculating...</div>
<div v-else-if="diveStore.hasProfile">
<!-- Show results -->
</div>
</div>
</template>// utils/buhlmann/__tests__/tissueLoading.test.ts
import { describe, it, expect } from 'vitest';
import { calculateTissueLoading } from '../tissueLoading';
describe('calculateTissueLoading', () => {
it('should calculate tissue loading correctly', () => {
const result = calculateTissueLoading(
0.79, // initial pressure (surface)
3.16, // inspired pressure (30m)
20, // time (minutes)
5.0 // half-time
);
expect(result).toBeCloseTo(2.89, 2);
});
it('should handle zero time', () => {
const result = calculateTissueLoading(0.79, 3.16, 0, 5.0);
expect(result).toBe(0.79);
});
});// components/__tests__/DiveCalculatorInput.test.ts
import { mount } from '@vue/test-utils';
import { describe, it, expect } from 'vitest';
import DiveCalculatorInput from '../DiveCalculatorInput.vue';
describe('DiveCalculatorInput', () => {
it('renders correctly', () => {
const wrapper = mount(DiveCalculatorInput);
expect(wrapper.find('h2').text()).toBe('Dive Parameters');
});
it('emits calculate event with correct data', async () => {
const wrapper = mount(DiveCalculatorInput);
await wrapper.find('input[name="depth"]').setValue(30);
await wrapper.find('input[name="time"]').setValue(25);
await wrapper.find('button').trigger('click');
expect(wrapper.emitted('calculate')).toBeTruthy();
expect(wrapper.emitted('calculate')[0][0]).toEqual({
depth: 30,
time: 25
});
});
});# Run all tests
npm run test
# Run with coverage
npm run test:coverage
# Run in watch mode
npm run test:watch
# Run specific test file
npm run test tissueLoading.test.ts1. Use Strict Types:
// ✅ Good
interface DiveParameters {
depth: number;
time: number;
gasMix: GasMix;
}
// ❌ Bad
interface DiveParameters {
depth: any;
time: any;
gasMix: any;
}2. Avoid Type Assertions:
// ✅ Good
const depth = parseFloat(input);
if (!isNaN(depth)) {
// Use depth
}
// ❌ Bad
const depth = input as number;3. Use Enums for Constants:
enum DivePhase {
Descent = 'descent',
Bottom = 'bottom',
Ascent = 'ascent',
Deco = 'deco'
}Follow Vue 3 Style Guide:
Component Names:
// ✅ Good - Multi-word
DiveCalculatorInput.vue
CompartmentChart.vue
// ❌ Bad - Single word
Calculator.vue
Chart.vueProps Definition:
// ✅ Good - Detailed
interface Props {
depth: number;
time: number;
gasMix?: GasMix;
}
// ❌ Bad - Minimal
const props = defineProps(['depth', 'time']);{
"extends": [
"plugin:vue/vue3-recommended",
"@vue/typescript/recommended"
],
"rules": {
"vue/multi-word-component-names": "error",
"@typescript-eslint/no-explicit-any": "error",
"no-console": "warn"
}
}- Read Contributing Guidelines
- Check existing issues
- Discuss major changes first
- Code follows style guide
- Tests added/updated
- Documentation updated
- Commit messages follow convention
- PR description is clear
- No console.log statements
- Types are properly defined
-
Automated Checks:
- Linting passes
- Tests pass
- Build succeeds
-
Manual Review:
- Code quality
- Architecture fit
- Performance impact
- Security considerations
-
Approval:
- At least one approval required
- All comments addressed
- CI/CD passes
Install Vue DevTools:
- Inspect component hierarchy
- View component state
- Track events
- Monitor Pinia stores
// .vscode/launch.json
{
"version": "0.2.0",
"configurations": [
{
"type": "chrome",
"request": "launch",
"name": "Debug Vue App",
"url": "http://localhost:5173",
"webRoot": "${workspaceFolder}/src"
}
]
}Issue: Hot reload not working
# Clear Vite cache
rm -rf node_modules/.vite
npm run devIssue: Type errors
# Regenerate types
npm run type-check// Lazy load routes
const routes = [
{
path: '/calculator',
component: () => import('@/views/CalculatorPage.vue')
}
];// ✅ Good - Cached
const totalTime = computed(() => {
return bottomTime.value + decoTime.value;
});
// ❌ Bad - Recalculated every time
const getTotalTime = () => {
return bottomTime.value + decoTime.value;
};import { debounce } from 'lodash-es';
const handleInput = debounce((value: number) => {
// Expensive calculation
}, 300);Next: API Reference - Backend API documentation
Previous: User Guide - User documentation