Skip to content

Developer Guide

Vincent Perrin edited this page Jan 19, 2026 · 1 revision

👨‍💻 Developer Guide

Complete guide for developers contributing to the Decompression Calculator project.

Table of Contents

  1. Development Setup
  2. Project Architecture
  3. Code Structure
  4. Development Workflow
  5. Component Development
  6. Algorithm Implementation
  7. State Management
  8. Testing
  9. Code Style
  10. Contributing

Development Setup

Prerequisites

  • Node.js 18+
  • npm or yarn
  • Git
  • VS Code (recommended)

Initial Setup

# 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

Recommended VS Code Extensions

{
  "recommendations": [
    "vue.volar",
    "vue.vscode-typescript-vue-plugin",
    "dbaeumer.vscode-eslint",
    "esbenp.prettier-vscode",
    "bradlc.vscode-tailwindcss"
  ]
}

Environment Configuration

Create .env.local for local development:

VITE_API_URL=http://localhost:3001
VITE_WEATHER_API_KEY=your_key_here
VITE_ENCRYPTION_KEY=dev_key_only

Project Architecture

Technology Stack

Frontend:

  • 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

Architecture Layers

┌─────────────────────────────────────┐
│         Presentation Layer          │
│  (Vue Components, Views, Router)    │
├─────────────────────────────────────┤
│        Application Layer            │
│    (Stores, Composables, i18n)      │
├─────────────────────────────────────┤
│         Business Logic              │
│  (Algorithm Utils, Services)        │
├─────────────────────────────────────┤
│          Data Layer                 │
│   (Types, Constants, API)           │
└─────────────────────────────────────┘

Code Structure

Directory Organization

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

File Naming Conventions

Components:

  • PascalCase: DiveCalculatorInput.vue
  • Descriptive names: CompartmentChart.vue

Utilities:

  • camelCase: tissueLoading.ts
  • Grouped by feature: buhlmann/decompression.ts

Types:

  • PascalCase interfaces: DiveProfile
  • Descriptive: DecompressionStop

Development Workflow

Branch Strategy

main (production)
  ↓
develop (integration)
  ↓
feature/feature-name (your work)

Creating a Feature

# 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-support

Conventional Commits

feat: new feature
fix: bug fix
docs: documentation
style: formatting
refactor: code restructuring
test: adding tests
chore: maintenance

Pull Request Process

  1. Create feature branch
  2. Implement changes
  3. Write/update tests
  4. Update documentation
  5. Create PR with description
  6. Address review comments
  7. Merge when approved

Component Development

Vue 3 Composition API

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>

Component Best Practices

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();

Algorithm Implementation

Bühlmann ZHL-16C

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 ...
}

Tissue Loading Calculation

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;
}

Gas Mix Calculations

// 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;
}

State Management

Pinia Store Structure

// 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;
    }
  }
});

Using Stores in Components

<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>

Testing

Unit Tests

// 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);
  });
});

Component Tests

// 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
    });
  });
});

Running Tests

# 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.ts

Code Style

TypeScript Guidelines

1. 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'
}

Vue Style Guide

Follow Vue 3 Style Guide:

Component Names:

// ✅ Good - Multi-word
DiveCalculatorInput.vue
CompartmentChart.vue

// ❌ Bad - Single word
Calculator.vue
Chart.vue

Props Definition:

// ✅ Good - Detailed
interface Props {
  depth: number;
  time: number;
  gasMix?: GasMix;
}

// ❌ Bad - Minimal
const props = defineProps(['depth', 'time']);

ESLint Configuration

{
  "extends": [
    "plugin:vue/vue3-recommended",
    "@vue/typescript/recommended"
  ],
  "rules": {
    "vue/multi-word-component-names": "error",
    "@typescript-eslint/no-explicit-any": "error",
    "no-console": "warn"
  }
}

Contributing

Before Contributing

  1. Read Contributing Guidelines
  2. Check existing issues
  3. Discuss major changes first

Contribution Checklist

  • 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

Code Review Process

  1. Automated Checks:

    • Linting passes
    • Tests pass
    • Build succeeds
  2. Manual Review:

    • Code quality
    • Architecture fit
    • Performance impact
    • Security considerations
  3. Approval:

    • At least one approval required
    • All comments addressed
    • CI/CD passes

Debugging

Vue DevTools

Install Vue DevTools:

  • Inspect component hierarchy
  • View component state
  • Track events
  • Monitor Pinia stores

Debug Configuration

// .vscode/launch.json
{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "chrome",
      "request": "launch",
      "name": "Debug Vue App",
      "url": "http://localhost:5173",
      "webRoot": "${workspaceFolder}/src"
    }
  ]
}

Common Issues

Issue: Hot reload not working

# Clear Vite cache
rm -rf node_modules/.vite
npm run dev

Issue: Type errors

# Regenerate types
npm run type-check

Performance Optimization

Code Splitting

// Lazy load routes
const routes = [
  {
    path: '/calculator',
    component: () => import('@/views/CalculatorPage.vue')
  }
];

Computed vs Methods

// ✅ Good - Cached
const totalTime = computed(() => {
  return bottomTime.value + decoTime.value;
});

// ❌ Bad - Recalculated every time
const getTotalTime = () => {
  return bottomTime.value + decoTime.value;
};

Debouncing Inputs

import { debounce } from 'lodash-es';

const handleInput = debounce((value: number) => {
  // Expensive calculation
}, 300);

Resources


Next: API Reference - Backend API documentation

Previous: User Guide - User documentation

Clone this wiki locally