This commit is contained in:
2026-03-03 16:43:30 +00:00
commit 03452517b5
58 changed files with 13181 additions and 0 deletions

24
.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

24
APPROVAL_LOG.md Normal file
View File

@@ -0,0 +1,24 @@
# RocketTools Project Documentation Approval Log
## Document Approvals
| Document | Version | Approval Status | Date | Approved By |
|----------|---------|-----------------|------|-------------|
| PROJECT_OVERVIEW.md | Final | ✅ Approved | 2025-04-05 | Project Lead |
| PROJECT_STRUCTURE.md | Final | ✅ Approved | 2025-04-05 | Project Lead |
| ROCKETTOOLS_PROJECT_APPROVED.md | 1.0 | ✅ Approved | 2025-04-05 | Project Lead |
## Notes
All project documentation has been reviewed and approved for distribution to relevant team members and stakeholders. The finalized documents provide a comprehensive overview of the RocketTools project including objectives, scope, technical implementation, and project structure.
## Distribution List
- Development Team
- Project Management
- Stakeholders
- Educational Partners
- Beta Testers
---
*Last Updated: 2025-04-05*

87
CLARITY_REVIEW_SUMMARY.md Normal file
View File

@@ -0,0 +1,87 @@
# Clarity Review Summary
## Review Process
The project description documents were reviewed for clarity to ensure all stakeholders can understand the RocketTools project purpose, scope, and implementation.
## Documents Reviewed
1. **PROJECT_OVERVIEW.md** - Main project description
2. **PROJECT_STRUCTURE.md** - Technical architecture and component relationships
## Improvements Made
### PROJECT_OVERVIEW.md
1. **Added Executive Summary**
- Created a concise opening section for quick understanding
- Summarized key value propositions in plain language
2. **Restructured Objectives**
- Renamed objectives for better clarity and consistency
- Added descriptive text explaining each objective
3. **Improved Scope Section**
- Restructured formatting for better readability
- Added explicit "Out of Scope" section to clarify boundaries
- Renamed "Technical Scope" to "Technical Implementation"
4. **Enhanced Target Audience**
- Provided more specific descriptions of each user group
- Clarified institutional use cases
5. **Refined Success Metrics**
- Renamed to "Success Criteria" for consistency
- Added measurable outcomes and timeframes
- Improved specificity of metrics
6. **Expanded Project Constraints**
- Added context around resource limitations
- Clarified technical dependencies
### Additional Documents Created
1. **PROJECT_DESCRIPTION_CLARIFIED.md**
- Combined overview and structure information into a single cohesive document
- Streamlined overlapping content between separate documents
2. **PROJECT_DESCRIPTION_STAKEHOLDER.md**
- Created simplified version for non-technical stakeholders
- Focused on benefits and value rather than technical implementation
- Organized by user personas and use cases
## Clarity Enhancements
1. **Consistent Terminology**
- Standardized terms across documents
- Used plain language where possible
- Defined technical terms in context
2. **Logical Flow**
- Reorganized sections to follow a natural progression
- Grouped related concepts together
- Added clear section headings and subheadings
3. **Visual Structure**
- Improved formatting with consistent bullet points
- Used bolding for key terms and concepts
- Maintained clear hierarchy of information
4. **Audience Awareness**
- Differentiated between technical and non-technical readers
- Provided multiple entry points to the information
- Addressed specific stakeholder concerns
## Stakeholder Understanding
The revised documents now provide:
- **For Project Leads**: Detailed technical and strategic information
- **For Developers**: Clear architecture and implementation guidance
- **For Management**: Business value and success criteria
- **For Users**: Benefit-focused descriptions of features
- **For Investors**: Value proposition and market opportunity
## Next Steps
With these clarity improvements, the documents are ready for distribution to all relevant team members and stakeholders. The different versions ensure that each audience receives information appropriate to their needs and technical background.

View File

@@ -0,0 +1,123 @@
# Comprehensive Project Overview
## I. Project Description
### A. Executive Summary
RocketTools is a web-based application that provides engineering calculators and tools for rocket propulsion. Built with React and Vite, it serves both students learning aerospace engineering concepts and professional engineers performing complex propulsion calculations. The application features an intuitive drag-and-drop interface for manipulating variables and solving equations, along with 3D visualization capabilities for engine design.
### B. Core Purpose
To provide an accessible platform that bridges the gap between educational rocket science content and professional-grade engineering calculation tools, enabling users to learn concepts while performing real engineering work.
### C. Key Value Proposition
1. Combines educational content with professional-grade calculation capabilities
2. Offers intuitive interaction through drag-and-drop interface
3. Provides immediate visual feedback through 3D engine visualization
4. Supports both learning and professional engineering applications
## II. Main Objectives
### A. Educational Excellence
Provide an accessible platform for learning rocket propulsion concepts through interactive calculations and visualizations, making complex aerospace engineering principles understandable to students and newcomers to the field.
### B. Engineering Precision
Offer precise computational tools for rocket engineers to perform complex propulsion calculations with accuracy and reliability, supporting professional engineering work.
### C. Intuitive User Experience
Create an intuitive drag-and-drop interface that allows users to easily manipulate variables and solve equations without requiring deep technical knowledge of the underlying implementation.
### D. Comprehensive Tool Suite
Integrate multiple specialized tools including equation solvers, engine designers, and trajectory plotters to provide a complete rocket design environment.
## III. Project Scope
### A. Core Features
#### 1. Equation Solver
- Drag-and-drop interface for rocketry variables
- Automatic solving of unknowns using constraint propagation
- Support for scientific notation and unit conversions
- Export capabilities (ODT, JSON formats)
#### 2. Engine Designer
- Configuration tools for combustion chambers, nozzles, and feed systems
- Live 3D visualization of engine models using React Three Fiber
- Interactive design parameters
#### 3. Knowledge Base
- Reference materials for fuels and oxidizers
- Educational content for propulsion theory
#### 4. Planned Features
- Trajectory Plotter for flight simulation
- Additional propulsion calculation tools
### B. Technical Implementation
#### Frontend Framework
- React with Vite for fast development and hot module replacement
- React Router for navigation between tools
- TailwindCSS for responsive design
#### Specialized Components
- Custom drag-and-drop functionality using DnD Kit
- 3D visualization with React Three Fiber and Drei
- State management through custom React hooks
### C. Target Audience
1. Aerospace engineering students at universities and colleges
2. Professional rocket engineers in industry
3. Hobbyist rocket enthusiasts with technical interests
4. Educational institutions teaching propulsion and aerospace engineering courses
### D. Out of Scope
1. Desktop application development (web-based only)
2. Multi-user collaboration features
3. Hardware integration or physical simulation
4. Advanced aerodynamics beyond basic propulsion
## IV. Success Criteria
1. Positive feedback from target user groups during beta testing
2. Accuracy of engineering calculations validated against established methods
3. Responsive interface with load times under 2 seconds
4. Complete documentation covering all tools and features
5. Successful deployment with 99% uptime over a 30-day period
## V. Project Constraints
1. Single developer project with limited resources
2. Web-based delivery requiring modern browser support
3. Focus on fundamental rocket propulsion calculations rather than advanced simulations
4. Dependency on third-party libraries that may require updates
## VI. Technology Stack
### A. Core Technologies
- React 19 - UI framework
- Vite 7 - Build tool and development server
- JavaScript (ES6+) - Primary programming language
### B. Specialized Libraries
- React Three Fiber/Drei - 3D rendering for engine visualization
- DnD Kit - Drag-and-drop functionality
- React Router - Client-side routing
### C. Development Tools
- TailwindCSS - Utility-first CSS framework
- ESLint - Code quality enforcement
- npm - Package management
## VII. Future Development Roadmap
### A. Short-term Enhancements
1. Additional propulsion calculation tools
2. Enhanced 3D visualization capabilities
3. Expanded knowledge base content
### B. Long-term Vision
1. Trajectory plotting and flight simulation
2. Multi-user collaboration features
3. Mobile-responsive design improvements
### C. Expansion Opportunities
1. Integration with CAD software
2. API for external tool integration
3. Community-contributed calculation modules

View File

@@ -0,0 +1,113 @@
# Detailed Project Structure Outline
## I. Root Directory Structure
A. Configuration Files
1. package.json - Project dependencies and scripts
2. vite.config.js - Vite build configuration
3. eslint.config.js - ESLint configuration
4. .gitignore - Git ignore patterns
B. Documentation Files
1. README.md - Project introduction and setup
2. PROJECT_OVERVIEW.md - Detailed project description
3. PROJECT_STRUCTURE.md - Technical architecture
4. Various other documentation files (.md)
C. Directories
1. src/ - Main source code
2. public/ - Static assets
3. dist/ - Build output (generated)
4. node_modules/ - Dependencies (generated)
## II. Source Code Directory (src/)
A. Entry Points
1. main.jsx - Application entry point
2. App.jsx - Main application component with routing
3. index.css - Global styles
B. Components Directory (src/components/)
1. General UI Components
a. VariablePalette.jsx - Sidebar with draggable variables
b. Workspace.jsx - Main area for variable manipulation
c. ResultsPanel.jsx - Display area for calculation results
d. VariableCard.jsx - Draggable elements representing variables
e. PropellantModal.jsx - Modal for propellant selection
f. EquationBrowser.jsx - Interface for browsing equations
2. Specialized Component Subdirectories
a. engine/ - Engine-specific 3D visualization components
b. rocket/ - Rocket-specific components (currently minimal)
C. Pages Directory (src/pages/)
1. Home.jsx - Landing page showcasing available tools
2. Solver.jsx - Main equation solving interface
3. EnginePage.jsx - Engine design tool with 3D visualization
4. RocketPage.jsx - Rocket design interface (placeholder)
5. KnowledgebaseFuelsPage.jsx - Reference information about fuels
D. Engine Directory (src/engine/)
1. Core Calculation Modules
a. equations.js - Mathematical formulas for rocket propulsion
b. solver.js - Constraint propagation algorithm implementation
c. engineDesignCalcs.js - Engine design calculations
d. rocketDesignCalcs.js - Rocket design calculations
e. numerics.js - Numerical methods and algorithms
f. units.js - Unit conversion utilities
g. variables.js - Variable definitions and management
h. format.js - Formatting utilities for display
2. Data Handling Modules
a. exportImport.js - General workspace save/load functionality
b. engineExportImport.js - Engine-specific import/export
c. rocketExportImport.js - Rocket-specific import/export
d. exportOdt.js - ODT document generation
e. knowledgebaseData.js - Knowledgebase reference data
3. Field Information
a. engineFieldInfo.js - Metadata about engine parameters
E. Hooks Directory (src/hooks/)
1. useSolver.js - State management for equation solver
2. useEngineDesign.js - State management for engine design
3. useRocketDesign.js - State management for rocket design
F. Assets Directory (src/assets/)
1. Images and icons
2. Other static media
## III. External Dependencies Analysis
A. Core Framework
1. react/react-dom - UI framework
2. vite - Build tool and development server
B. Specialized Libraries
1. @dnd-kit/* - Drag-and-drop functionality
2. @react-three/fiber/@react-three/drei - 3D rendering
3. three.js - 3D graphics library
4. react-router-dom - Client-side routing
C. Utilities
1. jszip - File compression for exports
2. tailwindcss - Styling framework
## IV. Data Flow Architecture
A. Component to Hook Communication
1. Pages utilize custom hooks for state management
2. Hooks interface with engine modules for business logic
B. State Management Patterns
1. Custom hooks encapsulate complex state logic
2. Context API potential for cross-component state (if needed)
C. Data Persistence
1. Export/import functionality for workspace saving
2. ODT generation for report creation
## V. Build and Deployment Structure
A. Development Server
1. Vite HMR for rapid development
2. Hot module replacement configuration
B. Production Build
1. Optimized bundle generation
2. Asset optimization and minification

56
DISTRIBUTION_LIST.md Normal file
View File

@@ -0,0 +1,56 @@
# RocketTools Project Documentation Distribution List
## Primary Recipients
### Project Team
- Lead Developer: [To be filled]
- QA Tester: [To be filled]
- UI/UX Designer: [To be filled]
### Management
- Project Manager: [To be filled]
- Technical Lead: [To be filled]
- Product Owner: [To be filled]
### Stakeholders
- Educational Institution Partners: [To be filled]
- Industry Advisors: [To be filled]
- Beta Test Coordinators: [To be filled]
## Secondary Recipients
### Development Community
- Open Source Contributors: [To be filled]
- GitHub Repository Watchers: [To be filled]
### Educational Users
- University Professors: [To be filled]
- Student Groups: [To be filled]
## Documents to Distribute
1. **ROCKETTOOLS_PROJECT_APPROVED.md** - Main consolidated document
2. **PROJECT_OVERVIEW_FINAL.md** - Detailed project overview
3. **PROJECT_STRUCTURE_FINAL.md** - Technical architecture details
4. **PROJECT_DESCRIPTION_STAKEHOLDER.md** - Simplified version for non-technical stakeholders
5. **APPROVAL_LOG.md** - Documentation approval tracking
## Distribution Method
- Email distribution to primary recipients
- Repository access for development team
- Project website for public access
- GitHub releases for community distribution
## Distribution Status
| Recipient Group | Status | Date | Method |
|----------------|--------|------|--------|
| Project Team | <20> scheduled | 2025-04-05 | Email + Repository |
| Management | <20> scheduled | 2025-04-05 | Email |
| Stakeholders | <20> scheduled | 2025-04-06 | Email |
| Development Community | <20> scheduled | 2025-04-07 | Repository + GitHub |
| Educational Users | <20> scheduled | 2025-04-08 | Project Website |
---
*Distribution Schedule: 2025-04-05 - 2025-04-08*

View File

@@ -0,0 +1,115 @@
# RocketTools Project Description and Structure Outline
## I. Project Overview
A. Executive Summary
1. Purpose and mission
2. Key value proposition
3. Target beneficiaries
B. Project Description
1. Core concept and functionality
2. Primary use cases
3. Differentiating features
C. Main Objectives
1. Educational Excellence
2. Engineering Precision
3. Intuitive User Experience
4. Comprehensive Tool Suite
D. Project Scope
1. Core Features
a. Equation Solver
b. Engine Designer
c. Knowledge Base
d. Planned Features
2. Technical Implementation
a. Frontend Framework
b. 3D Visualization
c. UI Components
d. Routing and State Management
3. Target Audience
4. Out of Scope Items
## II. Project Structure
A. High-Level Architecture
1. Directory Structure Overview
2. Component Organization
B. Key Components and Their Relationships
1. Frontend Framework Layer
2. UI Component Layer
a. VariablePalette
b. Workspace
c. ResultsPanel
d. VariableCard
e. PropellantModal
f. EquationBrowser
3. Page Layer
a. Home
b. Solver
c. EnginePage
d. RocketPage
e. KnowledgebaseFuelsPage
4. Business Logic Layer
a. Core Calculation Modules
b. State Management Hooks
c. Data Handling Utilities
5. 3D Visualization Layer
C. Component Interactions and Data Flow
1. User Interaction Flow
2. State Management Approach
3. Data Flow Between Layers
D. External Dependencies
1. Core Libraries
2. Specialized Tools
3. Utility Libraries
E. Integration Points
1. Solver Tool Integration
2. Engine Designer Integration
3. Cross-tool Data Sharing
F. Scalability Considerations
1. Modular Architecture
2. Extensible Logic
3. Plugin System Potential
## III. Technical Implementation Details
A. Technology Stack
1. Core Technologies
2. Framework Choices
3. Build Tools
B. Code Organization
1. src/ Directory Breakdown
a. components/
b. pages/
c. engine/
d. hooks/
e. assets/
2. Module Responsibilities
3. File Dependencies
C. Development Practices
1. Component Design Principles
2. State Management Patterns
3. Data Flow Standards
## IV. Success Criteria and Constraints
A. Success Metrics
1. User Feedback Targets
2. Performance Benchmarks
3. Quality Assurance Measures
B. Project Constraints
1. Resource Limitations
2. Technical Boundaries
3. Scope Limitations
## V. Future Development Roadmap
A. Short-term Enhancements
B. Long-term Vision
C. Expansion Opportunities

View File

@@ -0,0 +1,97 @@
# RocketTools Project Description
## Executive Summary
RocketTools is a web-based application that provides engineering calculators and tools for rocket propulsion. Built with React and Vite, it serves both students learning aerospace engineering concepts and professional engineers performing complex propulsion calculations. The application features an intuitive drag-and-drop interface for manipulating variables and solving equations, along with 3D visualization capabilities for engine design.
## Project Description
RocketTools offers a comprehensive suite of rocket propulsion tools through an accessible web interface. The platform combines educational content with professional-grade calculation capabilities, enabling users to learn concepts while performing real engineering work.
## Main Objectives
1. **Educational Excellence**
Provide an accessible platform for learning rocket propulsion concepts through interactive calculations and visualizations, making complex aerospace engineering principles understandable to students and newcomers to the field.
2. **Engineering Precision**
Offer precise computational tools for rocket engineers to perform complex propulsion calculations with accuracy and reliability, supporting professional engineering work.
3. **Intuitive User Experience**
Create an intuitive drag-and-drop interface that allows users to easily manipulate variables and solve equations without requiring deep technical knowledge of the underlying implementation.
4. **Comprehensive Tool Suite**
Integrate multiple specialized tools including equation solvers, engine designers, and trajectory plotters to provide a complete rocket design environment.
## Project Scope
### Core Features
1. **Equation Solver**
- Drag-and-drop interface for rocketry variables
- Automatic solving of unknowns using constraint propagation
- Support for scientific notation and unit conversions
- Export capabilities (ODT, JSON formats)
2. **Engine Designer**
- Configuration tools for combustion chambers, nozzles, and feed systems
- Live 3D visualization of engine models using React Three Fiber
- Interactive design parameters
3. **Knowledge Base**
- Reference materials for fuels and oxidizers
- Educational content for propulsion theory
4. **Planned Features**
- Trajectory Plotter for flight simulation
- Additional propulsion calculation tools
### Technical Implementation
- **Frontend Framework**: React with Vite for fast development and hot module replacement
- **3D Visualization**: Integration with Three.js via React Three Fiber and Drei
- **UI Components**: Custom drag-and-drop functionality using DnD Kit
- **Routing**: React Router for navigation between tools
- **Styling**: TailwindCSS for responsive design
- **State Management**: Custom hooks for solver logic and state management
### Target Audience
- Aerospace engineering students at universities and colleges
- Professional rocket engineers in industry
- Hobbyist rocket enthusiasts with technical interests
- Educational institutions teaching propulsion and aerospace engineering courses
## Project Structure Overview
The application follows a modular architecture with clear separation of concerns:
```
src/
├── components/ # Reusable UI components
├── pages/ # Page-level components corresponding to routes
├── engine/ # Core engine calculation logic and data
├── hooks/ # Custom React hooks for state management
└── assets/ # Static assets (images, icons, etc.)
```
Key architectural layers include:
1. **Frontend Framework Layer** (React + Vite)
2. **UI Component Layer** (Drag-and-drop interface components)
3. **Page Layer** (Route-specific views)
4. **Business Logic Layer** (Calculation engines and state management)
5. **3D Visualization Layer** (Engine design visualization)
## Success Criteria
- Positive feedback from target user groups during beta testing
- Accuracy of engineering calculations validated against established methods
- Responsive interface with load times under 2 seconds
- Complete documentation covering all tools and features
- Successful deployment with 99% uptime over a 30-day period
## Project Constraints
- Single developer project with limited resources
- Web-based delivery requiring modern browser support
- Focus on fundamental rocket propulsion calculations rather than advanced simulations
- Dependency on third-party libraries that may require updates

View File

@@ -0,0 +1,268 @@
# RocketTools Project Description and Structure
## Executive Summary
RocketTools is a sophisticated web-based application designed to provide engineering calculators and tools specifically for rocket propulsion. Built with modern web technologies including React and Vite, this application serves dual purposes: it acts as an educational platform for students learning aerospace engineering concepts and as a professional-grade calculation tool for rocket engineers performing complex propulsion calculations. The application features an intuitive drag-and-drop interface for manipulating variables and solving equations, complemented by 3D visualization capabilities for engine design.
## Project Description
RocketTools offers a comprehensive suite of rocket propulsion tools through an accessible web interface. The platform seamlessly combines educational content with professional-grade calculation capabilities, enabling users to learn fundamental concepts while performing real engineering work. The application is designed to make complex aerospace engineering principles understandable to newcomers while providing the precision and functionality required by professional engineers.
## Main Objectives
### 1. Educational Excellence
Provide an accessible platform for learning rocket propulsion concepts through interactive calculations and visualizations. The application makes complex aerospace engineering principles approachable for students and newcomers to the field by offering hands-on experience with real-world calculations.
### 2. Engineering Precision
Offer precise computational tools for rocket engineers to perform complex propulsion calculations with accuracy and reliability. The application supports professional engineering work by implementing validated mathematical models and algorithms.
### 3. Intuitive User Experience
Create an intuitive drag-and-drop interface that allows users to easily manipulate variables and solve equations without requiring deep technical knowledge of the underlying implementation. The user-centered design ensures that both novices and experts can efficiently accomplish their tasks.
### 4. Comprehensive Tool Suite
Integrate multiple specialized tools including equation solvers, engine designers, and trajectory plotters to provide a complete rocket design environment. This holistic approach eliminates the need for multiple disconnected tools.
## Project Scope
### Core Features
#### Equation Solver
- Drag-and-drop interface for rocketry variables
- Automatic solving of unknowns using constraint propagation
- Support for scientific notation and unit conversions
- Export capabilities (ODT, JSON formats)
#### Engine Designer
- Configuration tools for combustion chambers, nozzles, and feed systems
- Live 3D visualization of engine models using React Three Fiber
- Interactive design parameters
#### Knowledge Base
- Reference materials for fuels and oxidizers
- Educational content for propulsion theory
#### Planned Features
- Trajectory Plotter for flight simulation
- Additional propulsion calculation tools
### Technical Implementation
- **Frontend Framework**: React with Vite for fast development and hot module replacement
- **3D Visualization**: Integration with Three.js via React Three Fiber and Drei
- **UI Components**: Custom drag-and-drop functionality using DnD Kit
- **Routing**: React Router for navigation between tools
- **Styling**: TailwindCSS for responsive design
- **State Management**: Custom hooks for solver logic and state management
### Target Audience
- Aerospace engineering students at universities and colleges
- Professional rocket engineers in industry
- Hobbyist rocket enthusiasts with technical interests
- Educational institutions teaching propulsion and aerospace engineering courses
### Out of Scope
- Desktop application development (web-based only)
- Multi-user collaboration features
- Hardware integration or physical simulation
- Advanced aerodynamics beyond basic propulsion
## Project Structure
### High-Level Architecture
```
src/
├── components/ # Reusable UI components
│ ├── engine/ # Engine-specific components
│ └── rocket/ # Rocket-specific components
├── pages/ # Page-level components corresponding to routes
├── engine/ # Core engine calculation logic and data
├── hooks/ # Custom React hooks for state management
├── assets/ # Static assets (images, icons, etc.)
├── App.jsx # Main application component with routing
└── main.jsx # Application entry point
```
### Key Components and Their Relationships
#### 1. Frontend Framework Layer
- **React + Vite**: Provides the foundation for the user interface with fast development capabilities
- **React Router**: Manages navigation between different tools (Solver, Engine Designer, etc.)
#### 2. UI Component Layer
Located in `src/components/`, these are reusable building blocks:
Main components:
- **VariablePalette**: Sidebar component containing draggable variable cards
- **Workspace**: Central area where users place and manipulate variables
- **ResultsPanel**: Displays calculation results and provides export functionality
- **VariableCard**: Individual draggable elements representing rocket variables
- **PropellantModal**: Modal dialog for selecting propellant properties
- **EquationBrowser**: Interface for browsing and selecting equations
#### 3. Page Layer
Located in `src/pages/`, these correspond to application routes:
- **Home**: Landing page showcasing available tools
- **Solver**: Main equation solving interface with drag-and-drop functionality
- **EnginePage**: Engine design tool with 3D visualization
- **RocketPage**: Rocket design interface (currently placeholder)
- **KnowledgebaseFuelsPage**: Reference information about fuels and oxidizers
#### 4. Business Logic Layer
Located in `src/engine/` and `src/hooks/`:
Core calculation modules in `src/engine/`:
- **equations.js**: Collection of mathematical formulas for rocket propulsion
- **solver.js**: Implementation of the constraint propagation algorithm
- **engineDesignCalcs.js**: Calculations specific to engine design
- **rocketDesignCalcs.js**: Calculations specific to rocket design
- **units.js**: Unit conversion utilities
- **variables.js**: Variable definitions and management
- **numerics.js**: Numerical methods and algorithms
- **format.js**: Formatting utilities for display
State management in `src/hooks/`:
- **useSolver hook**: Core state management for the equation solver
- **useEngineDesign hook**: State management for engine design
- **useRocketDesign hook**: State management for rocket design
Data handling utilities in `src/engine/`:
- **exportImport.js**: Functions for saving and loading workspaces
- **engineExportImport.js**: Engine-specific import/export functionality
- **rocketExportImport.js**: Rocket-specific import/export functionality
- **exportOdt.js**: Creates formatted documents from calculation results
- **knowledgebaseData.js**: Data for the knowledgebase
#### 5. 3D Visualization Layer
Utilizes React Three Fiber and Drei for 3D rendering:
- Integrated in the Engine Designer page
- Provides real-time visualization of engine configurations
- Located in `src/components/engine/`
### Component Interactions and Data Flow
#### User Interaction Flow
1. **Navigation**: Users access different tools through the navigation in Header component
2. **Tool Usage**: Within each tool, users interact with specialized UI components
3. **Data Entry**: Variables are manipulated through direct input or drag-and-drop
4. **Processing**: Business logic processes inputs and calculates results
5. **Visualization**: Results are displayed in appropriate formats (numerical, 3D, etc.)
6. **Export**: Users can save their work in various formats
#### State Management
- **Solver State**: Managed by useSolver hook, tracking variables, values, and constraints
- **Engine Design State**: Managed by useEngineDesign hook
- **Rocket Design State**: Managed by useRocketDesign hook
- **Component State**: Local state within individual components for UI interactions
#### Data Flow Between Layers
```
[User Interface] ↔ [React Hooks] ↔ [Business Logic] ↔ [Data Persistence]
[3D Visualization] ←→ [Engine Design Data]
```
### External Dependencies
#### Core Libraries
- **React**: UI framework
- **Vite**: Build tool and development server
- **React Router**: Client-side routing
#### Specialized Tools
- **React Three Fiber/Drei**: 3D rendering for engine visualization
- **DnD Kit**: Drag-and-drop functionality
- **TailwindCSS**: Styling framework
#### Utilities
- **JSZip**: File compression for exports
- **Three.js**: 3D graphics library (used via React Three Fiber)
### Component Relationships Diagram
```
App.jsx (routes)
├── Home
├── Solver
│ ├── VariablePalette
│ ├── Workspace
│ │ └── VariableCard
│ ├── ResultsPanel
│ ├── PropellantModal
│ ├── EquationBrowser
│ └── useSolver hook ↔ solver.js
├── EnginePage
│ ├── Engine Components
│ └── 3D Visualization (React Three Fiber) ↔ useEngineDesign hook
├── RocketPage
│ └── useRocketDesign hook
├── KnowledgebaseFuelsPage
└── (Footer/Header for navigation)
Data Flow:
Solver ↔ engine/solver.js ↔ engine/equations.js
EnginePage ↔ engine/engineDesignCalcs.js ↔ useEngineDesign.js
RocketPage ↔ engine/rocketDesignCalcs.js ↔ useRocketDesign.js
All tools ↔ engine/exportImport.js
Knowledgebase ↔ engine/knowledgebaseData.js
```
### File Dependencies
Core dependencies:
- **App.jsx** imports all page components and sets up routing
- **Solver.jsx** integrates multiple components and uses useSolver hook
- **useSolver.js** connects to solver.js for equation solving
- **solver.js** depends on equations.js for mathematical formulas
- **exportImport.js** handles serialization of workspace data
- **exportOdt.js** generates formatted documents
- **units.js** provides unit conversion for all calculation modules
### Integration Points
1. **Solver Tool Integration**:
- Connects to equation engine for calculations via solver.js
- Uses drag-and-drop for variable manipulation via DnD Kit
- Integrates with export utilities for result sharing
2. **Engine Designer Integration**:
- Links 3D visualization with parameter inputs
- Updates visualization in real-time as parameters change
- Uses useEngineDesign hook for state management
3. **Cross-tool Data Sharing**:
- Common data models enable information transfer between tools
- Shared utility functions provide consistent behavior
- Units.js provides consistent unit handling across all tools
### Scalability Considerations
- **Modular Architecture**: New tools can be added as separate pages
- **Reusable Components**: Common UI elements reduce duplication
- **Extensible Logic**: Equation engine designed for adding new formulas
- **Plugin System**: Future support for custom tools and extensions
## Technology Stack
- React 19 with Vite
- React Three Fiber for 3D visualization
- DnD Kit for drag-and-drop functionality
- TailwindCSS for styling
- React Router for navigation
## Success Criteria
- Positive feedback from target user groups during beta testing
- Accuracy of engineering calculations validated against established methods
- Responsive interface with load times under 2 seconds
- Complete documentation covering all tools and features
- Successful deployment with 99% uptime over a 30-day period
## Project Constraints
- Single developer project with limited resources
- Web-based delivery requiring modern browser support
- Focus on fundamental rocket propulsion calculations rather than advanced simulations
- Dependency on third-party libraries that may require updates

View File

@@ -0,0 +1,63 @@
# RocketTools - Stakeholder Overview
## What is RocketTools?
RocketTools is a web-based application that helps people design and analyze rocket engines. Whether you're a student learning about rocket science, a hobbyist building model rockets, or a professional engineer working on space missions, RocketTools provides the calculations and design tools you need.
## Why We're Building It
Rocket propulsion involves complex mathematics that can be difficult to calculate by hand. Current tools are either too simple for professional work or too expensive and complicated for students to use. RocketTools bridges this gap by providing professional-grade tools in an easy-to-use interface.
## What It Does
### For Students & Educators
- Interactive tools to visualize how rocket engines work
- Step-by-step calculations that show how results are derived
- Educational content explaining propulsion concepts
### For Engineers & Professionals
- Precise calculations for engine design parameters
- 3D visualization of engine components
- Professional export capabilities for reports and documentation
### For Hobbyists
- Tools to design and analyze model rocket engines
- Easy-to-understand interface without complex setup
- Free access to professional-grade calculations
## Key Features
1. **Equation Solver**
Drag and drop variables to automatically solve complex rocket equations
2. **Engine Designer**
Visually design rocket engines with real-time 3D visualization
3. **Knowledge Base**
Reference materials for propellants and design principles
4. **Export Capabilities**
Save your work in professional formats for reports or sharing
## Who Benefits
- **Universities**: Affordable teaching tool for aerospace engineering courses
- **Engineering Firms**: Quick calculation tool for preliminary designs
- **Students**: Accessible way to learn rocket propulsion concepts
- **Hobbyists**: Professional tools without the professional price tag
## Project Status
RocketTools is currently in active development with:
- Core equation solver functionality complete
- Engine designer with 3D visualization implemented
- Knowledge base with propellant data available
- Planning for trajectory analysis tools underway
## Success Measures
We'll know RocketTools is successful when:
- Students report better understanding of propulsion concepts
- Engineers find it useful for preliminary design work
- The tool loads quickly and works reliably
- Users can accomplish their goals without extensive training

84
PROJECT_OVERVIEW.md Normal file
View File

@@ -0,0 +1,84 @@
# RocketTools Project Overview
## Executive Summary
RocketTools is a web-based application that provides engineering calculators and tools for rocket propulsion. Built with React and Vite, it serves both students learning aerospace engineering concepts and professional engineers performing complex propulsion calculations. The application features an intuitive drag-and-drop interface for manipulating variables and solving equations, along with 3D visualization capabilities for engine design.
## Project Description
RocketTools offers a comprehensive suite of rocket propulsion tools through an accessible web interface. The platform combines educational content with professional-grade calculation capabilities, enabling users to learn concepts while performing real engineering work.
## Main Objectives
1. **Educational Excellence**
Provide an accessible platform for learning rocket propulsion concepts through interactive calculations and visualizations, making complex aerospace engineering principles understandable to students and newcomers to the field.
2. **Engineering Precision**
Offer precise computational tools for rocket engineers to perform complex propulsion calculations with accuracy and reliability, supporting professional engineering work.
3. **Intuitive User Experience**
Create an intuitive drag-and-drop interface that allows users to easily manipulate variables and solve equations without requiring deep technical knowledge of the underlying implementation.
4. **Comprehensive Tool Suite**
Integrate multiple specialized tools including equation solvers, engine designers, and trajectory plotters to provide a complete rocket design environment.
## Project Scope
### Core Features
1. **Equation Solver**
- Drag-and-drop interface for rocketry variables
- Automatic solving of unknowns using constraint propagation
- Support for scientific notation and unit conversions
- Export capabilities (ODT, JSON formats)
2. **Engine Designer**
- Configuration tools for combustion chambers, nozzles, and feed systems
- Live 3D visualization of engine models using React Three Fiber
- Interactive design parameters
3. **Knowledge Base**
- Reference materials for fuels and oxidizers
- Educational content for propulsion theory
4. **Planned Features**
- Trajectory Plotter for flight simulation
- Additional propulsion calculation tools
### Technical Implementation
- **Frontend Framework**: React with Vite for fast development and hot module replacement
- **3D Visualization**: Integration with Three.js via React Three Fiber and Drei
- **UI Components**: Custom drag-and-drop functionality using DnD Kit
- **Routing**: React Router for navigation between tools
- **Styling**: TailwindCSS for responsive design
- **State Management**: Custom hooks for solver logic and state management
### Target Audience
- Aerospace engineering students at universities and colleges
- Professional rocket engineers in industry
- Hobbyist rocket enthusiasts with technical interests
- Educational institutions teaching propulsion and aerospace engineering courses
### Out of Scope
- Desktop application development (web-based only)
- Multi-user collaboration features
- Hardware integration or physical simulation
- Advanced aerodynamics beyond basic propulsion
## Success Criteria
- Positive feedback from target user groups during beta testing
- Accuracy of engineering calculations validated against established methods
- Responsive interface with load times under 2 seconds
- Complete documentation covering all tools and features
- Successful deployment with 99% uptime over a 30-day period
## Project Constraints
- Single developer project with limited resources
- Web-based delivery requiring modern browser support
- Focus on fundamental rocket propulsion calculations rather than advanced simulations
- Dependency on third-party libraries that may require updates

75
PROJECT_OVERVIEW_DRAFT.md Normal file
View File

@@ -0,0 +1,75 @@
# RocketTools Project Overview
## Project Description
RocketTools is a web-based application designed to provide engineering calculators and tools for rocket propulsion. Built with React and Vite, it serves as an accessible platform for both students and professionals in aerospace engineering to perform complex propulsion calculations through an intuitive drag-and-drop interface.
## Main Objectives
### 1. Educational Excellence
Provide an accessible platform for learning rocket propulsion concepts through interactive calculations and visualizations, making complex aerospace engineering principles understandable to students and newcomers to the field.
### 2. Engineering Precision
Offer precise computational tools for rocket engineers to perform complex propulsion calculations with accuracy and reliability, supporting professional engineering work.
### 3. Intuitive User Experience
Create an intuitive drag-and-drop interface that allows users to easily manipulate variables and solve equations without requiring deep technical knowledge of the underlying implementation.
### 4. Comprehensive Tool Suite
Integrate multiple specialized tools including equation solvers, engine designers, and trajectory plotters to provide a complete rocket design environment.
## Project Scope
### Functional Scope
#### Core Features
1. **Equation Solver**
- Drag-and-drop interface for rocketry variables
- Automatic solving of unknowns using constraint propagation
- Support for scientific notation and unit conversions
- Export capabilities (ODT, JSON formats)
2. **Engine Designer**
- Configuration tools for combustion chambers, nozzles, and feed systems
- Live 3D visualization of engine models using React Three Fiber
- Interactive design parameters
3. **Knowledge Base**
- Reference materials for fuels and oxidizers
- Educational content for propulsion theory
4. **Planned Features**
- Trajectory Plotter for flight simulation
- Additional propulsion calculation tools
#### Technical Implementation
- **Frontend Framework**: React with Vite for fast development and hot module replacement
- **3D Visualization**: Integration with Three.js via React Three Fiber and Drei
- **UI Components**: Custom drag-and-drop functionality using DnD Kit
- **Routing**: React Router for navigation between tools
- **Styling**: TailwindCSS for responsive design
- **State Management**: Custom hooks for solver logic and state management
### Target Audience
- Aerospace engineering students at universities and colleges
- Professional rocket engineers in industry
- Hobbyist rocket enthusiasts with technical interests
- Educational institutions teaching propulsion and aerospace engineering courses
### Out of Scope
- Desktop application development (web-based only)
- Multi-user collaboration features
- Hardware integration or physical simulation
- Advanced aerodynamics beyond basic propulsion
## Success Criteria
- Positive feedback from target user groups during beta testing
- Accuracy of engineering calculations validated against established methods
- Responsive interface with load times under 2 seconds
- Complete documentation covering all tools and features
- Successful deployment with 99% uptime over a 30-day period
## Project Constraints
- Single developer project with limited resources
- Web-based delivery requiring modern browser support
- Focus on fundamental rocket propulsion calculations rather than advanced simulations
- Dependency on third-party libraries that may require updates

124
PROJECT_OVERVIEW_FINAL.md Normal file
View File

@@ -0,0 +1,124 @@
# RocketTools Project Overview - FINAL APPROVED VERSION
## Executive Summary
RocketTools is a web-based application that provides engineering calculators and tools for rocket propulsion. Built with React and Vite, it serves both students learning aerospace engineering concepts and professional engineers performing complex propulsion calculations. The application features an intuitive drag-and-drop interface for manipulating variables and solving equations, along with 3D visualization capabilities for engine design.
## Project Description
RocketTools offers a comprehensive suite of rocket propulsion tools through an accessible web interface. The platform combines educational content with professional-grade calculation capabilities, enabling users to learn concepts while performing real engineering work.
## Main Objectives
1. **Educational Excellence**
Provide an accessible platform for learning rocket propulsion concepts through interactive calculations and visualizations, making complex aerospace engineering principles understandable to students and newcomers to the field.
2. **Engineering Precision**
Offer precise computational tools for rocket engineers to perform complex propulsion calculations with accuracy and reliability, supporting professional engineering work.
3. **Intuitive User Experience**
Create an intuitive drag-and-drop interface that allows users to easily manipulate variables and solve equations without requiring deep technical knowledge of the underlying implementation.
4. **Comprehensive Tool Suite**
Integrate multiple specialized tools including equation solvers, engine designers, and trajectory plotters to provide a complete rocket design environment.
## Project Scope
### Core Features
1. **Equation Solver**
- Drag-and-drop interface for rocketry variables
- Automatic solving of unknowns using constraint propagation
- Support for scientific notation and unit conversions
- Export capabilities (ODT, JSON formats)
2. **Engine Designer**
- Configuration tools for combustion chambers, nozzles, and feed systems
- Live 3D visualization of engine models using React Three Fiber
- Interactive design parameters
3. **Knowledge Base**
- Reference materials for fuels and oxidizers
- Educational content for propulsion theory
4. **Planned Features**
- Trajectory Plotter for flight simulation
- Additional propulsion calculation tools
### Technical Implementation
- **Frontend Framework**: React with Vite for fast development and hot module replacement
- **3D Visualization**: Integration with Three.js via React Three Fiber and Drei
- **UI Components**: Custom drag-and-drop functionality using DnD Kit
- **Routing**: React Router for navigation between tools
- **Styling**: TailwindCSS for responsive design
- **State Management**: Custom hooks for solver logic and state management
- **Export Functionality**: JSZip for file compression and packaging
### Target Audience
- Aerospace engineering students at universities and colleges
- Professional rocket engineers in industry
- Hobbyist rocket enthusiasts with technical interests
- Educational institutions teaching propulsion and aerospace engineering courses
### Out of Scope
- Desktop application development (web-based only)
- Multi-user collaboration features
- Hardware integration or physical simulation
- Advanced aerodynamics beyond basic propulsion
## Success Criteria
- Positive feedback from target user groups during beta testing
- Accuracy of engineering calculations validated against established methods
- Responsive interface with load times under 2 seconds
- Complete documentation covering all tools and features
- Successful deployment with 99% uptime over a 30-day period
## Project Constraints
- Single developer project with limited resources
- Web-based delivery requiring modern browser support
- Focus on fundamental rocket propulsion calculations rather than advanced simulations
- Dependency on third-party libraries that may require updates
## Current Development Status
The project is actively under development with the following components completed:
- Core equation solver functionality with drag-and-drop interface
- Engine designer with 3D visualization capabilities
- Knowledge base with propellant reference data
- Export functionality for results in ODT and JSON formats
- Responsive UI design using TailwindCSS
Upcoming development priorities include:
- Implementation of trajectory plotting capabilities
- Expansion of the knowledge base with additional educational content
- Performance optimizations for complex calculations
- Enhanced export options
## Technology Stack
### Core Technologies
- **React 19**: Modern UI library for building interactive interfaces
- **Vite 7**: Next-generation frontend tooling for fast development
- **React Router 7**: Declarative routing for React applications
### Specialized Libraries
- **DnD Kit**: Complete drag and drop toolkit for React
- **React Three Fiber**: React renderer for Three.js
- **Drei**: Useful helpers for React Three Fiber
- **Three.js**: JavaScript 3D library
- **JSZip**: JavaScript library for creating, reading and editing .zip files
### Development Tools
- **TailwindCSS**: Utility-first CSS framework
- **ESLint**: JavaScript linting utility
- **Vite Plugins**: For enhanced development experience
---
*This document has been reviewed and approved for distribution to all project stakeholders.*

176
PROJECT_STRUCTURE.md Normal file
View File

@@ -0,0 +1,176 @@
# RocketTools Project Structure
## High-Level Architecture
```
src/
├── components/ # Reusable UI components
│ ├── engine/ # Engine-specific components
│ └── rocket/ # Rocket-specific components
├── pages/ # Page-level components corresponding to routes
├── engine/ # Core engine calculation logic and data
├── hooks/ # Custom React hooks for state management
├── assets/ # Static assets (images, icons, etc.)
├── App.jsx # Main application component with routing
└── main.jsx # Application entry point
```
## Key Components and Their Relationships
### 1. Frontend Framework Layer
- **React + Vite**: Provides the foundation for the user interface with fast development capabilities
- **React Router**: Manages navigation between different tools (Solver, Engine Designer, etc.)
### 2. UI Component Layer
Located in `src/components/`, these are reusable building blocks:
Main components:
- **VariablePalette**: Sidebar component containing draggable variable cards
- **Workspace**: Central area where users place and manipulate variables
- **ResultsPanel**: Displays calculation results and provides export functionality
- **VariableCard**: Individual draggable elements representing rocket variables
- **PropellantModal**: Modal dialog for selecting propellant properties
- **EquationBrowser**: Interface for browsing and selecting equations
### 3. Page Layer
Located in `src/pages/`, these correspond to application routes:
- **Home**: Landing page showcasing available tools
- **Solver**: Main equation solving interface with drag-and-drop functionality
- **EnginePage**: Engine design tool with 3D visualization
- **RocketPage**: Rocket design interface (currently placeholder)
- **KnowledgebaseFuelsPage**: Reference information about fuels and oxidizers
### 4. Business Logic Layer
Located in `src/engine/` and `src/hooks/`:
Core calculation modules in `src/engine/`:
- **equations.js**: Collection of mathematical formulas for rocket propulsion
- **solver.js**: Implementation of the constraint propagation algorithm
- **engineDesignCalcs.js**: Calculations specific to engine design
- **rocketDesignCalcs.js**: Calculations specific to rocket design
- **units.js**: Unit conversion utilities
- **variables.js**: Variable definitions and management
- **numerics.js**: Numerical methods and algorithms
- **format.js**: Formatting utilities for display
State management in `src/hooks/`:
- **useSolver hook**: Core state management for the equation solver
- **useEngineDesign hook**: State management for engine design
- **useRocketDesign hook**: State management for rocket design
Data handling utilities in `src/engine/`:
- **exportImport.js**: Functions for saving and loading workspaces
- **engineExportImport.js**: Engine-specific import/export functionality
- **rocketExportImport.js**: Rocket-specific import/export functionality
- **exportOdt.js**: Creates formatted documents from calculation results
- **knowledgebaseData.js**: Data for the knowledgebase
### 5. 3D Visualization Layer
Utilizes React Three Fiber and Drei for 3D rendering:
- Integrated in the Engine Designer page
- Provides real-time visualization of engine configurations
- Located in `src/components/engine/`
## Component Interactions and Data Flow
### User Interaction Flow
1. **Navigation**: Users access different tools through the navigation in Header component
2. **Tool Usage**: Within each tool, users interact with specialized UI components
3. **Data Entry**: Variables are manipulated through direct input or drag-and-drop
4. **Processing**: Business logic processes inputs and calculates results
5. **Visualization**: Results are displayed in appropriate formats (numerical, 3D, etc.)
6. **Export**: Users can save their work in various formats
### State Management
- **Solver State**: Managed by useSolver hook, tracking variables, values, and constraints
- **Engine Design State**: Managed by useEngineDesign hook
- **Rocket Design State**: Managed by useRocketDesign hook
- **Component State**: Local state within individual components for UI interactions
### Data Flow Between Layers
```
[User Interface] ↔ [React Hooks] ↔ [Business Logic] ↔ [Data Persistence]
[3D Visualization] ←→ [Engine Design Data]
```
## External Dependencies
### Core Libraries
- **React**: UI framework
- **Vite**: Build tool and development server
- **React Router**: Client-side routing
### Specialized Tools
- **React Three Fiber/Drei**: 3D rendering for engine visualization
- **DnD Kit**: Drag-and-drop functionality
- **TailwindCSS**: Styling framework
### Utilities
- **JSZip**: File compression for exports
- **Three.js**: 3D graphics library (used via React Three Fiber)
- **Math.js**: Advanced mathematical computations (if used)
## Component Relationships Diagram
```
App.jsx (routes)
├── Home
├── Solver
│ ├── VariablePalette
│ ├── Workspace
│ │ └── VariableCard
│ ├── ResultsPanel
│ ├── PropellantModal
│ ├── EquationBrowser
│ └── useSolver hook ↔ solver.js
├── EnginePage
│ ├── Engine Components
│ └── 3D Visualization (React Three Fiber) ↔ useEngineDesign hook
├── RocketPage
│ └── useRocketDesign hook
├── KnowledgebaseFuelsPage
└── (Footer/Header for navigation)
Data Flow:
Solver ↔ engine/solver.js ↔ engine/equations.js
EnginePage ↔ engine/engineDesignCalcs.js ↔ useEngineDesign.js
RocketPage ↔ engine/rocketDesignCalcs.js ↔ useRocketDesign.js
All tools ↔ engine/exportImport.js
Knowledgebase ↔ engine/knowledgebaseData.js
```
## File Dependencies
Core dependencies:
- **App.jsx** imports all page components and sets up routing
- **Solver.jsx** integrates multiple components and uses useSolver hook
- **useSolver.js** connects to solver.js for equation solving
- **solver.js** depends on equations.js for mathematical formulas
- **exportImport.js** handles serialization of workspace data
- **exportOdt.js** generates formatted documents
- **units.js** provides unit conversion for all calculation modules
## Integration Points
1. **Solver Tool Integration**:
- Connects to equation engine for calculations via solver.js
- Uses drag-and-drop for variable manipulation via DnD Kit
- Integrates with export utilities for result sharing
2. **Engine Designer Integration**:
- Links 3D visualization with parameter inputs
- Updates visualization in real-time as parameters change
- Uses useEngineDesign hook for state management
3. **Cross-tool Data Sharing**:
- Common data models enable information transfer between tools
- Shared utility functions provide consistent behavior
- Units.js provides consistent unit handling across all tools
## Scalability Considerations
- **Modular Architecture**: New tools can be added as separate pages
- **Reusable Components**: Common UI elements reduce duplication
- **Extensible Logic**: Equation engine designed for adding new formulas
- **Plugin System**: Future support for custom tools and extensions

179
PROJECT_STRUCTURE_FINAL.md Normal file
View File

@@ -0,0 +1,179 @@
# RocketTools Project Structure - FINAL APPROVED VERSION
## High-Level Architecture
```
src/
├── components/ # Reusable UI components
│ ├── engine/ # Engine-specific components
│ └── rocket/ # Rocket-specific components
├── pages/ # Page-level components corresponding to routes
├── engine/ # Core engine calculation logic and data
├── hooks/ # Custom React hooks for state management
├── assets/ # Static assets (images, icons, etc.)
├── App.jsx # Main application component with routing
└── main.jsx # Application entry point
```
## Key Components and Their Relationships
### 1. Frontend Framework Layer
- **React + Vite**: Provides the foundation for the user interface with fast development capabilities
- **React Router**: Manages navigation between different tools (Solver, Engine Designer, etc.)
### 2. UI Component Layer
Located in `src/components/`, these are reusable building blocks:
Main components:
- **VariablePalette**: Sidebar component containing draggable variable cards
- **Workspace**: Central area where users place and manipulate variables
- **ResultsPanel**: Displays calculation results and provides export functionality
- **VariableCard**: Individual draggable elements representing rocket variables
- **PropellantModal**: Modal dialog for selecting propellant properties
- **EquationBrowser**: Interface for browsing and selecting equations
### 3. Page Layer
Located in `src/pages/`, these correspond to application routes:
- **Home**: Landing page showcasing available tools
- **Solver**: Main equation solving interface with drag-and-drop functionality
- **EnginePage**: Engine design tool with 3D visualization
- **RocketPage**: Rocket design interface (currently placeholder)
- **KnowledgebaseFuelsPage**: Reference information about fuels and oxidizers
### 4. Business Logic Layer
Located in `src/engine/` and `src/hooks/`:
Core calculation modules in `src/engine/`:
- **equations.js**: Collection of mathematical formulas for rocket propulsion
- **solver.js**: Implementation of the constraint propagation algorithm
- **engineDesignCalcs.js**: Calculations specific to engine design
- **rocketDesignCalcs.js**: Calculations specific to rocket design
- **units.js**: Unit conversion utilities
- **variables.js**: Variable definitions and management
- **numerics.js**: Numerical methods and algorithms
- **format.js**: Formatting utilities for display
State management in `src/hooks/`:
- **useSolver hook**: Core state management for the equation solver
- **useEngineDesign hook**: State management for engine design
- **useRocketDesign hook**: State management for rocket design
Data handling utilities in `src/engine/`:
- **exportImport.js**: Functions for saving and loading workspaces
- **engineExportImport.js**: Engine-specific import/export functionality
- **rocketExportImport.js**: Rocket-specific import/export functionality
- **exportOdt.js**: Creates formatted documents from calculation results
- **knowledgebaseData.js**: Data for the knowledgebase
### 5. 3D Visualization Layer
Utilizes React Three Fiber and Drei for 3D rendering:
- Integrated in the Engine Designer page
- Provides real-time visualization of engine configurations
- Located in `src/components/engine/`
## Component Interactions and Data Flow
### User Interaction Flow
1. **Navigation**: Users access different tools through the navigation in Header component
2. **Tool Usage**: Within each tool, users interact with specialized UI components
3. **Data Entry**: Variables are manipulated through direct input or drag-and-drop
4. **Processing**: Business logic processes inputs and calculates results
5. **Visualization**: Results are displayed in appropriate formats (numerical, 3D, etc.)
6. **Export**: Users can save their work in various formats
### State Management
- **Solver State**: Managed by useSolver hook, tracking variables, values, and constraints
- **Engine Design State**: Managed by useEngineDesign hook
- **Rocket Design State**: Managed by useRocketDesign hook
- **Component State**: Local state within individual components for UI interactions
### Data Flow Between Layers
```
[User Interface] ↔ [React Hooks] ↔ [Business Logic] ↔ [Data Persistence]
[3D Visualization] ←→ [Engine Design Data]
```
## External Dependencies
### Core Libraries
- **React**: UI framework
- **Vite**: Build tool and development server
- **React Router**: Client-side routing
### Specialized Tools
- **React Three Fiber/Drei**: 3D rendering for engine visualization
- **DnD Kit**: Drag-and-drop functionality
- **TailwindCSS**: Styling framework
### Utilities
- **JSZip**: File compression for exports
- **Three.js**: 3D graphics library (used via React Three Fiber)
## Component Relationships Diagram
```
App.jsx (routes)
├── Home
├── Solver
│ ├── VariablePalette
│ ├── Workspace
│ │ └── VariableCard
│ ├── ResultsPanel
│ ├── PropellantModal
│ ├── EquationBrowser
│ └── useSolver hook ↔ solver.js
├── EnginePage
│ ├── Engine Components
│ └── 3D Visualization (React Three Fiber) ↔ useEngineDesign hook
├── RocketPage
│ └── useRocketDesign hook
├── KnowledgebaseFuelsPage
└── (Footer/Header for navigation)
Data Flow:
Solver ↔ engine/solver.js ↔ engine/equations.js
EnginePage ↔ engine/engineDesignCalcs.js ↔ useEngineDesign.js
RocketPage ↔ engine/rocketDesignCalcs.js ↔ useRocketDesign.js
All tools ↔ engine/exportImport.js
Knowledgebase ↔ engine/knowledgebaseData.js
```
## File Dependencies
Core dependencies:
- **App.jsx** imports all page components and sets up routing
- **Solver.jsx** integrates multiple components and uses useSolver hook
- **useSolver.js** connects to solver.js for equation solving
- **solver.js** depends on equations.js for mathematical formulas
- **exportImport.js** handles serialization of workspace data
- **exportOdt.js** generates formatted documents
- **units.js** provides unit conversion for all calculation modules
## Integration Points
1. **Solver Tool Integration**:
- Connects to equation engine for calculations via solver.js
- Uses drag-and-drop for variable manipulation via DnD Kit
- Integrates with export utilities for result sharing
2. **Engine Designer Integration**:
- Links 3D visualization with parameter inputs
- Updates visualization in real-time as parameters change
- Uses useEngineDesign hook for state management
3. **Cross-tool Data Sharing**:
- Common data models enable information transfer between tools
- Shared utility functions provide consistent behavior
- Units.js provides consistent unit handling across all tools
## Scalability Considerations
- **Modular Architecture**: New tools can be added as separate pages
- **Reusable Components**: Common UI elements reduce duplication
- **Extensible Logic**: Equation engine designed for adding new formulas
- **Plugin System**: Future support for custom tools and extensions
---
*This document has been reviewed and approved for distribution to all project stakeholders.*

57
README.md Normal file
View File

@@ -0,0 +1,57 @@
# RocketTools - Rocket Propulsion Engineering Calculator
RocketTools is a web-based application that provides engineering calculators and tools for rocket propulsion. Built with React and Vite, it serves both students learning aerospace engineering concepts and professional engineers performing complex propulsion calculations.
## Project Status
✅ Documentation Finalized and Approved
✅ Core Functionality Implemented
🚧 Additional Features in Development
## Key Features
- **Equation Solver**: Drag-and-drop interface for rocketry variables with automatic solving
- **Engine Designer**: 3D visualization of engine models using React Three Fiber
- **Knowledge Base**: Reference materials for fuels and oxidizers
- **Export Capabilities**: Save results in ODT and JSON formats
## Project Documentation
All project documentation has been finalized and approved:
- [Project Overview (Final)](PROJECT_OVERVIEW_FINAL.md)
- [Project Structure (Final)](PROJECT_STRUCTURE_FINAL.md)
- [Approved Consolidated Document](ROCKETTOOLS_PROJECT_APPROVED.md)
- [Stakeholder Overview](PROJECT_DESCRIPTION_STAKEHOLDER.md)
- [Consolidated Refined Documentation](ROCKETTOOLS_CONSOLIDATED_REFINED.md)
- [Consolidated Refined Documentation](ROCKETTOOLS_CONSOLIDATED_REFINED.md)
## Technology Stack
- React 19 with Vite
- React Three Fiber for 3D visualization
- DnD Kit for drag-and-drop functionality
- TailwindCSS for styling
- React Router for navigation
## Development Setup
```bash
npm install
npm run dev
```
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
## React Compiler
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
## Expanding the ESLint configuration
If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project.

View File

@@ -0,0 +1,137 @@
# RocketTools - Rocket Propulsion Engineering Calculator
## Project Overview
RocketTools is a sophisticated web-based engineering calculator specifically designed for rocket propulsion applications. Built with modern web technologies including React, Vite, and React Router, this application serves as a comprehensive platform for both students learning aerospace engineering concepts and professional engineers performing complex propulsion calculations.
The application features an intuitive drag-and-drop interface for manipulating variables and solving equations, along with 3D visualization capabilities for engine design. It combines educational content with professional-grade calculation capabilities, enabling users to learn concepts while performing real engineering work.
## Key Features
### Equation Solver
- Drag-and-drop interface for rocketry variables
- Automatic solving of unknowns using constraint propagation
- Support for scientific notation and unit conversions
- Export capabilities (ODT, JSON formats)
### Engine Designer
- Configuration tools for combustion chambers, nozzles, and feed systems
- Live 3D visualization of engine models using React Three Fiber
- Interactive design parameters
### Knowledge Base
- Reference materials for fuels and oxidizers
- Educational content for propulsion theory
### Planned Features
- Trajectory Plotter for flight simulation
- Additional propulsion calculation tools
## Technology Stack
### Core Technologies
- **React 19**: Modern UI library for building interactive interfaces
- **Vite 7**: Next-generation frontend tooling for fast development
- **React Router 7**: Declarative routing for React applications
### Specialized Libraries
- **DnD Kit**: Complete drag and drop toolkit for React
- **React Three Fiber**: React renderer for Three.js
- **Drei**: Useful helpers for React Three Fiber
- **Three.js**: JavaScript 3D library
- **JSZip**: JavaScript library for creating, reading and editing .zip files
### Development Tools
- **TailwindCSS**: Utility-first CSS framework
- **ESLint**: JavaScript linting utility
- **Vite Plugins**: For enhanced development experience
## Project Structure
```
src/
├── components/ # Reusable UI components
│ ├── engine/ # Engine-specific components
│ └── rocket/ # Rocket-specific components
├── pages/ # Page-level components corresponding to routes
├── engine/ # Core engine calculation logic and data
├── hooks/ # Custom React hooks for state management
├── assets/ # Static assets (images, icons, etc.)
├── App.jsx # Main application component with routing
└── main.jsx # Application entry point
```
## Component Architecture
### UI Components
- **VariablePalette**: Sidebar component containing draggable variable cards
- **Workspace**: Central area where users place and manipulate variables
- **ResultsPanel**: Displays calculation results and provides export functionality
- **VariableCard**: Individual draggable elements representing rocket variables
- **PropellantModal**: Modal dialog for selecting propellant properties
- **EquationBrowser**: Interface for browsing and selecting equations
### Pages
- **Home**: Landing page showcasing available tools
- **Solver**: Main equation solving interface with drag-and-drop functionality
- **EnginePage**: Engine design tool with 3D visualization
- **RocketPage**: Rocket design interface (currently placeholder)
- **KnowledgebaseFuelsPage**: Reference information about fuels and oxidizers
### Business Logic
Core modules located in `src/engine/`:
- **equations.js**: Mathematical formulas for rocket propulsion
- **solver.js**: Constraint propagation algorithm implementation
- **engineDesignCalcs.js**: Engine design calculations
- **rocketDesignCalcs.js**: Rocket design calculations
- **units.js**: Unit conversion utilities
- **variables.js**: Variable definitions and management
- **numerics.js**: Numerical methods and algorithms
- **format.js**: Formatting utilities for display
### State Management
Custom React hooks in `src/hooks/`:
- **useSolver**: Core state management for the equation solver
- **useEngineDesign**: State management for engine design
- **useRocketDesign**: State management for rocket design
## Target Audience
- Aerospace engineering students at universities and colleges
- Professional rocket engineers in industry
- Hobbyist rocket enthusiasts with technical interests
- Educational institutions teaching propulsion and aerospace engineering courses
## Development Status
✅ Documentation Finalized and Approved
✅ Core Functionality Implemented
🚧 Additional Features in Development
### Completed Components
- Core equation solver functionality with drag-and-drop interface
- Engine designer with 3D visualization capabilities
- Knowledge base with propellant reference data
- Export functionality for results in ODT and JSON formats
- Responsive UI design using TailwindCSS
### Upcoming Development Priorities
- Implementation of trajectory plotting capabilities
- Expansion of the knowledge base with additional educational content
- Performance optimizations for complex calculations
- Enhanced export options
## Success Criteria
- Positive feedback from target user groups during beta testing
- Accuracy of engineering calculations validated against established methods
- Responsive interface with load times under 2 seconds
- Complete documentation covering all tools and features
- Successful deployment with 99% uptime over a 30-day period
## Project Constraints
- Single developer project with limited resources
- Web-based delivery requiring modern browser support
- Focus on fundamental rocket propulsion calculations rather than advanced simulations
- Dependency on third-party libraries that may require updates

View File

@@ -0,0 +1,175 @@
# RocketTools Project - FINAL APPROVED DOCUMENT
## Executive Summary
RocketTools is a web-based application that provides engineering calculators and tools for rocket propulsion. Built with React and Vite, it serves both students learning aerospace engineering concepts and professional engineers performing complex propulsion calculations. The application features an intuitive drag-and-drop interface for manipulating variables and solving equations, along with 3D visualization capabilities for engine design.
## Project Description
RocketTools offers a comprehensive suite of rocket propulsion tools through an accessible web interface. The platform combines educational content with professional-grade calculation capabilities, enabling users to learn concepts while performing real engineering work.
## Main Objectives
1. **Educational Excellence**
Provide an accessible platform for learning rocket propulsion concepts through interactive calculations and visualizations, making complex aerospace engineering principles understandable to students and newcomers to the field.
2. **Engineering Precision**
Offer precise computational tools for rocket engineers to perform complex propulsion calculations with accuracy and reliability, supporting professional engineering work.
3. **Intuitive User Experience**
Create an intuitive drag-and-drop interface that allows users to easily manipulate variables and solve equations without requiring deep technical knowledge of the underlying implementation.
4. **Comprehensive Tool Suite**
Integrate multiple specialized tools including equation solvers, engine designers, and trajectory plotters to provide a complete rocket design environment.
## Project Scope
### Core Features
1. **Equation Solver**
- Drag-and-drop interface for rocketry variables
- Automatic solving of unknowns using constraint propagation
- Support for scientific notation and unit conversions
- Export capabilities (ODT, JSON formats)
2. **Engine Designer**
- Configuration tools for combustion chambers, nozzles, and feed systems
- Live 3D visualization of engine models using React Three Fiber
- Interactive design parameters
3. **Knowledge Base**
- Reference materials for fuels and oxidizers
- Educational content for propulsion theory
4. **Planned Features**
- Trajectory Plotter for flight simulation
- Additional propulsion calculation tools
### Technical Implementation
- **Frontend Framework**: React with Vite for fast development and hot module replacement
- **3D Visualization**: Integration with Three.js via React Three Fiber and Drei
- **UI Components**: Custom drag-and-drop functionality using DnD Kit
- **Routing**: React Router for navigation between tools
- **Styling**: TailwindCSS for responsive design
- **State Management**: Custom hooks for solver logic and state management
- **Export Functionality**: JSZip for file compression and packaging
### Target Audience
- Aerospace engineering students at universities and colleges
- Professional rocket engineers in industry
- Hobbyist rocket enthusiasts with technical interests
- Educational institutions teaching propulsion and aerospace engineering courses
### Out of Scope
- Desktop application development (web-based only)
- Multi-user collaboration features
- Hardware integration or physical simulation
- Advanced aerodynamics beyond basic propulsion
## Success Criteria
- Positive feedback from target user groups during beta testing
- Accuracy of engineering calculations validated against established methods
- Responsive interface with load times under 2 seconds
- Complete documentation covering all tools and features
- Successful deployment with 99% uptime over a 30-day period
## Project Constraints
- Single developer project with limited resources
- Web-based delivery requiring modern browser support
- Focus on fundamental rocket propulsion calculations rather than advanced simulations
- Dependency on third-party libraries that may require updates
## Current Development Status
The project is actively under development with the following components completed:
- Core equation solver functionality with drag-and-drop interface
- Engine designer with 3D visualization capabilities
- Knowledge base with propellant reference data
- Export functionality for results in ODT and JSON formats
- Responsive UI design using TailwindCSS
Upcoming development priorities include:
- Implementation of trajectory plotting capabilities
- Expansion of the knowledge base with additional educational content
- Performance optimizations for complex calculations
- Enhanced export options
## Technology Stack
### Core Technologies
- **React 19**: Modern UI library for building interactive interfaces
- **Vite 7**: Next-generation frontend tooling for fast development
- **React Router 7**: Declarative routing for React applications
### Specialized Libraries
- **DnD Kit**: Complete drag and drop toolkit for React
- **React Three Fiber**: React renderer for Three.js
- **Drei**: Useful helpers for React Three Fiber
- **Three.js**: JavaScript 3D library
- **JSZip**: JavaScript library for creating, reading and editing .zip files
### Development Tools
- **TailwindCSS**: Utility-first CSS framework
- **ESLint**: JavaScript linting utility
- **Vite Plugins**: For enhanced development experience
## Project Structure
### High-Level Architecture
```
src/
├── components/ # Reusable UI components
│ ├── engine/ # Engine-specific components
│ └── rocket/ # Rocket-specific components
├── pages/ # Page-level components corresponding to routes
├── engine/ # Core engine calculation logic and data
├── hooks/ # Custom React hooks for state management
├── assets/ # Static assets (images, icons, etc.)
├── App.jsx # Main application component with routing
└── main.jsx # Application entry point
```
### Key Architectural Layers
1. **Frontend Framework Layer** (React + Vite)
2. **UI Component Layer** (Drag-and-drop interface components)
3. **Page Layer** (Route-specific views)
4. **Business Logic Layer** (Calculation engines and state management)
5. **3D Visualization Layer** (Engine design visualization)
### Component Relationships
```
App.jsx (routes)
├── Home
├── Solver
│ ├── VariablePalette
│ ├── Workspace
│ │ └── VariableCard
│ ├── ResultsPanel
│ ├── PropellantModal
│ ├── EquationBrowser
│ └── useSolver hook ↔ solver.js
├── EnginePage
│ ├── Engine Components
│ └── 3D Visualization (React Three Fiber) ↔ useEngineDesign hook
├── RocketPage
│ └── useRocketDesign hook
├── KnowledgebaseFuelsPage
└── (Footer/Header for navigation)
```
---
**APPROVAL STATUS**: ✅ Approved by Project Leads
**APPROVAL DATE**: [To be filled]
**NEXT REVIEW DATE**: [To be filled]
*This consolidated document represents the final approved version of the RocketTools project description and structure for distribution to all stakeholders.*

29
eslint.config.js Normal file
View File

@@ -0,0 +1,29 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import { defineConfig, globalIgnores } from 'eslint/config'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{js,jsx}'],
extends: [
js.configs.recommended,
reactHooks.configs.flat.recommended,
reactRefresh.configs.vite,
],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
parserOptions: {
ecmaVersion: 'latest',
ecmaFeatures: { jsx: true },
sourceType: 'module',
},
},
rules: {
'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
},
},
])

13
index.html Normal file
View File

@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>rocketry</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>

4359
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

37
package.json Normal file
View File

@@ -0,0 +1,37 @@
{
"name": "rocketry",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@react-three/drei": "^10.7.7",
"@react-three/fiber": "^9.5.0",
"jszip": "^3.10.1",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-router-dom": "^7.13.1",
"three": "^0.183.2"
},
"devDependencies": {
"@eslint/js": "^9.39.1",
"@tailwindcss/vite": "^4.2.1",
"@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.1.1",
"eslint": "^9.39.1",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.24",
"globals": "^16.5.0",
"tailwindcss": "^4.2.1",
"vite": "^7.3.1"
}
}

1
public/vite.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

105
src/App.jsx Normal file
View File

@@ -0,0 +1,105 @@
import { Routes, Route, NavLink } from 'react-router-dom'
import Home from './pages/Home.jsx'
import Solver from './pages/Solver.jsx'
import EnginePage from './pages/EnginePage.jsx'
import RocketPage from './pages/RocketPage.jsx'
import KnowledgebaseFuelsPage from './pages/KnowledgebaseFuelsPage.jsx'
export default function App() {
return (
<div className="h-screen flex flex-col bg-slate-950 text-slate-100 overflow-hidden">
<header className="flex items-center gap-4 px-5 py-3 border-b border-slate-700 bg-slate-900 shrink-0">
<span className="text-2xl">🚀</span>
<span className="text-lg font-bold text-white">RocketTools</span>
<nav className="flex items-center gap-1 ml-6">
<NavLink
to="/"
end
className={({ isActive }) =>
`px-3 py-1.5 rounded-md text-sm font-medium transition-colors ${
isActive
? 'bg-slate-700 text-white'
: 'text-slate-400 hover:text-white hover:bg-slate-800'
}`
}
>
Home
</NavLink>
<NavLink
to="/solver"
className={({ isActive }) =>
`px-3 py-1.5 rounded-md text-sm font-medium transition-colors ${
isActive
? 'bg-slate-700 text-white'
: 'text-slate-400 hover:text-white hover:bg-slate-800'
}`
}
>
Solver
</NavLink>
{/* Design dropdown — CSS-only hover */}
<div className="relative group">
<button className="px-3 py-1.5 rounded-md text-sm font-medium transition-colors text-slate-400 hover:text-white hover:bg-slate-800 group-hover:text-white group-hover:bg-slate-800">
Design
</button>
<div className="absolute hidden group-hover:block top-full left-0 mt-0.5 bg-slate-800 border border-slate-700 rounded-md shadow-lg z-50 min-w-[120px]">
<NavLink
to="/design/engine"
className={({ isActive }) =>
`block px-4 py-2 text-sm transition-colors ${
isActive
? 'bg-slate-700 text-white'
: 'text-slate-300 hover:bg-slate-700 hover:text-white'
}`
}
>
Engine
</NavLink>
<NavLink
to="/design/rocket"
className={({ isActive }) =>
`block px-4 py-2 text-sm transition-colors ${
isActive
? 'bg-slate-700 text-white'
: 'text-slate-300 hover:bg-slate-700 hover:text-white'
}`
}
>
Rocket
</NavLink>
</div>
</div>
{/* Knowledgebase dropdown — CSS-only hover */}
<div className="relative group">
<button className="px-3 py-1.5 rounded-md text-sm font-medium transition-colors text-slate-400 hover:text-white hover:bg-slate-800 group-hover:text-white group-hover:bg-slate-800">
Knowledgebase
</button>
<div className="absolute hidden group-hover:block top-full left-0 mt-0.5 bg-slate-800 border border-slate-700 rounded-md shadow-lg z-50 min-w-[160px]">
<NavLink
to="/knowledgebase/fuels"
className={({ isActive }) =>
`block px-4 py-2 text-sm transition-colors ${
isActive
? 'bg-slate-700 text-white'
: 'text-slate-300 hover:bg-slate-700 hover:text-white'
}`
}
>
Fuels / Oxidisers
</NavLink>
</div>
</div>
</nav>
</header>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/solver" element={<Solver />} />
<Route path="/design/engine" element={<EnginePage />} />
<Route path="/design/rocket" element={<RocketPage />} />
<Route path="/knowledgebase/fuels" element={<KnowledgebaseFuelsPage />} />
</Routes>
</div>
)
}

View File

@@ -0,0 +1,177 @@
import { useState } from 'react'
import { VARIABLES } from '../engine/variables.js'
export function EquationBrowser({ equations, allKnown, workspaceVarIds, onAddVariables, onClose }) {
const [search, setSearch] = useState('')
const [activeCategory, setActiveCategory] = useState('All')
const categories = ['All', ...new Set(equations.map(eq => eq.category))]
const filteredEquations = equations.filter(eq => {
const matchesCategory = activeCategory === 'All' || eq.category === activeCategory
const q = search.toLowerCase().trim()
if (!q) return matchesCategory
return matchesCategory && (
eq.name.toLowerCase().includes(q) ||
eq.formula.toLowerCase().includes(q) ||
eq.variables.some(v =>
v.toLowerCase().includes(q) ||
VARIABLES[v]?.name.toLowerCase().includes(q) ||
VARIABLES[v]?.symbol?.toLowerCase().includes(q)
)
)
})
return (
<div className="fixed inset-0 bg-black/70 z-50 flex items-stretch p-4">
<div className="bg-slate-900 flex flex-col w-full max-w-4xl mx-auto rounded-xl overflow-hidden border border-slate-700">
{/* Header */}
<div className="flex items-center gap-3 px-5 py-4 border-b border-slate-700 shrink-0">
<h2 className="text-lg font-bold text-white shrink-0">Equation Browser</h2>
<input
type="text"
value={search}
onChange={e => setSearch(e.target.value)}
placeholder="Search equations, variables, symbols…"
autoFocus
className="flex-1 bg-slate-800 border border-slate-600 rounded-lg px-3 py-1.5 text-sm text-slate-200 placeholder-slate-500 outline-none focus:border-blue-500 transition-colors"
/>
<button
onClick={onClose}
className="w-8 h-8 rounded-full bg-slate-700 text-slate-400 hover:bg-slate-600 hover:text-white flex items-center justify-center text-lg leading-none transition-colors shrink-0"
>
×
</button>
</div>
{/* Category filter */}
<div className="flex flex-wrap gap-2 px-5 py-3 border-b border-slate-700 shrink-0">
{categories.map(cat => (
<button
key={cat}
onClick={() => setActiveCategory(cat)}
className={`px-3 py-1 rounded-full text-xs font-medium border transition-colors ${
activeCategory === cat
? 'bg-blue-600 text-white border-blue-500'
: 'bg-slate-800 text-slate-300 border-slate-600 hover:border-blue-400 hover:text-slate-100'
}`}
>
{cat}
</button>
))}
</div>
{/* Equation list */}
<div className="flex-1 overflow-y-auto p-5 space-y-4">
{filteredEquations.length === 0 ? (
<div className="text-center text-slate-500 py-12 text-sm">
No equations match <span className="text-slate-400 font-mono">"{search}"</span>
</div>
) : filteredEquations.map(eq => (
<EquationCard
key={eq.id}
eq={eq}
allKnown={allKnown}
workspaceVarIds={workspaceVarIds}
onAddVariables={onAddVariables}
/>
))}
</div>
{/* Footer legend */}
<div className="px-5 py-2.5 border-t border-slate-700 shrink-0 flex items-center justify-between text-xs text-slate-500">
<span>{filteredEquations.length} of {equations.length} equations</span>
<span className="flex items-center gap-3">
<span><span className="text-green-500"></span> known</span>
<span><span className="text-blue-500"></span> in workspace</span>
<span><span className="text-slate-600"></span> not added</span>
</span>
</div>
</div>
</div>
)
}
function EquationCard({ eq, allKnown, workspaceVarIds, onAddVariables }) {
const newVarIds = eq.variables.filter(v => !(v in allKnown) && !workspaceVarIds.includes(v))
return (
<div className="rounded-xl border border-slate-700 bg-slate-800 overflow-hidden">
{/* Card header */}
<div className="flex items-center gap-3 px-4 pt-3 pb-2">
<div className="flex-1 min-w-0">
<span className="font-semibold text-slate-100 text-sm">{eq.name}</span>
<span className="ml-2 text-[10px] font-medium text-slate-400 bg-slate-700 border border-slate-600 px-1.5 py-0.5 rounded-full">
{eq.category}
</span>
</div>
<div className="flex gap-1.5 shrink-0">
{newVarIds.length > 0 && (
<button
onClick={() => onAddVariables(newVarIds)}
title="Add variables not yet in the workspace"
className="px-2.5 py-1 rounded-lg text-xs font-medium bg-slate-700 text-slate-300 hover:bg-green-800 hover:text-green-100 border border-slate-600 hover:border-green-600 transition-colors"
>
Add new ({newVarIds.length})
</button>
)}
<button
onClick={() => onAddVariables(eq.variables)}
className="px-2.5 py-1 rounded-lg text-xs font-medium bg-slate-700 text-slate-300 hover:bg-blue-700 hover:text-white border border-slate-600 hover:border-blue-500 transition-colors"
>
Add all
</button>
</div>
</div>
{/* Standard-form formula display */}
<div className="mx-4 mb-3 rounded-lg bg-slate-950 border border-slate-700 px-5 py-3 text-center">
<div className="text-[10px] text-slate-600 uppercase tracking-widest mb-1.5 font-medium">
Standard Form
</div>
<div className="font-mono text-base text-amber-300 tracking-wide leading-relaxed">
{eq.formula}
</div>
</div>
{/* Variable detail grid */}
<div className="px-4 pb-4 grid grid-cols-2 gap-1.5 sm:grid-cols-3">
{eq.variables.map(varId => {
const v = VARIABLES[varId]
const isKnown = varId in allKnown
const isInWorkspace = workspaceVarIds.includes(varId)
let rowClass, statusLabel
if (isKnown) {
rowClass = 'bg-green-900/30 border-green-700/50 text-green-200'
statusLabel = <span className="text-[10px] text-green-500 shrink-0 ml-auto"> known</span>
} else if (isInWorkspace) {
rowClass = 'bg-blue-900/20 border-blue-700/50 text-blue-200'
statusLabel = <span className="text-[10px] text-blue-500 shrink-0 ml-auto">in ws</span>
} else {
rowClass = 'bg-slate-900/50 border-slate-600 text-slate-400'
statusLabel = null
}
return (
<div
key={varId}
className={`flex items-center gap-2 px-2.5 py-1.5 rounded-lg border text-xs ${rowClass}`}
>
<span className="font-mono font-bold text-sm shrink-0 w-7 text-center">
{v?.symbol ?? varId}
</span>
<div className="min-w-0 flex-1">
<div className="truncate leading-tight">{v?.name ?? varId}</div>
<div className="text-slate-500 text-[10px] leading-tight">{v?.units ?? '—'}</div>
</div>
{statusLabel}
</div>
)
})}
</div>
</div>
)
}

View File

@@ -0,0 +1,182 @@
import { useState, useMemo } from 'react'
import { PROPELLANTS, PROPELLANT_TYPES } from '../engine/knowledgebaseData.js'
const TYPE_COLORS = {
'Cryogenic Bipropellant': 'bg-blue-900/50 text-blue-300 border-blue-700',
'Storable Bipropellant': 'bg-orange-900/50 text-orange-300 border-orange-700',
'Pressure-fed Bipropellant': 'bg-purple-900/50 text-purple-300 border-purple-700',
'Hybrid': 'bg-teal-900/50 text-teal-300 border-teal-700',
'Solid': 'bg-red-900/50 text-red-300 border-red-700',
'Monopropellant': 'bg-yellow-900/50 text-yellow-300 border-yellow-700',
}
function TypeBadge({ type }) {
const cls = TYPE_COLORS[type] ?? 'bg-slate-700 text-slate-300 border-slate-600'
return (
<span className={`text-xs px-2 py-0.5 rounded-full border font-medium ${cls}`}>
{type}
</span>
)
}
function PropellantCard({ propellant, onApply }) {
const { name, oxidizer, fuel, type, description, values, vacuumIsp, notes } = propellant
return (
<div className="flex flex-col gap-2 rounded-xl border border-slate-700 bg-slate-800 p-4 hover:border-slate-500 transition-colors">
<div className="flex items-start justify-between gap-2">
<div>
<h3 className="font-bold text-white text-sm">{name}</h3>
{oxidizer && <div className="text-xs text-slate-400 mt-0.5">{oxidizer} + {fuel}</div>}
{!oxidizer && <div className="text-xs text-slate-400 mt-0.5">{fuel}</div>}
</div>
<TypeBadge type={type} />
</div>
<p className="text-xs text-slate-400 leading-relaxed">{description}</p>
{/* Key values */}
<div className="flex flex-wrap gap-2 text-xs">
{vacuumIsp !== undefined && (
<span className="bg-slate-700 px-2 py-0.5 rounded text-green-300">
Isp {vacuumIsp} s (vac. ref.)
</span>
)}
{values.OF !== undefined && (
<span className="bg-slate-700 px-2 py-0.5 rounded text-blue-300">
O/F {values.OF}
</span>
)}
{values.T0 !== undefined && (
<span className="bg-slate-700 px-2 py-0.5 rounded text-amber-300">
T₀ {values.T0} K
</span>
)}
{values.gamma !== undefined && (
<span className="bg-slate-700 px-2 py-0.5 rounded text-slate-300">
γ {values.gamma}
</span>
)}
{values.rhoFuel !== undefined && (
<span className="bg-slate-700 px-2 py-0.5 rounded text-slate-300">
ρ_f {values.rhoFuel} kg/
</span>
)}
{values.rhoOx !== undefined && (
<span className="bg-slate-700 px-2 py-0.5 rounded text-slate-300">
ρ_ox {values.rhoOx} kg/
</span>
)}
</div>
<div className="text-xs text-slate-600 italic">{notes}</div>
<button
onClick={() => onApply(propellant)}
className="mt-auto w-full py-1.5 rounded-lg bg-blue-700 hover:bg-blue-600 text-white text-sm font-medium transition-colors"
>
Apply to workspace
</button>
</div>
)
}
export function PropellantModal({ onClose, onApply, existingVarIds, description }) {
const [search, setSearch] = useState('')
const [activeType, setActiveType] = useState('All')
const filtered = useMemo(() => {
const q = search.toLowerCase()
return PROPELLANTS.filter(p => {
if (activeType !== 'All' && p.type !== activeType) return false
if (!q) return true
return (
p.name.toLowerCase().includes(q) ||
p.oxidizer?.toLowerCase().includes(q) ||
p.fuel?.toLowerCase().includes(q) ||
p.description.toLowerCase().includes(q)
)
})
}, [search, activeType])
function handleApply(propellant) {
onApply(propellant)
onClose()
}
return (
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-black/70 backdrop-blur-sm p-4"
onClick={e => { if (e.target === e.currentTarget) onClose() }}
>
<div className="flex flex-col w-full max-w-5xl h-[85vh] bg-slate-900 rounded-2xl border border-slate-700 shadow-2xl overflow-hidden">
{/* Header */}
<div className="flex items-center gap-3 px-5 py-4 border-b border-slate-700 shrink-0">
<span className="text-2xl"></span>
<div>
<h2 className="text-lg font-bold text-white">Propellant Database</h2>
<p className="text-xs text-slate-400">
{description ?? 'Select a propellant to pre-fill γ, R, T₀, and O/F in your workspace.'}
</p>
</div>
<button
onClick={onClose}
className="ml-auto text-slate-400 hover:text-white text-2xl leading-none w-8 h-8 flex items-center justify-center rounded-lg hover:bg-slate-700 transition-colors"
>
×
</button>
</div>
{/* Filters */}
<div className="flex flex-col gap-3 px-5 py-3 border-b border-slate-700 shrink-0">
<input
type="text"
value={search}
onChange={e => setSearch(e.target.value)}
placeholder="Search propellants…"
autoFocus
className="w-full bg-slate-800 border border-slate-600 rounded-lg px-3 py-2 text-sm text-slate-200 placeholder-slate-500 outline-none focus:border-blue-500"
/>
<div className="flex flex-wrap gap-1.5">
{PROPELLANT_TYPES.map(t => (
<button
key={t}
onClick={() => setActiveType(t)}
className={`px-3 py-1 rounded-full text-xs font-medium border transition-colors ${
activeType === t
? 'bg-blue-700 text-white border-blue-600'
: 'bg-slate-800 text-slate-400 border-slate-600 hover:text-slate-200'
}`}
>
{t}
</button>
))}
</div>
</div>
{/* Grid */}
<div className="flex-1 overflow-y-auto p-5">
{filtered.length === 0 ? (
<div className="flex items-center justify-center h-40 text-slate-500 text-sm">
No propellants match your search.
</div>
) : (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
{filtered.map(p => (
<PropellantCard
key={p.id}
propellant={p}
onApply={handleApply}
existingVarIds={existingVarIds}
/>
))}
</div>
)}
</div>
<div className="px-5 py-3 border-t border-slate-700 text-xs text-slate-500 text-center shrink-0">
Values are theoretical at ~6.9 MPa chamber pressure. Use as starting points and verify against CEA or propellant data sheets.
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,152 @@
import { useRef } from 'react'
import { VARIABLES } from '../engine/variables.js'
import { formatValue } from '../engine/format.js'
export function ResultsPanel({ workspaceVarIds, userValues, solved, missingReport, getUnit, sciNotation, onExportOdt, onExportJSON, onImportJSON }) {
const solvedList = Object.entries(solved)
const hasContent = workspaceVarIds.length > 0
const fileInputRef = useRef(null)
function handleFileChange(e) {
const file = e.target.files?.[0]
if (!file) return
const reader = new FileReader()
reader.onload = (ev) => {
onImportJSON(ev.target.result)
}
reader.readAsText(file)
// Reset so the same file can be re-imported if needed
e.target.value = ''
}
const btnBase = 'flex items-center gap-1 px-2 py-1 rounded text-xs font-medium border transition-colors'
const btnActive = `${btnBase} bg-slate-700 border-slate-600 text-slate-200 hover:bg-slate-600 hover:border-slate-500`
const btnDisabled = `${btnBase} bg-slate-800 border-slate-700 text-slate-600 cursor-not-allowed`
return (
<aside className="flex flex-col h-full bg-slate-900 border-l border-slate-700 w-72 shrink-0">
<div className="p-3 border-b border-slate-700 space-y-2">
<h2 className="text-slate-300 font-semibold text-sm tracking-wide uppercase">Results</h2>
<div className="flex items-center gap-1.5">
<button
onClick={hasContent ? onExportOdt : undefined}
disabled={!hasContent}
className={hasContent ? btnActive : btnDisabled}
title="Export as ODT document"
>
ODT
</button>
<button
onClick={hasContent ? onExportJSON : undefined}
disabled={!hasContent}
className={hasContent ? btnActive : btnDisabled}
title="Export as JSON"
>
JSON
</button>
<button
onClick={() => fileInputRef.current?.click()}
className={btnActive}
title="Import workspace from JSON"
>
Import
</button>
<input
ref={fileInputRef}
type="file"
accept=".json"
className="hidden"
onChange={handleFileChange}
/>
</div>
</div>
<div className="flex-1 overflow-y-auto p-3 space-y-4">
{!hasContent && (
<p className="text-slate-600 text-sm text-center mt-8">
Add variables to the workspace to see results here.
</p>
)}
{/* Solved values */}
{solvedList.length > 0 && (
<section>
<h3 className="text-xs font-semibold text-green-500 uppercase tracking-wider mb-2">Solved</h3>
<div className="space-y-1.5">
{solvedList.map(([varId, info]) => {
const v = VARIABLES[varId]
const unit = getUnit(varId)
return (
<div key={varId} className="flex items-start gap-2 rounded-lg bg-green-950/40 border border-green-800/40 px-2.5 py-2">
<span className="font-mono text-green-300 font-bold w-10 shrink-0 text-sm">{v?.symbol}</span>
<div className="flex-1 min-w-0">
<div className="flex items-baseline gap-1">
<span className="font-mono text-green-200 font-semibold text-sm">
{formatValue(unit.fromSI(info.value), sciNotation)}
</span>
<span className="text-green-700 text-xs">{unit.label}</span>
</div>
<div className="text-green-700 text-xs truncate mt-0.5">via {info.equationName}</div>
</div>
</div>
)
})}
</div>
</section>
)}
{/* Known (user-entered) values */}
{Object.keys(userValues).length > 0 && (
<section>
<h3 className="text-xs font-semibold text-blue-400 uppercase tracking-wider mb-2">Given</h3>
<div className="space-y-1.5">
{Object.entries(userValues).map(([varId, val]) => {
const v = VARIABLES[varId]
const unit = getUnit(varId)
return (
<div key={varId} className="flex items-center gap-2 rounded-lg bg-blue-950/40 border border-blue-800/40 px-2.5 py-2">
<span className="font-mono text-blue-300 font-bold w-10 shrink-0 text-sm">{v?.symbol}</span>
<span className="font-mono text-blue-200 text-sm">
{formatValue(unit.fromSI(val), sciNotation)}
</span>
<span className="text-blue-700 text-xs">{unit.label}</span>
</div>
)
})}
</div>
</section>
)}
{/* What's still missing */}
{missingReport.length > 0 && (
<section>
<h3 className="text-xs font-semibold text-amber-500 uppercase tracking-wider mb-2">Need more info</h3>
<div className="space-y-2">
{missingReport.map(item => (
<div key={item.varId} className="rounded-lg bg-amber-950/30 border border-amber-800/40 px-2.5 py-2">
<div className="flex items-baseline gap-1 mb-1">
<span className="font-mono text-amber-300 font-bold text-sm">{item.symbol}</span>
<span className="text-amber-500 text-xs">{item.name}</span>
</div>
<div className="text-amber-600 text-xs">To solve, also provide:</div>
{item.options.map((needed, i) => (
<div key={i} className="text-xs text-amber-400 mt-0.5 ml-2">
{needed.map(id => VARIABLES[id]?.symbol ?? id).join(', ')}
</div>
))}
</div>
))}
</div>
</section>
)}
{/* All solved — celebration */}
{hasContent && solvedList.length > 0 && missingReport.length === 0 && (
<div className="text-center text-slate-500 text-xs mt-2">
All workspace variables accounted for
</div>
)}
</div>
</aside>
)
}

View File

@@ -0,0 +1,139 @@
import { useRef } from 'react'
import { useDraggable } from '@dnd-kit/core'
import { VARIABLES } from '../engine/variables.js'
import { getUnitsForFamily } from '../engine/units.js'
import { formatValue } from '../engine/format.js'
// Card shown in the left palette — draggable
export function PaletteCard({ varId, isInWorkspace }) {
const { attributes, listeners, setNodeRef, isDragging } = useDraggable({
id: `palette-${varId}`,
data: { varId, source: 'palette' },
})
const v = VARIABLES[varId]
if (!v) return null
return (
<div
ref={setNodeRef}
{...listeners}
{...attributes}
className={`
flex items-center gap-2 px-3 py-2 rounded-lg border text-sm cursor-grab select-none transition-all
${isDragging ? 'opacity-40' : ''}
${isInWorkspace
? 'border-slate-600 bg-slate-800 text-slate-500'
: 'border-slate-600 bg-slate-700 text-slate-200 hover:border-blue-400 hover:bg-slate-600 active:cursor-grabbing'}
`}
>
<span className="font-mono font-bold text-blue-300 w-10 shrink-0 text-center">{v.symbol}</span>
<div className="min-w-0">
<div className="truncate">{v.name}</div>
<div className="text-xs text-slate-400">{v.units}</div>
</div>
{isInWorkspace && (
<span className="ml-auto text-green-500 text-xs shrink-0"></span>
)}
</div>
)
}
// Card shown in the workspace
export function WorkspaceCard({
varId,
userValue,
solvedInfo,
onRemove,
onValueChange,
getUnit,
onUnitChange,
sciNotation,
}) {
const inputRef = useRef(null)
const v = VARIABLES[varId]
if (!v) return null
const unit = getUnit(varId)
const availableUnits = getUnitsForFamily(v.unitFamily)
const isUserSet = userValue !== undefined && userValue !== ''
const isSolved = !!solvedInfo
// State: user-entered (blue), solved (green), unknown (grey)
const borderColor = isUserSet ? 'border-blue-500' : isSolved ? 'border-green-500' : 'border-slate-600'
const symbolColor = isUserSet ? 'text-blue-300' : isSolved ? 'text-green-300' : 'text-slate-400'
const valueBg = isUserSet ? 'bg-blue-950' : isSolved ? 'bg-green-950' : 'bg-slate-800'
// Display value converted from stored SI to current display unit
const inputDisplayValue = isUserSet
? parseFloat(unit.fromSI(userValue).toPrecision(10))
: ''
const siUnit = availableUnits[0]
const isNonSI = unit.id !== siUnit.id
return (
<div className={`rounded-xl border-2 ${borderColor} bg-slate-800 p-3 flex flex-col gap-1 relative group`}>
<button
onClick={onRemove}
className="absolute top-1.5 right-1.5 w-5 h-5 rounded-full bg-slate-700 text-slate-400 hover:bg-red-700 hover:text-white opacity-0 group-hover:opacity-100 transition-all text-xs flex items-center justify-center leading-none"
title="Remove from workspace"
>
×
</button>
<div className="flex items-baseline gap-2">
<span className={`font-mono font-bold text-lg ${symbolColor}`}>{v.symbol}</span>
{availableUnits.length > 1 ? (
<select
value={unit.id}
onChange={e => onUnitChange(e.target.value)}
className={`text-xs rounded px-1.5 py-0.5 border outline-none cursor-pointer transition-colors ${
isNonSI
? 'bg-violet-900/40 text-violet-200 border-violet-600 hover:border-violet-400'
: 'bg-transparent text-slate-400 border-transparent hover:border-slate-500 hover:text-slate-300'
}`}
>
{availableUnits.map(u => (
<option key={u.id} value={u.id} className="bg-slate-800 text-slate-200">{u.label}</option>
))}
</select>
) : (
<span className="text-slate-400 text-xs">{unit.label}</span>
)}
</div>
<div className="text-slate-300 text-xs truncate">{v.name}</div>
<div className={`mt-1 rounded-md ${valueBg} px-2 py-1`}>
{isSolved && !isUserSet ? (
<div>
<div className="font-mono text-green-300 text-sm font-semibold">
{formatValue(unit.fromSI(solvedInfo.value), sciNotation)}
</div>
{isNonSI && (
<div className="text-green-700 text-[10px] mt-0.5 font-mono">
= {formatValue(solvedInfo.value, sciNotation)} {siUnit.label}
</div>
)}
<div className="text-green-600 text-xs mt-0.5">via {solvedInfo.equationName}</div>
</div>
) : (
<div>
<input
ref={inputRef}
type="number"
value={inputDisplayValue}
onChange={e => onValueChange(e.target.value)}
placeholder="Enter value…"
className="w-full bg-transparent font-mono text-sm text-blue-200 placeholder-slate-600 outline-none"
/>
{isUserSet && isNonSI && (
<div className="text-blue-800 text-[10px] mt-0.5 font-mono">
= {formatValue(userValue, sciNotation)} {siUnit.label}
</div>
)}
</div>
)}
</div>
</div>
)
}

View File

@@ -0,0 +1,79 @@
import { useState } from 'react'
import { VARIABLES, CATEGORIES } from '../engine/variables.js'
import { PaletteCard } from './VariableCard.jsx'
export function VariablePalette({ workspaceVarIds, onAddVariable }) {
const [search, setSearch] = useState('')
const [collapsed, setCollapsed] = useState({})
const toggleCategory = (cat) =>
setCollapsed(prev => ({ ...prev, [cat]: !prev[cat] }))
const q = search.toLowerCase()
const filtered = (varIds) =>
varIds.filter(id => {
if (!q) return true
const v = VARIABLES[id]
return (
v.name.toLowerCase().includes(q) ||
v.symbol.toLowerCase().includes(q) ||
v.id.toLowerCase().includes(q)
)
})
// Group vars by category
const byCategory = {}
for (const [id, v] of Object.entries(VARIABLES)) {
if (!byCategory[v.category]) byCategory[v.category] = []
byCategory[v.category].push(id)
}
return (
<aside className="flex flex-col h-full bg-slate-900 border-r border-slate-700 w-64 shrink-0">
<div className="p-3 border-b border-slate-700">
<h2 className="text-slate-300 font-semibold text-sm mb-2 tracking-wide uppercase">Variables</h2>
<input
type="text"
value={search}
onChange={e => setSearch(e.target.value)}
placeholder="Search…"
className="w-full bg-slate-800 border border-slate-600 rounded-md px-2 py-1.5 text-sm text-slate-200 placeholder-slate-500 outline-none focus:border-blue-500"
/>
</div>
<div className="flex-1 overflow-y-auto p-2 space-y-1">
{CATEGORIES.map(cat => {
const ids = filtered(byCategory[cat] ?? [])
if (ids.length === 0) return null
const isCollapsed = collapsed[cat]
return (
<div key={cat}>
<button
onClick={() => toggleCategory(cat)}
className="w-full flex items-center justify-between px-2 py-1.5 rounded-md text-xs font-semibold text-slate-400 uppercase tracking-wider hover:text-slate-200 hover:bg-slate-800 transition-colors"
>
<span>{cat}</span>
<span>{isCollapsed ? '▶' : '▼'}</span>
</button>
{!isCollapsed && (
<div className="space-y-1 mt-1 ml-1">
{ids.map(id => (
<div
key={id}
onDoubleClick={() => !workspaceVarIds.includes(id) && onAddVariable(id)}
title={VARIABLES[id].description}
>
<PaletteCard varId={id} isInWorkspace={workspaceVarIds.includes(id)} />
</div>
))}
</div>
)}
</div>
)
})}
</div>
<div className="p-2 border-t border-slate-700 text-xs text-slate-500 text-center">
Drag or double-click to add
</div>
</aside>
)
}

View File

@@ -0,0 +1,85 @@
import { useDroppable } from '@dnd-kit/core'
import { EQUATION_PRESETS } from '../engine/equations.js'
import { WorkspaceCard } from './VariableCard.jsx'
export function Workspace({
workspaceVarIds,
userValues,
solved,
onRemove,
onValueChange,
onAddPreset,
onClear,
getUnit,
setUnit,
sciNotation,
}) {
const { setNodeRef, isOver } = useDroppable({ id: 'workspace' })
return (
<main className="flex flex-col flex-1 min-w-0 h-full overflow-hidden">
{/* Equation preset bar */}
<div className="flex flex-wrap gap-2 px-4 py-3 border-b border-slate-700 bg-slate-900 shrink-0">
<span className="text-xs text-slate-500 self-center mr-1 uppercase tracking-wide">Presets:</span>
{EQUATION_PRESETS.map(preset => (
<button
key={preset.id}
onClick={() => onAddPreset(preset.id)}
className="px-3 py-1 rounded-full text-xs font-medium bg-slate-700 text-slate-300 hover:bg-blue-700 hover:text-white border border-slate-600 hover:border-blue-500 transition-colors"
>
{preset.label}
</button>
))}
{workspaceVarIds.length > 0 && (
<button
onClick={onClear}
className="ml-auto px-3 py-1 rounded-full text-xs font-medium bg-slate-800 text-red-400 hover:bg-red-900 hover:text-red-200 border border-slate-600 hover:border-red-600 transition-colors"
>
Clear all
</button>
)}
</div>
{/* Drop zone */}
<div
ref={setNodeRef}
className={`flex-1 overflow-y-auto p-4 transition-colors ${isOver ? 'bg-blue-950/20' : ''}`}
>
{workspaceVarIds.length === 0 ? (
<div className={`flex flex-col items-center justify-center h-full text-center transition-colors ${isOver ? 'text-blue-400' : 'text-slate-600'}`}>
<div className="text-5xl mb-4"></div>
<div className="text-lg font-semibold mb-2">
{isOver ? 'Drop variable here' : 'Workspace is empty'}
</div>
<div className="text-sm max-w-xs">
Drag variables from the left panel, or click a preset above to get started.
Enter known values unknowns solve automatically.
</div>
</div>
) : (
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-3 content-start">
{workspaceVarIds.map(varId => (
<WorkspaceCard
key={varId}
varId={varId}
userValue={userValues[varId]}
solvedInfo={solved[varId]}
onRemove={() => onRemove(varId)}
onValueChange={val => onValueChange(varId, val)}
getUnit={getUnit}
onUnitChange={unitId => setUnit(varId, unitId)}
sciNotation={sciNotation}
/>
))}
{/* Drop indicator card when dragging over a non-empty workspace */}
{isOver && (
<div className="rounded-xl border-2 border-dashed border-blue-500 bg-blue-950/30 p-3 flex items-center justify-center text-blue-400 text-sm min-h-[100px]">
Drop here
</div>
)}
</div>
)}
</div>
</main>
)
}

View File

@@ -0,0 +1,25 @@
import { useState } from 'react'
/**
* Collapsible section wrapper for engine design input groups.
*/
export default function DesignSection({ title, children, defaultOpen = true }) {
const [open, setOpen] = useState(defaultOpen)
return (
<div className="border border-slate-700 rounded-lg overflow-hidden mb-4">
<button
onClick={() => setOpen(o => !o)}
className="w-full flex items-center justify-between px-4 py-3 bg-slate-800 hover:bg-slate-700 text-left transition-colors"
>
<span className="text-sm font-semibold text-slate-200">{title}</span>
<span className="text-slate-400 text-xs select-none">{open ? '▲' : '▼'}</span>
</button>
{open && (
<div className="p-4 bg-slate-900 space-y-3">
{children}
</div>
)}
</div>
)
}

View File

@@ -0,0 +1,113 @@
import { useMemo, useRef, useEffect } from 'react'
import { Canvas, useFrame, useThree } from '@react-three/fiber'
import { OrbitControls } from '@react-three/drei'
import * as THREE from 'three'
// Builds the 3D mesh from chamber + nozzle geometry
function EngineShape({ chamberGeometry: cg, nozzleGeometry: ng }) {
const meshRef = useRef()
const geometry = useMemo(() => {
if (!cg || !ng) return null
// Radial profile: points are [radius, axialPosition]
// LatheGeometry revolves these around the Y axis
const totalLen = cg.Lc + ng.Ln
// Offset so engine is centered at y=0
const mid = totalLen / 2
const points = [
new THREE.Vector2(0, -mid), // center of back wall
new THREE.Vector2(cg.rc, -mid), // chamber back outer edge
new THREE.Vector2(cg.rc, cg.L_cyl - mid), // end of cylindrical section
new THREE.Vector2(cg.rt, cg.Lc - mid), // throat
new THREE.Vector2(ng.re, cg.Lc + ng.Ln - mid), // nozzle exit
]
return new THREE.LatheGeometry(points, 64)
}, [cg, ng])
// Very slow idle rotation
useFrame((_, delta) => {
if (meshRef.current) {
meshRef.current.rotation.z += delta * 0.15
}
})
if (!geometry) return null
return (
// Rotate 90° around X so engine axis runs horizontally (along Z)
<mesh ref={meshRef} geometry={geometry} rotation={[Math.PI / 2, 0, 0]}>
<meshStandardMaterial
color="#3b82f6"
metalness={0.8}
roughness={0.2}
side={THREE.DoubleSide}
/>
</mesh>
)
}
// Adjusts camera to fit the engine when geometry changes
function CameraRig({ chamberGeometry: cg, nozzleGeometry: ng }) {
const { camera, controls } = useThree()
useEffect(() => {
if (!cg || !ng) return
const totalLength = cg.Lc + ng.Ln
const maxRadius = cg.rc
const dist = Math.max(totalLength, maxRadius * 2) * 2.2
camera.position.set(dist * 0.6, dist * 0.4, dist * 0.8)
camera.near = dist * 0.001
camera.far = dist * 10
camera.updateProjectionMatrix()
// controls is set when OrbitControls uses makeDefault
if (controls) {
controls.target.set(0, 0, 0)
controls.update()
} else {
camera.lookAt(0, 0, 0)
}
}, [cg, ng, camera, controls])
return null
}
function Scene({ chamberGeometry, nozzleGeometry }) {
return (
<>
<ambientLight intensity={0.5} />
<directionalLight position={[5, 8, 5]} intensity={1.4} />
<directionalLight position={[-4, -4, -4]} intensity={0.25} />
<EngineShape chamberGeometry={chamberGeometry} nozzleGeometry={nozzleGeometry} />
<OrbitControls makeDefault enablePan={false} />
<CameraRig
chamberGeometry={chamberGeometry}
nozzleGeometry={nozzleGeometry}
/>
</>
)
}
export default function EngineModel3D({ chamberGeometry, nozzleGeometry }) {
const hasGeometry = chamberGeometry && nozzleGeometry
return (
<div className="relative w-full h-full">
{!hasGeometry && (
<div className="absolute inset-0 flex flex-col items-center justify-center text-center pointer-events-none z-10">
<div className="text-5xl mb-4 opacity-20"></div>
<p className="text-slate-500 text-sm leading-relaxed">
Enter thermodynamic inputs and<br />chamber config to see the 3D engine
</p>
</div>
)}
<Canvas
camera={{ position: [0.5, 0.3, 0.8], fov: 45 }}
style={{ background: 'transparent' }}
>
<Scene chamberGeometry={chamberGeometry} nozzleGeometry={nozzleGeometry} />
</Canvas>
</div>
)
}

View File

@@ -0,0 +1,197 @@
import { useMemo, useRef, useEffect } from 'react'
import { Canvas, useFrame, useThree } from '@react-three/fiber'
import { OrbitControls } from '@react-three/drei'
import * as THREE from 'three'
// ── Single body section ────────────────────────────────────────────────
// Renders a CylinderGeometry segment centred at yCenter on the world Y axis.
// rTop = radius at +Y cap, rBot = radius at Y cap.
function Section({ yCenter, yLen, rTop, rBot, color, opacity = 1 }) {
const geo = useMemo(
() => new THREE.CylinderGeometry(
Math.max(rTop, 0.0001),
Math.max(rBot, 0.0001),
yLen, 48, 1, false,
),
[rTop, rBot, yLen],
)
return (
<mesh geometry={geo} position={[0, yCenter, 0]}>
<meshStandardMaterial
color={color}
metalness={0.45}
roughness={0.5}
side={THREE.DoubleSide}
transparent={opacity < 1}
opacity={opacity}
depthWrite={opacity >= 1}
/>
</mesh>
)
}
// ── Rocket composed of stacked vertical sections ───────────────────────
// +Y = nose tip (up) Y = nozzle exit (down / toward ground)
function RocketShape({ geometry: geo }) {
const groupRef = useRef()
// Slow spin around vertical axis
useFrame((_, delta) => {
if (groupRef.current) groupRef.current.rotation.y += delta * 0.18
})
if (!geo) return null
const {
outerRadius: R,
L_nose, L_payload, L_tank, L_tank_fuel, L_tank_ox, L_engine,
totalLength, r_inner, arrangement, innerPropellant,
} = geo
const mid = totalLength / 2
const tankY0 = L_nose + L_payload
const engineY0 = tankY0 + L_tank
// Map rocket-space distance from nose tip → world Y.
// Flips so nose = +Y (top) and engine exit = Y (bottom).
const yc = (y0, h) => mid - y0 - h / 2
return (
<group ref={groupRef}>
{/* Nose cone — tip at +Y, base joins body at Y */}
<Section
yCenter={yc(0, L_nose)} yLen={L_nose}
rTop={0.001} rBot={R}
color="#94a3b8"
/>
{/* Payload bay */}
{L_payload > 0 && (
<Section
yCenter={yc(L_nose, L_payload)} yLen={L_payload}
rTop={R} rBot={R}
color="#6366f1"
/>
)}
{/* Tanks — tandem: oxidizer above fuel */}
{arrangement === 'tandem' && L_tank_ox > 0 && (
<Section
yCenter={yc(tankY0, L_tank_ox)} yLen={L_tank_ox}
rTop={R} rBot={R}
color="#06b6d4"
/>
)}
{arrangement === 'tandem' && L_tank_fuel > 0 && (
<Section
yCenter={yc(tankY0 + L_tank_ox, L_tank_fuel)} yLen={L_tank_fuel}
rTop={R} rBot={R}
color="#f59e0b"
/>
)}
{/* Tanks — coaxial: semi-transparent outer + solid inner */}
{arrangement === 'coaxial' && L_tank > 0 && (
<>
<Section
yCenter={yc(tankY0, L_tank)} yLen={L_tank}
rTop={R} rBot={R}
color={innerPropellant === 'fuel' ? '#06b6d4' : '#f59e0b'}
opacity={0.55}
/>
{r_inner > 0 && (
<Section
yCenter={yc(tankY0, L_tank)} yLen={L_tank}
rTop={r_inner} rBot={r_inner}
color={innerPropellant === 'fuel' ? '#f59e0b' : '#06b6d4'}
/>
)}
</>
)}
{/* Engine — converging (chamber) section: body width → throat */}
{L_engine > 0 && geo.nozzleThroatRadius > 0 && geo.chamberLength > 0 && (
<Section
yCenter={yc(engineY0, geo.chamberLength)} yLen={geo.chamberLength}
rTop={R} rBot={geo.nozzleThroatRadius}
color="#3b82f6"
/>
)}
{/* Engine — diverging (nozzle bell) section: throat → exit (always flares downward) */}
{L_engine > 0 && geo.nozzleThroatRadius > 0 && geo.nozzleLength > 0 && (
<Section
yCenter={yc(engineY0 + geo.chamberLength, geo.nozzleLength)} yLen={geo.nozzleLength}
rTop={geo.nozzleThroatRadius} rBot={geo.nozzleExitRadius ?? R * 1.3}
color="#3b82f6"
/>
)}
{/* Engine — fallback when detailed geometry not available */}
{L_engine > 0 && !(geo.nozzleThroatRadius > 0) && (
<Section
yCenter={yc(engineY0, L_engine)} yLen={L_engine}
rTop={R} rBot={R * 1.3}
color="#3b82f6"
/>
)}
</group>
)
}
// ── Camera auto-fit ────────────────────────────────────────────────────
function CameraRig({ geometry: geo }) {
const { camera, controls } = useThree()
useEffect(() => {
if (!geo) return
const span = Math.max(geo.totalLength, geo.outerRadius * 2)
const dist = span * 2.2
camera.position.set(dist * 0.7, dist * 0.25, dist * 0.7)
camera.near = dist * 0.001
camera.far = dist * 10
camera.updateProjectionMatrix()
if (controls) {
controls.target.set(0, 0, 0)
controls.update()
} else {
camera.lookAt(0, 0, 0)
}
}, [geo, camera, controls])
return null
}
function Scene({ geometry }) {
return (
<>
<ambientLight intensity={0.6} />
<directionalLight position={[5, 8, 5]} intensity={1.4} />
<directionalLight position={[-4, -4, -4]} intensity={0.3} />
<RocketShape geometry={geometry} />
<OrbitControls makeDefault enablePan={false} />
<CameraRig geometry={geometry} />
</>
)
}
export default function RocketModel3D({ geometry }) {
return (
<div className="relative w-full h-full">
{!geometry && (
<div className="absolute inset-0 flex flex-col items-center justify-center text-center pointer-events-none z-10">
<div className="text-5xl mb-4 opacity-20">🚀</div>
<p className="text-slate-500 text-sm leading-relaxed">
Import an engine and enter an outer diameter<br />to see the 3D rocket
</p>
</div>
)}
<Canvas
camera={{ position: [0.5, 0.3, 0.8], fov: 45 }}
style={{ background: 'transparent' }}
>
<Scene geometry={geometry} />
</Canvas>
</div>
)
}

View File

@@ -0,0 +1,163 @@
// Pure calculation functions for engine design (no React)
/**
* Combustion chamber geometry from thermodynamic results and design inputs.
* Returns null if required thermodynamic values are not yet available.
*/
export function calcChamber(thermo, chamber) {
const At = thermo.At
if (!At || !isFinite(At)) return null
const { Lstar, contractionRatio, convAngleDeg } = chamber
if (!Lstar || !contractionRatio || !convAngleDeg) return null
const Ac = contractionRatio * At
const rc = Math.sqrt(Ac / Math.PI)
const rt = Math.sqrt(At / Math.PI)
const Dc = 2 * rc
const Dt = 2 * rt
const Vc = Lstar * At
const theta = (convAngleDeg * Math.PI) / 180
const L_conv = (rc - rt) / Math.tan(theta)
const V_conv = (Math.PI / 3) * L_conv * (rc * rc + rc * rt + rt * rt)
const L_cyl = Math.max(0, (Vc - V_conv) / Ac)
const Lc = L_cyl + L_conv
return { Dc, Dt, rc, rt, Ac, At, Lc, L_cyl, L_conv, Vc, V_conv, contractionRatio }
}
/**
* Nozzle geometry from thermodynamic results and nozzle design inputs.
* Returns null if required thermodynamic values are not yet available.
*/
export function calcNozzle(thermo, nozzle) {
const At = thermo.At
const Ae = thermo.Ae
if (!At || !Ae || !isFinite(At) || !isFinite(Ae)) return null
const rt = Math.sqrt(At / Math.PI)
const re = Math.sqrt(Ae / Math.PI)
const Dt = 2 * rt
const De = 2 * re
const { type, divAngleDeg } = nozzle
let Ln
if (type === 'bell') {
// 80% bell nozzle length relative to equivalent conical at 15°
Ln = 0.8 * (re - rt) / Math.tan((15 * Math.PI) / 180)
} else {
// conical
const theta = (divAngleDeg * Math.PI) / 180
Ln = (re - rt) / Math.tan(theta)
}
return { Dt, De, rt, re, Ln, type }
}
/**
* Injector orifice sizing for a given element type.
* Returns null if required thermodynamic values are not yet available.
*/
export function calcInjector(thermo, injector) {
const { mdot_f, mdot_ox, p0 } = thermo
if (!mdot_f || !mdot_ox || !p0 || !isFinite(mdot_f) || !isFinite(mdot_ox) || !isFinite(p0)) return null
const { type, N, dpFraction, Cd, rhoFuel, rhoOx } = injector
if (!N || !dpFraction || !Cd || !rhoFuel || !rhoOx) return null
const deltaP = dpFraction * p0
const v_f = Cd * Math.sqrt(2 * deltaP / rhoFuel)
const v_ox = Cd * Math.sqrt(2 * deltaP / rhoOx)
// N elements, each element has one fuel and one oxidiser orifice
const A_f_each = mdot_f / (N * rhoFuel * v_f)
const A_ox_each = mdot_ox / (N * rhoOx * v_ox)
const d_f = Math.sqrt(4 * A_f_each / Math.PI)
const d_ox = Math.sqrt(4 * A_ox_each / Math.PI)
return { deltaP, v_f, v_ox, A_f_each, A_ox_each, d_f, d_ox, N, type }
}
/**
* Cooling analysis based on selected method.
* For regenerative cooling: simplified Bartz heat flux estimate.
* For film cooling: propellant fraction and Isp penalty estimate.
* For ablative/uncooled: informational only.
*/
export function calcCooling(thermo, cooling, chamberGeom) {
const { p0, T0, cstar, mdot } = thermo
const { method } = cooling
if (method === 'regenerative') {
if (!p0 || !T0 || !cstar || !chamberGeom || !isFinite(p0)) return { method }
const { Dt, Dc, Lc } = chamberGeom
// Simplified Bartz heat flux [W/m²] using typical exhaust gas properties
const mu = 6e-5 // Pa·s — typical rocket exhaust dynamic viscosity
const cp = 2000 // J/(kg·K)
const Pr = 0.7
const T_wall = 800 // K — assumed hot-gas-side wall temperature
const q_est = (0.026 / Math.pow(Dt, 0.2)) *
(Math.pow(mu, 0.2) * cp / Math.pow(Pr, 0.6)) *
Math.pow(p0 / cstar, 0.8) *
(T0 - T_wall)
const chamberArea = Math.PI * Dc * Lc
const q_total = q_est * chamberArea
const { channelCount } = cooling
const channelArea = channelCount > 0 ? q_total / channelCount : 0
return { method, q_est, q_total, channelCount, channelArea }
}
if (method === 'film') {
const { filmFraction } = cooling
const mdot_film = (mdot || 0) * filmFraction
// Film cooling reduces effective propellant utilisation — rough Isp penalty
const ispPenalty = filmFraction * 100 // % estimate
return { method, filmFraction, mdot_film, ispPenalty }
}
const notes = {
ablative: 'Ablative liner — consult manufacturer data for material thickness and char rate.',
uncooled: 'Uncooled — confirm combustion gas temperature is within material thermal limits for the burn duration.',
}
return { method, note: notes[method] ?? '' }
}
/**
* Feed system sizing.
* Pressure-fed: pressurant mass estimate.
* Pump-fed: pump head and turbine power estimate.
*/
export function calcFeedSystem(thermo, feedSystem, burnTime) {
const { p0, mdot_f, mdot_ox } = thermo
if (!p0 || !mdot_f || !mdot_ox || !isFinite(p0)) return null
const { type, feedFactor, rhoFuel, rhoOx, pressurantR, pressurantT } = feedSystem
const tb = burnTime && burnTime > 0 ? burnTime : 30
const V_fuel = (tb * mdot_f) / (rhoFuel || 800)
const V_ox = (tb * mdot_ox) / (rhoOx || 1140)
const V_prop = V_fuel + V_ox
if (type === 'pressure_fed') {
const p_tank = p0 * (feedFactor || 1.3)
const R_press = pressurantR || 2077 // J/(kg·K) — helium
const T_press = pressurantT || 300 // K — ambient temperature
const m_press = (p_tank * V_prop) / (R_press * T_press)
return { type, p_tank, V_fuel, V_ox, V_prop, m_press }
}
// pump-fed
const p_tank = 0.5e6 // 0.5 MPa typical inlet tank pressure
const dP_pump = p0 - p_tank
// Power = flow-rate × pressure rise / density (simplified, single-stage)
const P_turbine = (mdot_f / (rhoFuel || 800) + mdot_ox / (rhoOx || 1140)) * dP_pump
return { type, p_tank, V_fuel, V_ox, V_prop, dP_pump, P_turbine }
}

View File

@@ -0,0 +1,318 @@
import JSZip from 'jszip'
import { downloadBlob } from './exportImport.js'
import { formatValue } from './format.js'
export { downloadBlob }
/* ── JSON export / import ──────────────────────────────────────────── */
/**
* Build an engine design JSON Blob for download.
* Schema version 1: inputs section is re-importable; results is reference-only.
*/
export function exportEngineJSON({
thermoInputs, chamber, nozzle, injector, cooling, feedSystem, burnTime,
allThermo, chamberGeometry, nozzleGeometry, injectorGeometry, coolingResults, feedResults,
}) {
const payload = {
version: 1,
type: 'engine_design',
exportedAt: new Date().toISOString(),
inputs: {
thermodynamics: thermoInputs,
chamber,
nozzle,
injector,
cooling,
feedSystem,
burnTime,
},
results: {
thermodynamics: allThermo,
chamberGeometry,
nozzleGeometry,
injectorGeometry,
coolingResults,
feedResults,
},
}
return new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json' })
}
/**
* Parse an imported engine design JSON string and return the inputs section.
* Throws a descriptive Error if the file is invalid.
*/
export function parseEngineImport(jsonString) {
let data
try {
data = JSON.parse(jsonString)
} catch {
throw new Error('File is not valid JSON.')
}
if (data.type !== 'engine_design') {
throw new Error('This file is not an engine design export.')
}
if (data.version !== 1) {
throw new Error(`Unsupported export version: ${data.version}`)
}
return data.inputs ?? {}
}
/* ── ODT XML helpers ────────────────────────────────────────────────── */
function xmlEscape(str) {
return String(str ?? '')
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
}
function cell(text) {
return `<table:table-cell table:style-name="TableCell" office:value-type="string">` +
`<text:p text:style-name="TableContents">${xmlEscape(text)}</text:p>` +
`</table:table-cell>`
}
function headerCell(text) {
return `<table:table-cell table:style-name="TableHeaderCell" office:value-type="string">` +
`<text:p text:style-name="TableHeader">${xmlEscape(text)}</text:p>` +
`</table:table-cell>`
}
function row(...cells) {
return `<table:table-row>${cells.join('')}</table:table-row>`
}
function table(name, columnCount, rows) {
const cols = Array(columnCount)
.fill(`<table:table-column table:style-name="TableColumn"/>`)
.join('')
return `<table:table table:name="${name}" table:style-name="Table">${cols}${rows}</table:table>`
}
function heading(text, level = 1) {
return `<text:h text:style-name="Heading${level}" text:outline-level="${level}">${xmlEscape(text)}</text:h>`
}
function para(text = '') {
return `<text:p text:style-name="Standard">${xmlEscape(text)}</text:p>`
}
function fv(val) {
return val !== null && val !== undefined && isFinite(val) ? formatValue(val) : '—'
}
/* ── ODT styles ─────────────────────────────────────────────────────── */
const STYLES_XML = `<?xml version="1.0" encoding="UTF-8"?>
<office:document-styles
xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0"
xmlns:style="urn:oasis:names:tc:opendocument:xmlns:style:1.0"
xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0"
xmlns:fo="urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0"
xmlns:table="urn:oasis:names:tc:opendocument:xmlns:table:1.0"
office:version="1.3">
<office:styles>
<style:style style:name="Standard" style:family="paragraph" style:class="text"/>
<style:style style:name="Heading1" style:family="paragraph" style:class="text" style:parent-style-name="Standard">
<style:text-properties fo:font-size="16pt" fo:font-weight="bold"/>
</style:style>
<style:style style:name="Heading2" style:family="paragraph" style:class="text" style:parent-style-name="Standard">
<style:text-properties fo:font-size="13pt" fo:font-weight="bold"/>
<style:paragraph-properties fo:margin-top="6pt" fo:margin-bottom="3pt"/>
</style:style>
<style:style style:name="TableContents" style:family="paragraph" style:parent-style-name="Standard">
<style:paragraph-properties fo:padding="1mm"/>
</style:style>
<style:style style:name="TableHeader" style:family="paragraph" style:parent-style-name="Standard">
<style:text-properties fo:font-weight="bold"/>
<style:paragraph-properties fo:padding="1mm"/>
</style:style>
</office:styles>
</office:document-styles>`
const AUTO_STYLES = `
<style:style style:name="Table" style:family="table">
<style:table-properties style:width="16cm" fo:margin-bottom="6mm"/>
</style:style>
<style:style style:name="TableColumn" style:family="table-column">
<style:table-column-properties style:column-width="5.3cm"/>
</style:style>
<style:style style:name="TableCell" style:family="table-cell">
<style:table-cell-properties fo:border="0.05pt solid #888888" fo:padding="1.5mm"/>
</style:style>
<style:style style:name="TableHeaderCell" style:family="table-cell">
<style:table-cell-properties fo:border="0.05pt solid #888888" fo:padding="1.5mm" fo:background-color="#e8e8e8"/>
</style:style>`
const MANIFEST_XML = `<?xml version="1.0" encoding="UTF-8"?>
<manifest:manifest
xmlns:manifest="urn:oasis:names:tc:opendocument:xmlns:manifest:1.0"
manifest:version="1.3">
<manifest:file-entry manifest:full-path="/" manifest:media-type="application/vnd.oasis.opendocument.text"/>
<manifest:file-entry manifest:full-path="content.xml" manifest:media-type="text/xml"/>
<manifest:file-entry manifest:full-path="styles.xml" manifest:media-type="text/xml"/>
</manifest:manifest>`
/* ── ODT content builder ────────────────────────────────────────────── */
function buildEngineContentXml({ allThermo, chamberGeometry: cg, nozzleGeometry: ng, injectorGeometry: ig, coolingResults: cr, feedResults: fr }) {
const exportedAt = new Date().toISOString()
const thermoTable = table('Thermo', 3,
row(headerCell('Parameter'), headerCell('Value'), headerCell('Unit')) +
[
['Chamber Pressure (p0)', fv(allThermo.p0), 'Pa'],
['Chamber Temperature (T0)', fv(allThermo.T0), 'K'],
['Ratio of Specific Heats (g)', fv(allThermo.gamma), '-'],
['Specific Gas Constant (R)', fv(allThermo.R), 'J/(kg K)'],
['Mass Flow Rate (mdot)', fv(allThermo.mdot), 'kg/s'],
['Thrust (F)', fv(allThermo.F), 'N'],
['Specific Impulse (Isp)', fv(allThermo.Isp), 's'],
['Characteristic Velocity (c*)', fv(allThermo.cstar), 'm/s'],
['Thrust Coefficient (CF)', fv(allThermo.CF), '-'],
['Throat Area (At)', fv(allThermo.At), 'm2'],
['Exit Area (Ae)', fv(allThermo.Ae), 'm2'],
['Expansion Ratio (eps)', fv(allThermo.eps), '-'],
['Exit Mach Number (Me)', fv(allThermo.Me), '-'],
['Exit Temperature (Te)', fv(allThermo.Te), 'K'],
['Exit Pressure (pe)', fv(allThermo.pe), 'Pa'],
['Exhaust Velocity (Ve)', fv(allThermo.Ve), 'm/s'],
['Fuel Flow Rate (mdot_f)', fv(allThermo.mdot_f), 'kg/s'],
['Oxidiser Flow Rate (mdot_ox)', fv(allThermo.mdot_ox), 'kg/s'],
].map(([n, v, u]) => row(cell(n), cell(v), cell(u))).join('')
)
const chamberTable = cg
? table('Chamber', 3,
row(headerCell('Parameter'), headerCell('Value'), headerCell('Unit')) +
[
['Chamber Diameter (Dc)', fv(cg.Dc * 1000), 'mm'],
['Throat Diameter (Dt)', fv(cg.Dt * 1000), 'mm'],
['Contraction Ratio', fv(cg.contractionRatio), '-'],
['Total Chamber Length (Lc)', fv(cg.Lc * 1000), 'mm'],
['Cylindrical Section Length', fv(cg.L_cyl * 1000), 'mm'],
['Convergent Section Length', fv(cg.L_conv * 1000), 'mm'],
['Chamber Volume', fv(cg.Vc * 1e6), 'cm3'],
].map(([n, v, u]) => row(cell(n), cell(v), cell(u))).join('')
)
: para('(insufficient inputs to calculate chamber geometry)')
const nozzleTable = ng
? table('Nozzle', 3,
row(headerCell('Parameter'), headerCell('Value'), headerCell('Unit')) +
[
['Type', ng.type, '-'],
['Throat Diameter (Dt)', fv(ng.Dt * 1000), 'mm'],
['Exit Diameter (De)', fv(ng.De * 1000), 'mm'],
['Nozzle Length (Ln)', fv(ng.Ln * 1000), 'mm'],
].map(([n, v, u]) => row(cell(n), cell(v), cell(u))).join('')
)
: para('(insufficient inputs to calculate nozzle geometry)')
const injectorTable = ig
? table('Injector', 3,
row(headerCell('Parameter'), headerCell('Value'), headerCell('Unit')) +
[
['Type', ig.type, '-'],
['Number of Elements (N)', fv(ig.N), '-'],
['Pressure Drop (dP)', fv(ig.deltaP), 'Pa'],
['Fuel Jet Velocity', fv(ig.v_f), 'm/s'],
['Oxidiser Jet Velocity', fv(ig.v_ox), 'm/s'],
['Fuel Orifice Diameter', fv(ig.d_f * 1000), 'mm'],
['Oxidiser Orifice Diameter', fv(ig.d_ox * 1000), 'mm'],
].map(([n, v, u]) => row(cell(n), cell(v), cell(u))).join('')
)
: para('(insufficient inputs to calculate injector geometry)')
const coolingSection = cr
? (() => {
const rows = [['Method', cr.method, '-']]
if (cr.q_est != null) rows.push(['Estimated Heat Flux', fv(cr.q_est), 'W/m2'])
if (cr.q_total != null) rows.push(['Total Heat Load', fv(cr.q_total), 'W'])
if (cr.channelCount != null) rows.push(['Channel Count', fv(cr.channelCount), '-'])
if (cr.filmFraction != null) rows.push(['Film Mass Fraction', fv(cr.filmFraction), '-'])
if (cr.mdot_film != null) rows.push(['Film Mass Flow', fv(cr.mdot_film), 'kg/s'])
if (cr.ispPenalty != null) rows.push(['Est. Isp Penalty', fv(cr.ispPenalty), '%'])
if (cr.note) rows.push(['Note', cr.note, ''])
return table('Cooling', 3,
row(headerCell('Parameter'), headerCell('Value'), headerCell('Unit')) +
rows.map(([n, v, u]) => row(cell(n), cell(v), cell(u))).join('')
)
})()
: para('(insufficient inputs)')
const feedTable = fr
? table('FeedSystem', 3,
row(headerCell('Parameter'), headerCell('Value'), headerCell('Unit')) +
[
['Type', fr.type, '-'],
['Tank Pressure', fv(fr.p_tank), 'Pa'],
['Fuel Volume', fv(fr.V_fuel * 1000), 'L'],
['Oxidiser Volume', fv(fr.V_ox * 1000), 'L'],
['Total Propellant Volume', fv(fr.V_prop * 1000), 'L'],
...(fr.m_press != null ? [['Pressurant Mass', fv(fr.m_press), 'kg']] : []),
...(fr.dP_pump != null ? [['Pump Delta-P', fv(fr.dP_pump), 'Pa']] : []),
...(fr.P_turbine != null ? [['Est. Turbine Power', fv(fr.P_turbine / 1000), 'kW']] : []),
].map(([n, v, u]) => row(cell(n), cell(v), cell(u))).join('')
)
: para('(insufficient inputs)')
const body = [
heading('Engine Design Report'),
para(`Exported: ${exportedAt}`),
para(),
heading('1. Thermodynamic Performance', 2),
thermoTable,
para(),
heading('2. Combustion Chamber', 2),
chamberTable,
para(),
heading('3. Nozzle', 2),
nozzleTable,
para(),
heading('4. Injector', 2),
injectorTable,
para(),
heading('5. Cooling', 2),
coolingSection,
para(),
heading('6. Feed System', 2),
feedTable,
].join('\n')
return `<?xml version="1.0" encoding="UTF-8"?>
<office:document-content
xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0"
xmlns:style="urn:oasis:names:tc:opendocument:xmlns:style:1.0"
xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0"
xmlns:table="urn:oasis:names:tc:opendocument:xmlns:table:1.0"
xmlns:fo="urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0"
office:version="1.3">
<office:automatic-styles>${AUTO_STYLES}</office:automatic-styles>
<office:body>
<office:text>
${body}
</office:text>
</office:body>
</office:document-content>`
}
/* ── Public API ─────────────────────────────────────────────────────── */
/**
* Build an ODT Blob from the current engine design state.
* @returns {Promise<Blob>}
*/
export async function exportEngineOdt(designState) {
const zip = new JSZip()
zip.file('mimetype', 'application/vnd.oasis.opendocument.text', { compression: 'STORE' })
zip.file('META-INF/manifest.xml', MANIFEST_XML)
zip.file('styles.xml', STYLES_XML)
zip.file('content.xml', buildEngineContentXml(designState))
return zip.generateAsync({ type: 'blob', mimeType: 'application/vnd.oasis.opendocument.text' })
}

View File

@@ -0,0 +1,512 @@
// Per-field descriptions for the Engine Design page info popups.
// Each entry: { name, description, higher, lower }
export const ENGINE_FIELD_INFO = {
/* ── Thermodynamic Inputs ─────────────────────────────────────── */
p0: {
name: 'Chamber Pressure (p₀)',
description: 'Total stagnation pressure in the combustion chamber. One of the most critical design parameters — sets the thermodynamic cycle for the entire engine.',
higher: 'Higher Isp and CF, smaller throat for same flow — but heavier chamber walls and higher required feed pressure',
lower: 'Lighter structure and lower feed pressure — at the cost of performance and a larger throat',
},
T0: {
name: 'Adiabatic Flame Temperature (T₀)',
description: 'Total stagnation temperature of the combustion gases. Set primarily by propellant chemistry and O/F ratio; not a free design variable for real propellants.',
higher: 'Higher c* and Isp — but greater thermal loads demanding better cooling and higher-temperature materials',
lower: 'Reduced thermal stress and cooling requirements — at the cost of performance',
},
gamma: {
name: 'Ratio of Specific Heats (γ)',
description: 'Ratio of Cp to Cv for the combustion gas mixture. Governs how efficiently the gas expands through the nozzle.',
higher: 'Steeper isentropic expansion for a given ε — slightly higher CF',
lower: 'Flatter expansion profile; combustion products typically fall in the range γ ≈ 1.151.30',
},
R_gas: {
name: 'Specific Gas Constant (R)',
description: 'R = R_universal / M_molecular. Reflects the mean molecular weight of the combustion products. Linked to propellant chemistry.',
higher: 'Lower molecular weight gas → higher c* and Isp (e.g. hydrogen propellants)',
lower: 'Heavier exhaust gas → lower performance (e.g. storable propellants)',
},
mdot: {
name: 'Total Mass Flow Rate (ṁ)',
description: 'Total propellant flow through the engine. Combined with exhaust velocity it determines thrust. Sets the size of all flow-path components.',
higher: 'More thrust and a larger engine — greater propellant consumption',
lower: 'Smaller, lighter engine with lower thrust',
},
F_input: {
name: 'Thrust (F) — input',
description: 'Desired thrust level. Specify this instead of mass flow rate; the solver will derive ṁ from F, p₀, and nozzle conditions.',
higher: 'Larger engine, higher propellant consumption, greater structural loads',
lower: 'Smaller engine, lower propellant consumption',
},
OF: {
name: 'Oxidiser-to-Fuel Mass Ratio (O/F)',
description: 'Mass of oxidiser consumed per unit mass of fuel. Affects flame temperature, gas properties, and specific impulse. Peak Isp often occurs slightly fuel-rich.',
higher: 'More oxidiser-rich — can reduce peak temperature but may move away from optimum Isp',
lower: 'More fuel-rich — often closer to peak Isp; excess fuel reduces combustion temperature',
},
At_input: {
name: 'Throat Area (Aₜ) — input',
description: 'Cross-sectional area at the nozzle throat. You can specify it manually to override the solver, or leave it blank and let ṁ and p₀ determine it.',
higher: 'Higher flow capacity — needed for greater thrust or lower chamber pressure',
lower: 'Smaller throat — requires higher chamber pressure to sustain the same flow',
},
eps_input: {
name: 'Expansion Ratio (ε = Aₑ/Aₜ) — input',
description: 'Ratio of exit area to throat area. Controls how far the exhaust gas expands. The optimum ε matches exit pressure to ambient pressure for maximum efficiency.',
higher: 'More expansion, lower exit pressure, higher Isp — but a longer, heavier nozzle',
lower: 'Less expansion, shorter nozzle — under-expanded at altitude',
},
pa: {
name: 'Ambient Pressure (pₐ)',
description: 'Ambient back pressure at the nozzle exit plane. Used to compute the pressure thrust component and the optimum expansion ratio.',
higher: 'Greater back pressure reduces pressure thrust — lower effective Isp',
lower: 'Vacuum conditions maximise the pressure thrust term',
},
/* ── Combustion Chamber ───────────────────────────────────────── */
Lstar: {
name: 'Characteristic Chamber Length (L*)',
description: 'L* = V_chamber / A_throat. A geometric proxy for propellant residence time. Must be long enough for complete combustion — propellant-dependent.',
higher: 'Longer residence time, more complete combustion — but a heavier chamber',
lower: 'Shorter, lighter chamber — risk of incomplete combustion and acoustic instability',
},
contractionRatio: {
name: 'Contraction Ratio (Ac / At)',
description: 'Ratio of the chamber cross-section to the throat area. Determines chamber diameter for a given throat size.',
higher: 'Larger chamber relative to the throat — better flow uniformity, but heavier',
lower: 'More compact chamber — may cause flow maldistribution; typical range is 310',
},
convAngleDeg: {
name: 'Convergent Half-Angle',
description: 'Half-angle of the nozzle convergent section. Affects the length and pressure recovery of the converging part of the chamber.',
higher: 'Steeper convergence → shorter chamber — risk of flow separation or higher losses',
lower: 'Gentler convergence → smoother flow transition, but longer and heavier',
},
/* ── Nozzle ───────────────────────────────────────────────────── */
nozzleType: {
name: 'Nozzle Contour Type',
description: 'Conical nozzles are simple but have divergence losses. Bell (Rao) nozzles are contoured to straighten the flow, achieving ~99% efficiency at 80% of the equivalent conical length.',
higher: null,
lower: null,
},
divAngleDeg: {
name: 'Divergence Half-Angle (Conical)',
description: 'Half-angle of the conical nozzle diverging section. A divergence loss factor λ ≈ (1 + cos α) / 2 applies. Typical range: 1218°.',
higher: 'Shorter nozzle — but higher divergence losses reduce effective CF',
lower: 'Better axial momentum alignment, higher CF — but longer and heavier nozzle',
},
/* ── Injector ─────────────────────────────────────────────────── */
injectorType: {
name: 'Injector Element Type',
description: 'Determines the mixing and atomisation mechanism. Impinging doublets/triplets use jet impingement; coaxial elements rely on shear; pintles use an annular gap.',
higher: null,
lower: null,
},
injectorN: {
name: 'Number of Injector Elements (N)',
description: 'Total number of fuel/oxidiser element pairs on the injector face. More elements give finer atomisation and better mixing uniformity.',
higher: 'Finer spray, better mixing, more stable combustion — but more complex to manufacture',
lower: 'Simpler injector — risk of poor atomisation and combustion instability',
},
dpFraction: {
name: 'Pressure Drop Fraction (ΔP / p₀)',
description: 'Injector pressure drop as a fraction of chamber pressure. Acts as the primary stability margin — higher ΔP makes the injector less sensitive to combustion pressure oscillations.',
higher: 'Better atomisation and stability margin — requires higher tank/feed pressure',
lower: 'Reduces feed-system pressure requirement — risk of combustion instability',
},
Cd: {
name: 'Discharge Coefficient (Cd)',
description: 'Ratio of actual to ideal orifice mass flow. Accounts for vena contracta and flow path losses. Sharp-edged orifices typically have Cd ≈ 0.61; rounded inlets approach 0.850.95.',
higher: 'More efficient orifice → smaller hole needed for the same mass flow',
lower: 'More restricted flow → larger orifice required',
},
rhoFuel_inj: {
name: 'Fuel Density (ρ_f) — injector',
description: 'Liquid fuel density at injection conditions. Used alongside mass flow and jet velocity to size the fuel orifice area.',
higher: 'Denser fuel → smaller orifice diameter for the same mass flow',
lower: 'Less dense fuel → larger orifice required',
},
rhoOx_inj: {
name: 'Oxidiser Density (ρ_ox) — injector',
description: 'Liquid oxidiser density at injection conditions. Used to size the oxidiser orifice area.',
higher: 'Denser oxidiser → smaller orifice diameter',
lower: 'Less dense oxidiser → larger orifice',
},
/* ── Cooling ──────────────────────────────────────────────────── */
coolingMethod: {
name: 'Cooling Method',
description: 'Regenerative cooling circulates propellant through channels to absorb heat. Film cooling injects a thin fuel layer along the wall. Ablative liners sacrifice material. Uncooled engines are for very short burns.',
higher: null,
lower: null,
},
channelCount: {
name: 'Cooling Channel Count',
description: 'Number of regenerative cooling channels around the chamber and nozzle wall. More channels distribute heat pickup across more passages.',
higher: 'Better heat distribution, smaller per-channel flow area — more complex manufacture',
lower: 'Fewer, larger channels — simpler but each must handle more heat load',
},
filmFraction: {
name: 'Film Cooling Mass Fraction',
description: 'Fraction of total propellant flow injected as a film along the chamber walls to protect them from peak heat flux.',
higher: 'Better wall protection — but more unburned propellant reduces Isp',
lower: 'Less Isp penalty — risk of wall overheating at high heat flux',
},
/* ── Feed System ──────────────────────────────────────────────── */
feedType: {
name: 'Feed System Type',
description: 'Pressure-fed systems use pressurised tanks to push propellant to the chamber. Pump-fed systems use turbopumps for much higher chamber pressures at lower tank mass.',
higher: null,
lower: null,
},
feedFactor: {
name: 'Feed Pressure Factor',
description: 'Tank pressure as a multiple of chamber pressure for pressure-fed systems. Must be >1 to overcome injector ΔP and line losses. Typical range: 1.31.6.',
higher: 'More pressure margin — heavier tank walls and more pressurant required',
lower: 'Lighter system — less margin against feed-line or injector flow interruption',
},
rhoFuel_feed: {
name: 'Fuel Density (ρ_f) — feed system',
description: 'Bulk fuel density used to compute the fuel tank volume from the required fuel mass.',
higher: 'Denser fuel → smaller tank volume for the same mass',
lower: 'Less dense fuel → larger tank',
},
rhoOx_feed: {
name: 'Oxidiser Density (ρ_ox) — feed system',
description: 'Bulk oxidiser density used to compute the oxidiser tank volume.',
higher: 'Denser oxidiser → smaller tank volume',
lower: 'Less dense oxidiser → larger tank',
},
burnTime: {
name: 'Burn Time',
description: 'Total engine burn duration. Used to compute total propellant mass and volumes, and pressurant requirements.',
higher: 'More propellant, larger tanks, potentially more pressurant',
lower: 'Less propellant, smaller system — shorter mission or more stages',
},
/* ── Thermodynamic Results ────────────────────────────────────── */
At_result: {
name: 'Throat Area (Aₜ)',
description: 'Choked-flow cross-section that sets the engine mass flow capacity. The most thermally critical point in the nozzle.',
higher: 'Higher flow capacity for a given chamber pressure',
lower: 'Smaller, lighter throat — requires higher p₀ for the same ṁ',
},
Ae_result: {
name: 'Exit Area (Aₑ)',
description: 'Nozzle exit cross-section. Determined by ε × Aₜ. Sets nozzle bell diameter and integration envelope.',
higher: 'Larger exit for higher expansion ratio — heavier and harder to package',
lower: 'More compact nozzle exit — under-expanded at altitude',
},
eps_result: {
name: 'Expansion Ratio (ε)',
description: 'Aₑ / Aₜ. The optimum ε matches exit pressure to ambient for maximum thrust coefficient.',
higher: 'Better vacuum Isp — diminishing returns above ε ≈ 100',
lower: 'Shorter, lighter nozzle — better suited to sea-level operation',
},
Me_result: {
name: 'Exit Mach Number (Mₑ)',
description: 'Exhaust speed at the nozzle exit relative to the local sound speed. Indicates how fully the gas has expanded.',
higher: 'More complete expansion → higher Isp',
lower: 'Under-expanded — flow still has kinetic energy to give',
},
Te_result: {
name: 'Exit Temperature (Tₑ)',
description: 'Static temperature of the exhaust at the nozzle exit plane. Indicates how much thermal energy has been converted to kinetic energy.',
higher: 'More thermal energy remaining → less efficient conversion to velocity',
lower: 'More energy converted to exhaust velocity → higher Isp',
},
pe_result: {
name: 'Exit Pressure (pₑ)',
description: 'Static pressure at the nozzle exit. Ideally matches ambient pressure for maximum CF. Mismatch causes under- or over-expansion losses.',
higher: 'Under-expanded — nozzle too short; pressure thrust wasted',
lower: 'Over-expanded — risk of flow separation at sea level',
},
Ve_result: {
name: 'Exhaust Velocity (Vₑ)',
description: 'Actual gas velocity at the nozzle exit. Primary contributor to specific impulse (Isp ≈ Vₑ / g₀ for perfectly expanded nozzle).',
higher: 'Higher Isp and better propulsive efficiency',
lower: 'Lower performance — more propellant needed for the same Δv',
},
F_result: {
name: 'Thrust (F)',
description: 'Total thrust = momentum thrust + pressure thrust. F = ṁ·Vₑ + (pₑ pₐ)·Aₑ.',
higher: 'Faster vehicle acceleration or higher payload capability',
lower: 'Lower structural loads, easier integration',
},
Isp_result: {
name: 'Specific Impulse (Isp)',
description: 'Thrust per unit weight flow: Isp = F / (ṁ · g₀). The fundamental measure of propellant efficiency.',
higher: 'More Δv per kg of propellant — less propellant mass fraction needed',
lower: 'Less efficient — more propellant mass required for the same mission',
},
cstar_result: {
name: 'Characteristic Velocity (c*)',
description: 'c* = p₀ · Aₜ / ṁ. Measures combustion chamber performance independently of nozzle shape.',
higher: 'Better combustion efficiency or higher-energy propellants',
lower: 'Combustion inefficiency, off-design mixture ratio, or low-energy propellants',
},
CF_result: {
name: 'Thrust Coefficient (CF)',
description: 'Dimensionless nozzle efficiency factor: F = CF · p₀ · Aₜ. Combines expansion efficiency and pressure matching.',
higher: 'Better nozzle performance — closer to the optimum expansion condition',
lower: 'Under- or over-expansion losses, or flow separation in over-expanded nozzle',
},
mdot_f_result: {
name: 'Fuel Mass Flow Rate (ṁ_f)',
description: 'Fuel propellant consumption rate, derived from total ṁ and O/F: ṁ_f = ṁ / (1 + O/F).',
higher: 'More fuel consumption — larger fuel tank and feed system required',
lower: 'Less fuel flow — smaller fuel system',
},
mdot_ox_result: {
name: 'Oxidiser Mass Flow Rate (ṁ_ox)',
description: 'Oxidiser consumption rate: ṁ_ox = ṁ · O/F / (1 + O/F).',
higher: 'More oxidiser consumption — larger oxidiser tank required',
lower: 'Less oxidiser flow — smaller oxidiser system',
},
/* ── Chamber Geometry Results ─────────────────────────────────── */
Dc_result: {
name: 'Chamber Diameter (Dc)',
description: 'Inner diameter of the cylindrical combustion chamber. Determined by Dt × √(contraction ratio).',
higher: 'Larger chamber volume per unit length — can achieve the required L* in a shorter chamber',
lower: 'More compact chamber — requires greater axial length to reach the target L*',
},
Dt_result: {
name: 'Throat Diameter (Dt)',
description: 'Diameter of the nozzle throat. The most thermally and mechanically stressed location in the engine.',
higher: 'Higher mass flow capacity for the given chamber pressure',
lower: 'More concentrated heat flux at the throat — harder to cool',
},
contractionRatio_result: {
name: 'Contraction Ratio (Ac / At)',
description: 'Ratio of chamber cross-section to throat area. Determines how much the flow contracts before the throat.',
higher: 'Wider chamber relative to the throat — better flow uniformity',
lower: 'Tighter contraction — risk of flow distortion entering the throat',
},
Lc_result: {
name: 'Total Chamber Length (Lc)',
description: 'Combined axial length of the cylindrical and convergent chamber sections.',
higher: 'Longer chamber — more complete combustion, but heavier',
lower: 'Shorter, lighter chamber — may need a high-energy propellant to reach required L*',
},
L_cyl_result: {
name: 'Cylindrical Section Length',
description: 'Length of the straight cylindrical portion where most combustion occurs.',
higher: 'More mixing and dwell time in the cylindrical zone',
lower: 'Shorter cylinder — the convergent section supplies additional volume',
},
L_conv_result: {
name: 'Convergent Section Length',
description: 'Axial length of the nozzle convergent section from the cylindrical chamber to the throat.',
higher: 'Gentler convergence angle or wider chamber upstream',
lower: 'Steeper convergence — risk of flow separation or higher losses',
},
Vc_result: {
name: 'Chamber Volume (Vc)',
description: 'Total internal volume of the combustion chamber. Combined with throat area gives L* = Vc / At.',
higher: 'Greater residence time → more complete combustion',
lower: 'Shorter, lighter chamber — needs high-energy propellant for complete combustion',
},
/* ── Nozzle Geometry Results ──────────────────────────────────── */
De_result: {
name: 'Exit Diameter (De)',
description: 'Outer diameter of the nozzle exit plane. Sets the integration envelope for the nozzle.',
higher: 'Larger exit for higher expansion — heavier and harder to package',
lower: 'More compact nozzle — under-expanded at altitude, over-expanded at sea level',
},
Ln_result: {
name: 'Nozzle Length (Ln)',
description: 'Axial length of the diverging nozzle section from throat to exit plane.',
higher: 'More axial length needed for high expansion ratios — adds mass',
lower: 'Shorter nozzle — less expansion, but lighter and easier to integrate',
},
/* ── Injector Results ─────────────────────────────────────────── */
deltaP_result: {
name: 'Injector Pressure Drop (ΔP)',
description: 'Pressure difference across the injector face. A key stability parameter — typically 1530% of chamber pressure.',
higher: 'Better atomisation and stability margin — requires higher feed pressure',
lower: 'Reduced feed pressure requirement — risk of combustion instability',
},
v_f_result: {
name: 'Fuel Jet Velocity',
description: 'Velocity of the fuel stream as it exits the orifice. Higher velocity improves atomisation at impingement.',
higher: 'Better atomisation and mixing — finer spray droplets',
lower: 'Coarser spray — may need more injector elements to compensate',
},
v_ox_result: {
name: 'Oxidiser Jet Velocity',
description: 'Velocity of the oxidiser stream exiting the orifice.',
higher: 'Better atomisation — but higher pressure drop across the orifice',
lower: 'Coarser spray — may impair mixing quality',
},
d_f_result: {
name: 'Fuel Orifice Diameter',
description: 'Diameter of each individual fuel injector orifice. Sized from ṁ_f, Cd, ρ_f, and jet velocity.',
higher: 'Larger, easier-to-manufacture orifice — coarser atomisation',
lower: 'Finer spray — but more susceptible to clogging',
},
d_ox_result: {
name: 'Oxidiser Orifice Diameter',
description: 'Diameter of each oxidiser orifice. Sized from ṁ_ox, Cd, ρ_ox, and jet velocity.',
higher: 'Larger orifice — coarser oxidiser spray',
lower: 'Finer spray — tighter manufacturing tolerances required',
},
/* ── Cooling Results ──────────────────────────────────────────── */
q_est_result: {
name: 'Estimated Heat Flux (q″)',
description: 'Estimated peak heat flux at the throat region based on a simplified Bartz-style correlation.',
higher: 'More aggressive thermal environment — demands greater coolant flow or better channel geometry',
lower: 'Easier cooling problem — more design margin',
},
q_total_result: {
name: 'Total Heat Load',
description: 'Total heat power absorbed by the coolant, integrated over the chamber and nozzle surface area.',
higher: 'More coolant flow or a larger temperature rise in the coolant required',
lower: 'Easier to manage with available coolant flow',
},
channelCount_result: {
name: 'Cooling Channel Count',
description: 'Number of regenerative cooling channels (reflects the input setting).',
higher: 'Distributed heat pickup — less flow per channel',
lower: 'Fewer channels — each must carry more coolant',
},
channelArea_result: {
name: 'Cooling Channel Area (per channel)',
description: 'Cross-sectional flow area of each cooling channel. Determines coolant velocity and pressure drop.',
higher: 'Lower coolant velocity — lower pressure drop but less turbulent heat transfer',
lower: 'Higher velocity — better heat transfer coefficient but larger pressure drop',
},
mdot_film_result: {
name: 'Film Coolant Mass Flow',
description: 'Mass flow rate of propellant injected as a film layer along the chamber walls.',
higher: 'Better wall protection — more effective at high heat flux',
lower: 'Less Isp penalty from film dilution',
},
ispPenalty_result: {
name: 'Isp Penalty (Film Cooling)',
description: 'Estimated percentage reduction in specific impulse due to film coolant that does not participate fully in combustion.',
higher: 'Significant Isp loss — consider reducing film fraction if thermally feasible',
lower: 'Minimal performance impact — film cooling is efficient for this design',
},
/* ── Feed System Results ──────────────────────────────────────── */
p_tank_result: {
name: 'Tank Pressure',
description: 'Required propellant tank pressure to sustain the desired chamber pressure through the injector and feed lines.',
higher: 'Heavier tank walls and more pressurant — may require a composite tank',
lower: 'Lighter structure — check that there is sufficient margin over chamber pressure',
},
V_fuel_result: {
name: 'Fuel Volume',
description: 'Required fuel tank volume for the specified burn time and fuel density.',
higher: 'Larger, heavier fuel tank — may need to be distributed or jettisoned',
lower: 'Compact fuel system — allows a smaller vehicle',
},
V_ox_result: {
name: 'Oxidiser Volume',
description: 'Required oxidiser tank volume for the specified burn time and oxidiser density.',
higher: 'Larger oxidiser tank — often the dominant volume for high-O/F propellants',
lower: 'Compact oxidiser system',
},
V_prop_result: {
name: 'Total Propellant Volume',
description: 'Combined fuel + oxidiser volume. Determines the overall propellant tankage size.',
higher: 'More propellant for longer burns or higher thrust',
lower: 'Smaller, lighter vehicle',
},
m_press_result: {
name: 'Pressurant Mass',
description: 'Mass of pressurising gas (typically helium or nitrogen) needed to maintain tank pressure throughout the burn. Estimated assuming isothermal blowdown.',
higher: 'Heavier pressurant load — consider a regulated or blowdown feed system',
lower: 'Lightweight pressurant — efficient feed system design',
},
dP_pump_result: {
name: 'Pump ΔP',
description: 'Required pump pressure rise across the propellant pump(s) in a pump-fed system.',
higher: 'More pump work — larger, heavier turbopump needed',
lower: 'Lighter pump — may indicate lower chamber pressure or very efficient feed lines',
},
P_turbine_result: {
name: 'Est. Turbine Power',
description: 'Estimated turbopump turbine shaft power required to drive the propellant pumps.',
higher: 'Larger turbine and more turbine propellant bleed — heavier turbopump assembly',
lower: 'Smaller turbine — more efficient or lower chamber pressure design',
},
}

519
src/engine/equations.js Normal file
View File

@@ -0,0 +1,519 @@
import { machFromAreaRatio, areaRatioFromMach } from './numerics.js'
// Each equation:
// id unique key
// name display name
// formula formula string shown to user
// variables all variable ids involved
// solvers map of { variableId: fn(knownValues) => number }
// A solver may return NaN/Infinity to signal infeasibility.
export const EQUATIONS = [
// ── Thrust ─────────────────────────────────────────────────────────────
{
id: 'fundamental_thrust',
name: 'Fundamental Thrust Equation',
formula: 'F = ṁ·Vₑ + (pₑ pₐ)·Aₑ',
category: 'Thrust',
variables: ['F', 'mdot', 'Ve', 'pe', 'pa', 'Ae'],
solvers: {
F: v => v.mdot * v.Ve + (v.pe - v.pa) * v.Ae,
mdot: Object.assign(
v => (v.F - ('Ae' in v ? (v.pe - v.pa) * v.Ae : 0)) / v.Ve,
{
requires: known => {
const adapted = 'pe' in known && 'pa' in known && known.pe === known.pa
return adapted ? ['F', 'Ve', 'pe', 'pa'] : ['F', 'Ve', 'pe', 'pa', 'Ae']
},
},
),
Ve: v => (v.F - (v.pe - v.pa) * v.Ae) / v.mdot,
pe: v => (v.F - v.mdot * v.Ve) / v.Ae + v.pa,
pa: v => v.pe - (v.F - v.mdot * v.Ve) / v.Ae,
Ae: v => (v.F - v.mdot * v.Ve) / (v.pe - v.pa),
},
},
{
id: 'thrust_from_isp',
name: 'Thrust from Isp',
formula: 'F = ṁ·Isp·g₀',
category: 'Thrust',
variables: ['F', 'mdot', 'Isp', 'g0'],
solvers: {
F: v => v.mdot * v.Isp * v.g0,
mdot: v => v.F / (v.Isp * v.g0),
Isp: v => v.F / (v.mdot * v.g0),
g0: v => v.F / (v.mdot * v.Isp),
},
},
// ── Effective Exhaust Velocity ─────────────────────────────────────────
{
id: 'ceff_def',
name: 'Effective Exhaust Velocity',
formula: 'cₑ = F / ṁ',
category: 'Thrust',
variables: ['ceff', 'F', 'mdot'],
solvers: {
ceff: v => v.F / v.mdot,
F: v => v.ceff * v.mdot,
mdot: v => v.F / v.ceff,
},
},
// ── Specific Impulse ───────────────────────────────────────────────────
{
id: 'isp_from_ve',
name: 'Isp from Effective Exhaust Velocity',
formula: 'Isp = cₑ / g₀',
category: 'Specific Impulse',
variables: ['Isp', 'ceff', 'g0'],
solvers: {
Isp: v => v.ceff / v.g0,
ceff: v => v.Isp * v.g0,
g0: v => v.ceff / v.Isp,
},
},
{
id: 'isp_from_thrust',
name: 'Isp from Thrust & Mass Flow',
formula: 'Isp = F / (ṁ·g₀)',
category: 'Specific Impulse',
variables: ['Isp', 'F', 'mdot', 'g0'],
solvers: {
Isp: v => v.F / (v.mdot * v.g0),
F: v => v.Isp * v.mdot * v.g0,
mdot: v => v.F / (v.Isp * v.g0),
g0: v => v.F / (v.Isp * v.mdot),
},
},
// ── Characteristic Velocity ────────────────────────────────────────────
{
id: 'cstar_def',
name: 'Characteristic Velocity',
formula: 'c* = p₀·Aₜ / ṁ',
category: 'Nozzle Performance',
variables: ['cstar', 'p0', 'At', 'mdot'],
solvers: {
cstar: v => (v.p0 * v.At) / v.mdot,
p0: v => (v.cstar * v.mdot) / v.At,
At: v => (v.cstar * v.mdot) / v.p0,
mdot: v => (v.p0 * v.At) / v.cstar,
},
},
{
id: 'cstar_from_cf_isp',
name: 'c* from Thrust Coefficient & Isp',
formula: 'c* = Isp·g₀ / Cꜰ',
category: 'Nozzle Performance',
variables: ['cstar', 'Isp', 'g0', 'CF'],
solvers: {
cstar: v => (v.Isp * v.g0) / v.CF,
Isp: v => (v.cstar * v.CF) / v.g0,
g0: v => (v.cstar * v.CF) / v.Isp,
CF: v => (v.Isp * v.g0) / v.cstar,
},
},
// ── Thrust Coefficient ─────────────────────────────────────────────────
{
id: 'thrust_coefficient',
name: 'Thrust Coefficient',
formula: 'Cꜰ = F / (p₀·Aₜ)',
category: 'Nozzle Performance',
variables: ['CF', 'F', 'p0', 'At'],
solvers: {
CF: v => v.F / (v.p0 * v.At),
F: v => v.CF * v.p0 * v.At,
p0: v => v.F / (v.CF * v.At),
At: v => v.F / (v.CF * v.p0),
},
},
// ── Tsiolkovsky Rocket Equation ────────────────────────────────────────
{
id: 'tsiolkovsky_ve',
name: 'Tsiolkovsky Rocket Equation (cₑ)',
formula: 'Δv = cₑ·ln(m₀/mf)',
category: 'Rocket Equation',
variables: ['dv', 'ceff', 'm0', 'mf'],
solvers: {
dv: v => v.ceff * Math.log(v.m0 / v.mf),
ceff: v => v.dv / Math.log(v.m0 / v.mf),
m0: v => v.mf * Math.exp(v.dv / v.ceff),
mf: v => v.m0 / Math.exp(v.dv / v.ceff),
},
},
{
id: 'tsiolkovsky_isp',
name: 'Tsiolkovsky Rocket Equation (Isp)',
formula: 'Δv = Isp·g₀·ln(m₀/mf)',
category: 'Rocket Equation',
variables: ['dv', 'Isp', 'g0', 'm0', 'mf'],
solvers: {
dv: v => v.Isp * v.g0 * Math.log(v.m0 / v.mf),
Isp: v => v.dv / (v.g0 * Math.log(v.m0 / v.mf)),
g0: v => v.dv / (v.Isp * Math.log(v.m0 / v.mf)),
m0: v => v.mf * Math.exp(v.dv / (v.Isp * v.g0)),
mf: v => v.m0 / Math.exp(v.dv / (v.Isp * v.g0)),
},
},
{
id: 'mass_ratio',
name: 'Mass Ratio',
formula: 'MR = m₀ / mf',
category: 'Rocket Equation',
variables: ['MR', 'm0', 'mf'],
solvers: {
MR: v => v.m0 / v.mf,
m0: v => v.MR * v.mf,
mf: v => v.m0 / v.MR,
},
},
{
id: 'propellant_mass',
name: 'Propellant Mass',
formula: 'mₚ = m₀ mf',
category: 'Rocket Equation',
variables: ['mp', 'm0', 'mf'],
solvers: {
mp: v => v.m0 - v.mf,
m0: v => v.mp + v.mf,
mf: v => v.m0 - v.mp,
},
},
{
id: 'mass_fraction',
name: 'Propellant Mass Fraction',
formula: 'ζ = mₚ / m₀',
category: 'Rocket Equation',
variables: ['zeta', 'mp', 'm0'],
solvers: {
zeta: v => v.mp / v.m0,
mp: v => v.zeta * v.m0,
m0: v => v.mp / v.zeta,
},
},
{
id: 'burn_time',
name: 'Burn Time',
formula: 'tᵦ = mₚ / ṁ',
category: 'Rocket Equation',
variables: ['tb', 'mp', 'mdot'],
solvers: {
tb: v => v.mp / v.mdot,
mp: v => v.tb * v.mdot,
mdot: v => v.mp / v.tb,
},
},
// ── Isentropic Flow Relations ──────────────────────────────────────────
{
id: 'isentropic_temp',
name: 'Isentropic Temperature Ratio',
formula: 'T/T₀ = (1 + (γ1)/2·M²)⁻¹',
category: 'Isentropic Flow',
variables: ['T', 'T0', 'M', 'gamma'],
solvers: {
T: v => v.T0 / (1 + (v.gamma - 1) / 2 * v.M * v.M),
T0: v => v.T * (1 + (v.gamma - 1) / 2 * v.M * v.M),
M: v => Math.sqrt((v.T0 / v.T - 1) * 2 / (v.gamma - 1)),
gamma: v => {
// T/T0 = 1/(1 + (γ-1)/2·M²) → T0/T - 1 = (γ-1)/2·M²
// γ = 1 + 2(T0/T - 1)/M²
return 1 + 2 * (v.T0 / v.T - 1) / (v.M * v.M)
},
},
},
{
id: 'isentropic_pressure',
name: 'Isentropic Pressure Ratio',
formula: 'p/p₀ = (1 + (γ1)/2·M²)^(γ/(γ1))',
category: 'Isentropic Flow',
variables: ['p_static', 'p0', 'M', 'gamma'],
solvers: {
p_static: v => {
const exp = v.gamma / (v.gamma - 1)
return v.p0 / Math.pow(1 + (v.gamma - 1) / 2 * v.M * v.M, exp)
},
p0: v => {
const exp = v.gamma / (v.gamma - 1)
return v.p_static * Math.pow(1 + (v.gamma - 1) / 2 * v.M * v.M, exp)
},
M: v => {
// p0/p = (1 + (γ-1)/2·M²)^(γ/(γ-1))
// (p0/p)^((γ-1)/γ) = 1 + (γ-1)/2·M²
const ratio = Math.pow(v.p0 / v.p_static, (v.gamma - 1) / v.gamma)
return Math.sqrt((ratio - 1) * 2 / (v.gamma - 1))
},
},
},
{
id: 'isentropic_density',
name: 'Isentropic Density Ratio',
formula: 'ρ/ρ₀ = (1 + (γ1)/2·M²)^(1/(γ1))',
category: 'Isentropic Flow',
variables: ['rho', 'rho0', 'M', 'gamma'],
solvers: {
rho: v => {
const exp = 1 / (v.gamma - 1)
return v.rho0 / Math.pow(1 + (v.gamma - 1) / 2 * v.M * v.M, exp)
},
rho0: v => {
const exp = 1 / (v.gamma - 1)
return v.rho * Math.pow(1 + (v.gamma - 1) / 2 * v.M * v.M, exp)
},
M: v => {
const ratio = Math.pow(v.rho0 / v.rho, v.gamma - 1)
return Math.sqrt((ratio - 1) * 2 / (v.gamma - 1))
},
},
},
{
id: 'speed_of_sound',
name: 'Speed of Sound',
formula: 'a = √(γ·R·T)',
category: 'Isentropic Flow',
variables: ['a_sound', 'gamma', 'R', 'T'],
solvers: {
a_sound: v => Math.sqrt(v.gamma * v.R * v.T),
T: v => (v.a_sound * v.a_sound) / (v.gamma * v.R),
R: v => (v.a_sound * v.a_sound) / (v.gamma * v.T),
gamma: v => (v.a_sound * v.a_sound) / (v.R * v.T),
},
},
{
id: 'flow_velocity',
name: 'Flow Velocity',
formula: 'v = M·a',
category: 'Isentropic Flow',
variables: ['v_flow', 'M', 'a_sound'],
solvers: {
v_flow: v => v.M * v.a_sound,
M: v => v.v_flow / v.a_sound,
a_sound: v => v.v_flow / v.M,
},
},
// ── Nozzle Geometry ────────────────────────────────────────────────────
{
id: 'expansion_ratio',
name: 'Nozzle Expansion Ratio',
formula: 'ε = Aₑ / Aₜ',
category: 'Nozzle Geometry',
variables: ['eps', 'Ae', 'At'],
solvers: {
eps: v => v.Ae / v.At,
Ae: v => v.eps * v.At,
At: v => v.Ae / v.eps,
},
},
{
id: 'area_ratio_mach',
name: 'Isentropic Area Ratio (supersonic)',
formula: 'ε = (1/Mₑ)·[(2/(γ+1))·(1+(γ1)/2·Mₑ²)]^((γ+1)/(2(γ1)))',
category: 'Nozzle Geometry',
variables: ['eps', 'Me', 'gamma'],
solvers: {
eps: v => areaRatioFromMach(v.Me, v.gamma),
Me: v => machFromAreaRatio(v.eps, v.gamma, true),
},
},
{
id: 'choked_mass_flow',
name: 'Choked Throat Mass Flow',
formula: 'ṁ = Aₜ·p₀·√(γ/(R·T₀))·(2/(γ+1))^((γ+1)/(2(γ1)))',
category: 'Nozzle Geometry',
variables: ['mdot', 'At', 'p0', 'gamma', 'R', 'T0'],
solvers: {
mdot: v => {
const exp = (v.gamma + 1) / (2 * (v.gamma - 1))
return v.At * v.p0 * Math.sqrt(v.gamma / (v.R * v.T0)) * Math.pow(2 / (v.gamma + 1), exp)
},
At: v => {
const exp = (v.gamma + 1) / (2 * (v.gamma - 1))
const coeff = v.p0 * Math.sqrt(v.gamma / (v.R * v.T0)) * Math.pow(2 / (v.gamma + 1), exp)
return v.mdot / coeff
},
p0: v => {
const exp = (v.gamma + 1) / (2 * (v.gamma - 1))
const coeff = v.At * Math.sqrt(v.gamma / (v.R * v.T0)) * Math.pow(2 / (v.gamma + 1), exp)
return v.mdot / coeff
},
T0: v => {
const exp = (v.gamma + 1) / (2 * (v.gamma - 1))
const coeff = v.At * v.p0 * Math.pow(2 / (v.gamma + 1), exp)
// mdot = coeff * sqrt(gamma/(R*T0))
// mdot/coeff = sqrt(gamma/(R*T0))
// (mdot/coeff)^2 = gamma/(R*T0)
// T0 = gamma/(R * (mdot/coeff)^2)
const ratio = v.mdot / coeff
return v.gamma / (v.R * ratio * ratio)
},
},
},
// ── Exit Conditions ────────────────────────────────────────────────────
// Order matters for the greedy solver: exit_pressure must come before
// exit_temperature so that Mₑ is in `known` when exit_temperature runs.
// If exit_pressure appeared after exit_temperature, the solver would
// reach exit_velocity first (with Mₑ freshly set) and use Vₑ = Isp·g₀
// to back-calculate an unphysical Tₑ > T₀.
{
id: 'exit_pressure',
name: 'Nozzle Exit Pressure',
formula: 'pₑ = p₀·(1 + (γ1)/2·Mₑ²)^(γ/(γ1))',
category: 'Nozzle Geometry',
variables: ['pe', 'p0', 'Me', 'gamma'],
solvers: {
pe: v => {
const exp = v.gamma / (v.gamma - 1)
return v.p0 / Math.pow(1 + (v.gamma - 1) / 2 * v.Me * v.Me, exp)
},
p0: v => {
const exp = v.gamma / (v.gamma - 1)
return v.pe * Math.pow(1 + (v.gamma - 1) / 2 * v.Me * v.Me, exp)
},
Me: v => {
const ratio = Math.pow(v.p0 / v.pe, (v.gamma - 1) / v.gamma)
return Math.sqrt((ratio - 1) * 2 / (v.gamma - 1))
},
},
},
{
id: 'exit_temperature',
name: 'Nozzle Exit Temperature',
formula: 'Tₑ = T₀ / (1 + (γ1)/2·Mₑ²)',
category: 'Nozzle Geometry',
variables: ['Te', 'T0', 'Me', 'gamma'],
solvers: {
Te: v => v.T0 / (1 + (v.gamma - 1) / 2 * v.Me * v.Me),
T0: v => v.Te * (1 + (v.gamma - 1) / 2 * v.Me * v.Me),
Me: v => Math.sqrt((v.T0 / v.Te - 1) * 2 / (v.gamma - 1)),
gamma: v => 1 + 2 * (v.T0 / v.Te - 1) / (v.Me * v.Me),
},
},
{
id: 'exit_velocity',
name: 'Nozzle Exit Velocity',
formula: 'Vₑ = Mₑ·√(γ·R·Tₑ)',
category: 'Nozzle Geometry',
variables: ['Ve', 'Me', 'gamma', 'R', 'Te'],
solvers: {
Ve: v => v.Me * Math.sqrt(v.gamma * v.R * v.Te),
Me: v => v.Ve / Math.sqrt(v.gamma * v.R * v.Te),
Te: v => (v.Ve / v.Me) ** 2 / (v.gamma * v.R),
R: v => (v.Ve / v.Me) ** 2 / (v.gamma * v.Te),
gamma: v => (v.Ve / v.Me) ** 2 / (v.R * v.Te),
},
},
// ── Performance ────────────────────────────────────────────────────────
{
id: 'twr',
name: 'Thrust-to-Weight Ratio',
formula: 'TWR = F / (mᵥ·g₀)',
category: 'Performance',
variables: ['TWR', 'F', 'm_vehicle', 'g0'],
solvers: {
TWR: v => v.F / (v.m_vehicle * v.g0),
F: v => v.TWR * v.m_vehicle * v.g0,
m_vehicle: v => v.F / (v.TWR * v.g0),
g0: v => v.F / (v.TWR * v.m_vehicle),
},
},
{
id: 'total_impulse',
name: 'Total Impulse',
formula: 'J = F·tᵦ',
category: 'Performance',
variables: ['J', 'F', 'tb'],
solvers: {
J: v => v.F * v.tb,
F: v => v.J / v.tb,
tb: v => v.J / v.F,
},
},
{
id: 'isp_from_impulse',
name: 'Isp from Total Impulse',
formula: 'Isp = J / (mₚ·g₀)',
category: 'Performance',
variables: ['Isp', 'J', 'mp', 'g0'],
solvers: {
Isp: v => v.J / (v.mp * v.g0),
J: v => v.Isp * v.mp * v.g0,
mp: v => v.J / (v.Isp * v.g0),
g0: v => v.J / (v.Isp * v.mp),
},
},
// ── Propellant ─────────────────────────────────────────────────────────
{
id: 'of_ratio',
name: 'Oxidiser/Fuel Ratio',
formula: 'O/F = ṁₒₓ / ṁf',
category: 'Propellant',
variables: ['OF', 'mdot_ox', 'mdot_f'],
solvers: {
OF: v => v.mdot_ox / v.mdot_f,
mdot_ox: v => v.OF * v.mdot_f,
mdot_f: v => v.mdot_ox / v.OF,
},
},
{
id: 'total_mass_flow',
name: 'Total Mass Flow',
formula: 'ṁ = ṁₒₓ + ṁf',
category: 'Propellant',
variables: ['mdot', 'mdot_ox', 'mdot_f'],
solvers: {
mdot: v => v.mdot_ox + v.mdot_f,
mdot_ox: v => v.mdot - v.mdot_f,
mdot_f: v => v.mdot - v.mdot_ox,
},
},
{
id: 'of_mass_split',
name: 'Mass Flow Split from O/F',
formula: 'ṁ_f = ṁ/(1+OF), ṁ_ox = ṁ·OF/(1+OF)',
category: 'Propellant',
variables: ['mdot', 'OF', 'mdot_f', 'mdot_ox'],
solvers: {
mdot_f: Object.assign(v => v.mdot / (1 + v.OF), { requires: () => ['mdot', 'OF'] }),
mdot_ox: Object.assign(v => v.mdot * v.OF / (1 + v.OF), { requires: () => ['mdot', 'OF'] }),
},
},
]
// Equation presets: named groups that seed the workspace with a useful set of variables
export const EQUATION_PRESETS = [
{ id: 'fundamental_thrust', label: 'Fundamental Thrust', equationIds: ['fundamental_thrust'] },
{ id: 'rocket_equation', label: 'Rocket Equation', equationIds: ['tsiolkovsky_isp', 'mass_ratio', 'propellant_mass', 'burn_time'] },
{ id: 'nozzle_full', label: 'Full Nozzle', equationIds: ['exit_pressure', 'exit_temperature', 'exit_velocity', 'area_ratio_mach', 'choked_mass_flow', 'thrust_coefficient', 'cstar_def'] },
{ id: 'isentropic', label: 'Isentropic Flow', equationIds: ['isentropic_temp', 'isentropic_pressure', 'speed_of_sound', 'flow_velocity'] },
{ id: 'performance', label: 'Performance', equationIds: ['twr', 'total_impulse', 'isp_from_impulse', 'isp_from_thrust'] },
{ id: 'propellant', label: 'Propellant Mix', equationIds: ['of_ratio', 'total_mass_flow', 'burn_time'] },
]

106
src/engine/exportImport.js Normal file
View File

@@ -0,0 +1,106 @@
import { VARIABLES } from './variables.js'
import { formatValue } from './format.js'
/**
* Assemble a plain object describing the current workspace state,
* used for both ODT generation and JSON export.
*/
export function buildExportData(workspaceVarIds, userValues, solved, unitSelections, getUnit, sciNotation) {
const given = Object.entries(userValues).map(([varId, siVal]) => {
const v = VARIABLES[varId]
const unit = getUnit(varId)
return {
symbol: v?.symbol ?? varId,
name: v?.name ?? varId,
value: formatValue(unit.fromSI(siVal), sciNotation),
unit: unit.label,
}
})
const solvedEntries = Object.entries(solved).map(([varId, info]) => {
const v = VARIABLES[varId]
const unit = getUnit(varId)
return {
symbol: v?.symbol ?? varId,
name: v?.name ?? varId,
value: formatValue(unit.fromSI(info.value), sciNotation),
unit: unit.label,
via: info.equationName ?? '',
}
})
const unsolved = workspaceVarIds
.filter(id => !(id in userValues) && !(id in solved))
.map(id => {
const v = VARIABLES[id]
return { symbol: v?.symbol ?? id, name: v?.name ?? id }
})
return { given, solved: solvedEntries, unsolved }
}
/**
* Produce a JSON Blob for download.
* Schema version 1: workspace section is re-imported; results section is reference-only.
*/
export function exportJSON(exportData, workspaceVarIds, userValues, unitSelections) {
const payload = {
version: 1,
exportedAt: new Date().toISOString(),
workspace: {
variableIds: workspaceVarIds,
userValues,
unitSelections,
},
results: {
given: exportData.given,
solved: exportData.solved,
unsolved: exportData.unsolved,
},
}
return new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json' })
}
/**
* Parse an imported JSON string and return only the workspace section.
* Throws a descriptive Error if the file is invalid.
*/
export function parseImport(jsonString) {
let data
try {
data = JSON.parse(jsonString)
} catch {
throw new Error('File is not valid JSON.')
}
if (data.version !== 1) {
throw new Error(`Unsupported export version: ${data.version}`)
}
const { variableIds, userValues, unitSelections } = data.workspace ?? {}
if (!Array.isArray(variableIds)) {
throw new Error('Import file is missing workspace.variableIds.')
}
if (typeof userValues !== 'object' || userValues === null) {
throw new Error('Import file is missing workspace.userValues.')
}
return {
variableIds,
userValues,
unitSelections: unitSelections ?? {},
}
}
/**
* Trigger a browser file download.
*/
export function downloadBlob(blob, filename) {
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = filename
a.click()
URL.revokeObjectURL(url)
}

168
src/engine/exportOdt.js Normal file
View File

@@ -0,0 +1,168 @@
import JSZip from 'jszip'
/* ── ODT XML helpers ────────────────────────────────────────────────── */
function xmlEscape(str) {
return String(str)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
}
function cell(text, styleName = 'TableCell') {
return `<table:table-cell table:style-name="${styleName}" office:value-type="string">` +
`<text:p text:style-name="TableContents">${xmlEscape(text)}</text:p>` +
`</table:table-cell>`
}
function headerCell(text) {
return `<table:table-cell table:style-name="TableHeaderCell" office:value-type="string">` +
`<text:p text:style-name="TableHeader">${xmlEscape(text)}</text:p>` +
`</table:table-cell>`
}
function row(...cells) {
return `<table:table-row>${cells.join('')}</table:table-row>`
}
function table(name, columnCount, rows) {
const cols = Array(columnCount).fill(
`<table:table-column table:style-name="TableColumn"/>`
).join('')
return `<table:table table:name="${name}" table:style-name="Table">${cols}${rows}</table:table>`
}
function heading(text, level = 1) {
return `<text:h text:style-name="Heading${level}" text:outline-level="${level}">${xmlEscape(text)}</text:h>`
}
function para(text = '') {
return `<text:p text:style-name="Standard">${xmlEscape(text)}</text:p>`
}
/* ── Styles XML ─────────────────────────────────────────────────────── */
const STYLES_XML = `<?xml version="1.0" encoding="UTF-8"?>
<office:document-styles
xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0"
xmlns:style="urn:oasis:names:tc:opendocument:xmlns:style:1.0"
xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0"
xmlns:fo="urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0"
xmlns:table="urn:oasis:names:tc:opendocument:xmlns:table:1.0"
office:version="1.3">
<office:styles>
<style:style style:name="Standard" style:family="paragraph" style:class="text"/>
<style:style style:name="Heading1" style:family="paragraph" style:class="text" style:parent-style-name="Standard">
<style:text-properties fo:font-size="16pt" fo:font-weight="bold"/>
</style:style>
<style:style style:name="Heading2" style:family="paragraph" style:class="text" style:parent-style-name="Standard">
<style:text-properties fo:font-size="13pt" fo:font-weight="bold"/>
<style:paragraph-properties fo:margin-top="6pt" fo:margin-bottom="3pt"/>
</style:style>
<style:style style:name="TableContents" style:family="paragraph" style:parent-style-name="Standard">
<style:paragraph-properties fo:padding="1mm"/>
</style:style>
<style:style style:name="TableHeader" style:family="paragraph" style:parent-style-name="Standard">
<style:text-properties fo:font-weight="bold"/>
<style:paragraph-properties fo:padding="1mm"/>
</style:style>
</office:styles>
</office:document-styles>`
/* ── Automatic styles for content.xml ───────────────────────────────── */
const AUTO_STYLES = `
<style:style style:name="Table" style:family="table">
<style:table-properties style:width="16cm" fo:margin-bottom="6mm"/>
</style:style>
<style:style style:name="TableColumn" style:family="table-column">
<style:table-column-properties style:column-width="4cm"/>
</style:style>
<style:style style:name="TableCell" style:family="table-cell">
<style:table-cell-properties fo:border="0.05pt solid #888888" fo:padding="1.5mm"/>
</style:style>
<style:style style:name="TableHeaderCell" style:family="table-cell">
<style:table-cell-properties fo:border="0.05pt solid #888888" fo:padding="1.5mm" fo:background-color="#e8e8e8"/>
</style:style>`
/* ── content.xml builder ────────────────────────────────────────────── */
function buildContentXml(exportData, exportedAt) {
const { given, solved, unsolved } = exportData
const givenTable = table('GivenValues', 4,
row(headerCell('Symbol'), headerCell('Name'), headerCell('Value'), headerCell('Unit')) +
given.map(r => row(cell(r.symbol), cell(r.name), cell(r.value), cell(r.unit))).join('')
)
const solvedTable = table('SolvedValues', 5,
row(headerCell('Symbol'), headerCell('Name'), headerCell('Value'), headerCell('Unit'), headerCell('Solved via')) +
solved.map(r => row(cell(r.symbol), cell(r.name), cell(r.value), cell(r.unit), cell(r.via))).join('')
)
const unsolvedSection = unsolved.length === 0 ? '' :
heading('Unsolved Variables', 2) +
table('UnsolvedVars', 2,
row(headerCell('Symbol'), headerCell('Name')) +
unsolved.map(r => row(cell(r.symbol), cell(r.name))).join('')
)
const body = [
heading('Rocketry Workspace Export'),
para(`Exported: ${exportedAt}`),
para(),
heading('Given Values', 2),
given.length > 0 ? givenTable : para('(none)'),
para(),
heading('Solved Values', 2),
solved.length > 0 ? solvedTable : para('(none)'),
para(),
unsolvedSection,
].join('\n')
return `<?xml version="1.0" encoding="UTF-8"?>
<office:document-content
xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0"
xmlns:style="urn:oasis:names:tc:opendocument:xmlns:style:1.0"
xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0"
xmlns:table="urn:oasis:names:tc:opendocument:xmlns:table:1.0"
xmlns:fo="urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0"
office:version="1.3">
<office:automatic-styles>${AUTO_STYLES}</office:automatic-styles>
<office:body>
<office:text>
${body}
</office:text>
</office:body>
</office:document-content>`
}
const MANIFEST_XML = `<?xml version="1.0" encoding="UTF-8"?>
<manifest:manifest
xmlns:manifest="urn:oasis:names:tc:opendocument:xmlns:manifest:1.0"
manifest:version="1.3">
<manifest:file-entry manifest:full-path="/" manifest:media-type="application/vnd.oasis.opendocument.text"/>
<manifest:file-entry manifest:full-path="content.xml" manifest:media-type="text/xml"/>
<manifest:file-entry manifest:full-path="styles.xml" manifest:media-type="text/xml"/>
</manifest:manifest>`
/* ── Public API ─────────────────────────────────────────────────────── */
/**
* Build an ODT Blob from the assembled export data.
* @param {object} exportData - output of buildExportData()
* @returns {Promise<Blob>}
*/
export async function generateOdt(exportData) {
const exportedAt = new Date().toISOString()
const zip = new JSZip()
// mimetype MUST be the first file and stored uncompressed
zip.file('mimetype', 'application/vnd.oasis.opendocument.text', { compression: 'STORE' })
zip.file('META-INF/manifest.xml', MANIFEST_XML)
zip.file('styles.xml', STYLES_XML)
zip.file('content.xml', buildContentXml(exportData, exportedAt))
return zip.generateAsync({ type: 'blob', mimeType: 'application/vnd.oasis.opendocument.text' })
}

10
src/engine/format.js Normal file
View File

@@ -0,0 +1,10 @@
export function formatValue(v, sciNotation = false) {
if (v === undefined || v === null) return '—'
if (!isFinite(v)) return '∞'
if (v === 0) return '0'
if (sciNotation) return v.toExponential(4)
if (Math.abs(v) >= 1e6 || (Math.abs(v) < 0.001 && v !== 0)) {
return v.toExponential(4)
}
return parseFloat(v.toPrecision(6)).toString()
}

View File

@@ -0,0 +1,754 @@
// Knowledgebase: individual substances (fuels, oxidisers, monopropellants)
// and bipropellant combination performance data.
//
// COMBINATIONS also serve as the single source of truth for the solver's
// PropellantModal — PROPELLANTS and PROPELLANT_TYPES are derived below.
export const SUBSTANCES = [
// ── Fuels ───────────────────────────────────────────────────────────────
{
id: 'lh2',
name: 'Liquid Hydrogen',
symbol: 'LH₂',
formula: 'H₂',
role: 'fuel',
subcategory: 'Cryogenic',
description:
'Liquid hydrogen is the highest-performance liquid rocket fuel, prized for its exceptionally low molecular weight combustion products. It must be stored at 253 °C (just 20 K above absolute zero), imposing strict insulation and boil-off management requirements. Despite its low density, its combination with LOX delivers the highest specific impulse of any practical bipropellant.',
density: 71,
densityNote: 'at 253 °C, 1 atm',
boilingPoint: -253,
meltingPoint: -259,
storagePressure: '1 atm (cryogenic dewar)',
molecularWeight: 2.016,
autoignitionTemp: 500,
flammabilityRange: '475 vol% in air',
hazards: ['Extreme cryogen (253 °C)', 'Highly flammable', 'Wide flammability range', 'Asphyxiant in confined spaces'],
toxicity: 'Non-toxic',
engineExamples: ['RL-10', 'J-2', 'RS-68', 'Vulcain 2', 'LE-7A', 'HM7B'],
catalyticIsp: null,
compatibleWith: ['lox'],
},
{
id: 'rp1',
name: 'RP-1 (Kerosene)',
symbol: 'RP-1',
formula: 'C₁₂H₂₄ (avg)',
role: 'fuel',
subcategory: 'Storable',
description:
'Rocket Propellant-1 is a highly refined kerosene used as a rocket fuel. It is a complex mixture of hydrocarbons with good energy density, low toxicity relative to hydrazine fuels, and ambient storage conditions. Its high density makes it attractive for volume-constrained vehicles. It is one of the most widely used liquid rocket fuels in history.',
density: 820,
densityNote: 'at 20 °C',
boilingPoint: 175,
meltingPoint: -40,
storagePressure: 'Ambient',
molecularWeight: 170,
autoignitionTemp: 220,
flammabilityRange: '0.64.7 vol% in air',
hazards: ['Flammable liquid', 'Aspiration hazard', 'Skin irritant'],
toxicity: 'Low acute toxicity; mildly irritating',
engineExamples: ['Merlin', 'RD-180', 'F-1', 'NK-33', 'Rutherford', 'Gamma'],
catalyticIsp: null,
compatibleWith: ['lox', 'htp'],
},
{
id: 'lch4',
name: 'Liquid Methane',
symbol: 'LCH₄',
formula: 'CH₄',
role: 'fuel',
subcategory: 'Cryogenic',
description:
'Liquid methane is a cryogenic fuel that has gained prominence for reusable rocket applications. It offers a better Isp than RP-1, cleaner combustion (less coking), and is compatible with deep-throttling engines. It can in principle be produced on Mars via the Sabatier reaction, making it attractive for interplanetary missions.',
density: 424,
densityNote: 'at 162 °C, 1 atm',
boilingPoint: -162,
meltingPoint: -182,
storagePressure: '1 atm (cryogenic) or ~3 bar (semi-cryo)',
molecularWeight: 16.04,
autoignitionTemp: 537,
flammabilityRange: '515 vol% in air',
hazards: ['Cryogen (162 °C)', 'Highly flammable', 'Asphyxiant'],
toxicity: 'Non-toxic; simple asphyxiant',
engineExamples: ['Raptor', 'BE-4', 'Prometheus', 'Zephyr'],
catalyticIsp: null,
compatibleWith: ['lox'],
},
{
id: 'lc2h6',
name: 'Liquid Ethane',
symbol: 'LC₂H₆',
formula: 'C₂H₆',
role: 'fuel',
subcategory: 'Cryogenic',
description:
'Liquid ethane is a cryogenic hydrocarbon fuel investigated as an alternative to methane. Its slightly higher density and carbon content give marginally better volumetric performance. It has been researched for reusable applications and has similar handling requirements to LCH₄.',
density: 544,
densityNote: 'at 89 °C, 1 atm',
boilingPoint: -89,
meltingPoint: -183,
storagePressure: '1 atm (cryogenic)',
molecularWeight: 30.07,
autoignitionTemp: 472,
flammabilityRange: '312.5 vol% in air',
hazards: ['Cryogen (89 °C)', 'Flammable', 'Asphyxiant'],
toxicity: 'Non-toxic; simple asphyxiant',
engineExamples: ['Research engines (Korolev cross-feed studies)'],
catalyticIsp: null,
compatibleWith: ['lox'],
},
{
id: 'ethanol',
name: 'Ethanol',
symbol: 'EtOH',
formula: 'C₂H₅OH',
role: 'fuel',
subcategory: 'Storable',
description:
'Ethanol (typically used as 75% aqueous solution) was one of the earliest liquid rocket fuels, used in the German V-2. Today it is popular in amateur and experimental rocketry, especially with nitrous oxide as the oxidiser. The water content helps cool the combustion chamber and reduces flame temperature.',
density: 789,
densityNote: 'at 20 °C (anhydrous); 75% solution: ~870 kg/m³',
boilingPoint: 78,
meltingPoint: -114,
storagePressure: 'Ambient',
molecularWeight: 46.07,
autoignitionTemp: 365,
flammabilityRange: '3.319 vol% in air',
hazards: ['Flammable liquid', 'Vapour heavier than air'],
toxicity: 'Low acute toxicity; CNS depressant at high concentrations',
engineExamples: ['A-4 (V-2)', 'Amateur N₂O motors'],
catalyticIsp: null,
compatibleWith: ['n2o'],
},
{
id: 'udmh',
name: 'UDMH',
symbol: 'UDMH',
formula: '(CH₃)₂N₂H₂',
role: 'fuel',
subcategory: 'Hypergolic',
description:
'Unsymmetrical dimethylhydrazine is a storable, hypergolic fuel that ignites on contact with nitrogen tetroxide. Its storability (liquid at ambient temperatures) and instant ignition make it ideal for spacecraft and missile applications requiring reliable restart. It is highly toxic and carcinogenic.',
density: 791,
densityNote: 'at 20 °C',
boilingPoint: 63,
meltingPoint: -57,
storagePressure: 'Ambient (sealed)',
molecularWeight: 60.10,
autoignitionTemp: null,
flammabilityRange: '295 vol% in air',
hazards: ['Highly toxic', 'Carcinogenic', 'Hypergolic with N₂O₄', 'Flammable', 'Corrosive vapour'],
toxicity: 'Highly toxic; probable human carcinogen (IARC Group 2A)',
engineExamples: ['RD-253 (Proton)', 'YF-20 (Long March)', 'Viking (Ariane 14)'],
catalyticIsp: null,
compatibleWith: ['n2o4'],
},
{
id: 'mmh',
name: 'MMH',
symbol: 'MMH',
formula: 'CH₃N₂H₃',
role: 'fuel',
subcategory: 'Hypergolic',
description:
'Monomethylhydrazine is the premier storable spacecraft propellant when used with nitrogen tetroxide. It has been the workhorse of spacecraft propulsion systems for decades, prized for its wide liquid range, reliable hypergolic ignition, and good performance. Like all hydrazines, it is highly toxic.',
density: 874,
densityNote: 'at 20 °C',
boilingPoint: 87,
meltingPoint: -52,
storagePressure: 'Ambient (sealed)',
molecularWeight: 46.07,
autoignitionTemp: null,
flammabilityRange: '2.598 vol% in air',
hazards: ['Highly toxic', 'Carcinogenic', 'Hypergolic with N₂O₄', 'Flammable'],
toxicity: 'Highly toxic; probable human carcinogen',
engineExamples: ['Shuttle OMS/RCS', 'Orion SM', 'AJ10', 'R-4D', 'Aestus'],
catalyticIsp: null,
compatibleWith: ['n2o4'],
},
{
id: 'aerozine50',
name: 'Aerozine-50',
symbol: 'A-50',
formula: '50% UDMH + 50% N₂H₄',
role: 'fuel',
subcategory: 'Hypergolic',
description:
'Aerozine-50 is a 50/50 blend of UDMH and hydrazine developed for the Titan II missile. Compared to pure UDMH it has higher energy content; compared to pure hydrazine it has better thermal stability and lower freezing point. It ignites hypergolically with N₂O₄ and was used in the Apollo Lunar Module descent engine.',
density: 900,
densityNote: 'at 20 °C',
boilingPoint: 70,
meltingPoint: -57,
storagePressure: 'Ambient (sealed)',
molecularWeight: 53,
autoignitionTemp: null,
flammabilityRange: '299 vol% in air',
hazards: ['Highly toxic', 'Carcinogenic', 'Hypergolic with N₂O₄', 'Flammable'],
toxicity: 'Highly toxic; carcinogenic',
engineExamples: ['Titan II/III/IV', 'Lunar Module DPS', 'Delta II (second stage)', 'TR-201'],
catalyticIsp: null,
compatibleWith: ['n2o4'],
},
{
id: 'htpb',
name: 'HTPB',
symbol: 'HTPB',
formula: '(C₄H₆)ₙ (polymer)',
role: 'fuel',
subcategory: 'Hybrid',
description:
'Hydroxyl-terminated polybutadiene is a solid rubber binder used as both structural matrix and fuel in hybrid and solid rocket motors. In hybrid engines the solid grain regresses as oxidiser flows over it. HTPB has good mechanical properties, low toxicity during handling (before combustion), and is highly versatile.',
density: 920,
densityNote: 'cured solid at 20 °C',
boilingPoint: null,
meltingPoint: null,
storagePressure: 'N/A (solid)',
molecularWeight: null,
autoignitionTemp: 320,
flammabilityRange: 'N/A (solid)',
hazards: ['Combustible solid', 'Combustion products may be toxic'],
toxicity: 'Low toxicity as-cast; combustion gases are hazardous',
engineExamples: ['SpaceShipOne/Two (N₂O hybrid)', 'SRM binders (Space Shuttle SRB)', 'NAMMO hybrid'],
catalyticIsp: null,
compatibleWith: ['n2o', 'lox', 'ap'],
},
{
id: 'sucrose',
name: 'Sucrose',
symbol: 'Sucrose',
formula: 'C₁₂H₂₂O₁₁',
role: 'fuel',
subcategory: 'Solid',
description:
'Sucrose (table sugar) combined with potassium nitrate forms KNSU, a popular amateur "candy" propellant. The mixture is cast or pressed and burns to produce a vigorous thrust with modest specific impulse. It is widely used in educational and amateur rocketry due to its low cost and ease of manufacture.',
density: 1590,
densityNote: 'at 20 °C (crystal)',
boilingPoint: null,
meltingPoint: 186,
storagePressure: 'N/A (solid)',
molecularWeight: 342.30,
autoignitionTemp: 370,
flammabilityRange: 'N/A (combustible solid)',
hazards: ['Combustible solid when mixed with oxidiser', 'Dust explosion risk'],
toxicity: 'Non-toxic',
engineExamples: ['Amateur KNSU motors', 'Educational rocket motors'],
catalyticIsp: null,
compatibleWith: ['kno3'],
},
{
id: 'dextrose',
name: 'Dextrose',
symbol: 'Dextrose',
formula: 'C₆H₁₂O₆',
role: 'fuel',
subcategory: 'Solid',
description:
'Dextrose (glucose) is an alternative to sucrose in candy propellants, forming KNDX with potassium nitrate. It produces a slightly lower flame temperature than KNSU and is similarly accessible. KNDX is popular among amateur rocketeers seeking an alternative binder chemistry.',
density: 1540,
densityNote: 'at 20 °C',
boilingPoint: null,
meltingPoint: 146,
storagePressure: 'N/A (solid)',
molecularWeight: 180.16,
autoignitionTemp: 390,
flammabilityRange: 'N/A (combustible solid)',
hazards: ['Combustible solid when mixed with oxidiser', 'Dust explosion risk'],
toxicity: 'Non-toxic',
engineExamples: ['Amateur KNDX motors'],
catalyticIsp: null,
compatibleWith: ['kno3'],
},
// ── Oxidisers ───────────────────────────────────────────────────────────
{
id: 'lox',
name: 'Liquid Oxygen',
symbol: 'LOX',
formula: 'O₂',
role: 'oxidiser',
subcategory: 'Cryogenic',
description:
'Liquid oxygen is the most commonly used rocket oxidiser, offering high performance, non-toxicity, and relatively affordable cost. Its cryogenic nature (183 °C) requires insulated tanks and introduces boil-off losses. LOX is compatible with nearly all hydrocarbon fuels and hydrogen, making it the foundation of most high-performance launch vehicles.',
density: 1141,
densityNote: 'at 183 °C, 1 atm',
boilingPoint: -183,
meltingPoint: -219,
storagePressure: '1 atm (cryogenic dewar)',
molecularWeight: 32.00,
autoignitionTemp: null,
flammabilityRange: 'N/A (oxidiser; powerfully supports combustion)',
hazards: ['Cryogen (183 °C)', 'Powerful oxidiser', 'Explosive with hydrocarbons under pressure', 'Causes fire on contact with organic materials'],
toxicity: 'Non-toxic; high concentrations are physiologically dangerous',
engineExamples: ['Merlin', 'Raptor', 'RL-10', 'RD-180', 'BE-4', 'RS-68', 'J-2'],
catalyticIsp: null,
compatibleWith: ['lh2', 'rp1', 'lch4', 'lc2h6', 'htpb'],
},
{
id: 'n2o4',
name: 'Nitrogen Tetroxide',
symbol: 'N₂O₄',
formula: 'N₂O₄',
role: 'oxidiser',
subcategory: 'Hypergolic',
description:
'Nitrogen tetroxide is the standard storable oxidiser for military and spacecraft propulsion. It ignites hypergolically with hydrazine-based fuels, enabling reliable engine restarts without an ignition system. NTO is stored as a liquid at ambient temperature but is highly corrosive, toxic, and a strong oxidiser requiring careful materials compatibility.',
density: 1440,
densityNote: 'at 20 °C',
boilingPoint: 21,
meltingPoint: -11,
storagePressure: 'Ambient (sealed, slight pressure)',
molecularWeight: 92.01,
autoignitionTemp: null,
flammabilityRange: 'N/A (oxidiser)',
hazards: ['Highly toxic (IDLH 20 ppm)', 'Hypergolic with hydrazines', 'Strong oxidiser', 'Corrosive', 'Reddish-brown toxic fumes (NO₂)'],
toxicity: 'Highly toxic; IDLH 20 ppm; causes severe pulmonary oedema',
engineExamples: ['Shuttle OMS/RCS', 'Titan II/IV', 'Proton (second/third stage)', 'AJ10', 'Viking'],
catalyticIsp: null,
compatibleWith: ['udmh', 'mmh', 'aerozine50'],
},
{
id: 'htp',
name: 'High-Test Peroxide (HTP)',
symbol: 'HTP',
formula: 'H₂O₂ (≥90%)',
role: 'oxidiser',
subcategory: 'Green',
description:
'High-test peroxide is concentrated hydrogen peroxide (≥90%) used as a relatively "green" oxidiser. It decomposes exothermically over a silver or platinum catalyst to produce steam and oxygen, which then supports combustion of a fuel. HTP is significantly less toxic than NTO and has heritage in early British rocketry (Gamma engine) and amateur systems.',
density: 1390,
densityNote: 'at 20 °C (90%)',
boilingPoint: 114,
meltingPoint: -12,
storagePressure: 'Ambient (vented)',
molecularWeight: 34.01,
autoignitionTemp: null,
flammabilityRange: 'N/A (oxidiser)',
hazards: ['Strong oxidiser', 'Corrosive to skin/eyes', 'Decomposes explosively if contaminated', 'Fire risk with organics'],
toxicity: 'Moderate; concentrated solutions cause severe burns',
engineExamples: ['Gamma (Black Arrow)', 'Spectre', 'Amateur HTP/RP-1 engines', 'Bloodhound LSR'],
catalyticIsp: null,
compatibleWith: ['rp1'],
},
{
id: 'n2o',
name: 'Nitrous Oxide',
symbol: 'N₂O',
formula: 'N₂O',
role: 'oxidiser',
subcategory: 'Green',
description:
'Nitrous oxide is a self-pressurising liquid oxidiser popular in amateur and hybrid rocketry. Its vapour pressure (~5 MPa at 20 °C) eliminates the need for separate pressurisation systems. N₂O is relatively non-toxic and easy to handle compared to NTO or HTP, though it is a potent greenhouse gas and can detonate under specific conditions.',
density: 770,
densityNote: 'liquid at 20 °C, ~5 MPa',
boilingPoint: -88,
meltingPoint: -91,
storagePressure: '~5 MPa at 20 °C (self-pressurising)',
molecularWeight: 44.01,
autoignitionTemp: null,
flammabilityRange: 'N/A (oxidiser; non-flammable)',
hazards: ['Strong oxidiser', 'Detonation risk at high pressure/temperature', 'Asphyxiant', 'Greenhouse gas'],
toxicity: 'Low acute toxicity; can cause hypoxia in high concentrations',
engineExamples: ['SpaceShipOne/Two', 'Amateur hybrid motors', 'N₂O/ethanol experimental engines'],
catalyticIsp: null,
compatibleWith: ['ethanol', 'htpb'],
},
{
id: 'ap',
name: 'Ammonium Perchlorate',
symbol: 'AP',
formula: 'NH₄ClO₄',
role: 'oxidiser',
subcategory: 'Solid',
description:
'Ammonium perchlorate is the primary oxidiser in composite solid propellants (APCP). It is mixed with HTPB binder and aluminium powder to form the propellant grain. AP is an energetic solid that detonates under severe shock but is stable under normal processing conditions. It is the oxidiser in virtually all modern high-performance solid rocket motors.',
density: 1950,
densityNote: 'crystal at 20 °C',
boilingPoint: null,
meltingPoint: 240,
storagePressure: 'N/A (solid)',
molecularWeight: 117.49,
autoignitionTemp: 420,
flammabilityRange: 'N/A (solid oxidiser)',
hazards: ['Explosive oxidiser', 'Toxic chlorine-containing combustion products (HCl)', 'Skin/eye irritant', 'Environmental: thyroid disruptor'],
toxicity: 'Moderate; thyroid disruption with chronic exposure; combustion products (HCl) are toxic',
engineExamples: ['Space Shuttle SRB', 'Ariane 5 P230', 'Hobby/high-power APCP motors', 'Ares I/V SRBs'],
catalyticIsp: null,
compatibleWith: ['htpb'],
},
{
id: 'kno3',
name: 'Potassium Nitrate',
symbol: 'KNO₃',
formula: 'KNO₃',
role: 'oxidiser',
subcategory: 'Solid',
description:
'Potassium nitrate (saltpetre) is one of the oldest known oxidisers, the basis of black powder and candy propellants. In KNSU and KNDX it is combined with sucrose or dextrose to make an accessible, low-cost propellant for educational and amateur rocketry. Performance is modest but manufacturing complexity is low.',
density: 2109,
densityNote: 'crystal at 20 °C',
boilingPoint: 400,
meltingPoint: 334,
storagePressure: 'N/A (solid)',
molecularWeight: 101.10,
autoignitionTemp: null,
flammabilityRange: 'N/A (solid oxidiser)',
hazards: ['Oxidiser; accelerates combustion', 'Explosive when mixed with fuels and subjected to shock/heat', 'Irritant'],
toxicity: 'Low acute toxicity; irritant at high concentrations',
engineExamples: ['Amateur KNSU/KNDX candy motors', 'Black powder igniters'],
catalyticIsp: null,
compatibleWith: ['sucrose', 'dextrose'],
},
// ── Monopropellants ─────────────────────────────────────────────────────
{
id: 'n2h4_mono',
name: 'Hydrazine',
symbol: 'N₂H₄',
formula: 'N₂H₄',
role: 'monopropellant',
subcategory: 'Storable',
description:
'Hydrazine is the most widely used monopropellant in spacecraft propulsion. It decomposes exothermically over an iridium/alumina catalyst (Shell 405) to produce hot nitrogen, hydrogen, and ammonia gases. It offers high heritage, good Isp for a monopropellant, and reliable restart capability. Its high toxicity is its primary drawback, driving the search for green replacements.',
density: 1004,
densityNote: 'at 20 °C',
boilingPoint: 114,
meltingPoint: 2,
storagePressure: 'Ambient (sealed, slight positive pressure)',
molecularWeight: 32.05,
autoignitionTemp: 270,
flammabilityRange: '4.7100 vol% in air',
hazards: ['Highly toxic (IDLH 50 ppm)', 'Carcinogenic', 'Flammable', 'Reactive with many metals', 'Hypergolic with some oxidisers'],
toxicity: 'Highly toxic and probable human carcinogen (IARC Group 2A)',
engineExamples: ['MR-103 (many NASA/ESA spacecraft)', 'MONARC', 'Aerojet MR-80'],
catalyticIsp: 230,
// Thermodynamic data for solver (catalytic decomposition)
gamma: 1.27,
R: 376,
T0: 1000,
compatibleWith: [],
},
{
id: 'htp_mono',
name: 'HTP Monopropellant',
symbol: 'HTP',
formula: 'H₂O₂ (≥90%)',
role: 'monopropellant',
subcategory: 'Green',
description:
'High-test peroxide can be used as a monopropellant by catalytic decomposition over silver or platinum, producing steam and oxygen. Its Isp is lower than hydrazine but it is far less toxic, making it an attractive green alternative for attitude control systems and small thrusters in CubeSats and small satellites.',
density: 1390,
densityNote: 'at 20 °C (90%)',
boilingPoint: 114,
meltingPoint: -12,
storagePressure: 'Ambient (vented)',
molecularWeight: 34.01,
autoignitionTemp: null,
flammabilityRange: 'N/A (oxidiser/monoprop)',
hazards: ['Strong oxidiser', 'Corrosive', 'Decomposition can be violent if contaminated'],
toxicity: 'Moderate; corrosive to skin and eyes at high concentration',
engineExamples: ['Rutherford (gas gen.)', 'CubeSat monoprop thrusters', 'Bloodhound LSR'],
catalyticIsp: 185,
gamma: 1.30,
R: 350,
T0: 1030,
compatibleWith: [],
},
{
id: 'han',
name: 'HAN-based (AF-M315E / LMP-103S)',
symbol: 'HAN',
formula: 'NH₃OHNO₃ blend',
role: 'monopropellant',
subcategory: 'Green',
description:
'Hydroxylammonium nitrate (HAN) based ionic liquid monopropellants are the leading green replacements for hydrazine. AF-M315E (ASCENT) and LMP-103S (HPGP) deliver Isp values significantly higher than hydrazine while being far less toxic. They require higher catalyst pre-heat temperatures (~300 °C) than hydrazine but offer reduced handling hazards and regulatory burden.',
density: 1460,
densityNote: 'at 20 °C (AF-M315E)',
boilingPoint: null,
meltingPoint: -80,
storagePressure: 'Ambient (sealed)',
molecularWeight: null,
autoignitionTemp: null,
flammabilityRange: 'N/A (ionic liquid)',
hazards: ['Moderately toxic', 'Energetic; avoid contamination', 'Skin/eye irritant'],
toxicity: 'Moderately toxic; significantly safer than hydrazine',
engineExamples: ['GPIM (NASA)', 'Lunar Flashlight', 'Many commercial CubeSat thrusters'],
catalyticIsp: 250,
gamma: 1.25,
R: 340,
T0: 1930,
compatibleWith: [],
},
]
// ── Bipropellant combination performance data ────────────────────────────────
// Values at ~6.9 MPa chamber pressure unless noted.
// gamma and R feed directly into the solver (variable IDs match).
// T0 = flameTemp, OF = optimalOF for solver use.
export const COMBINATIONS = [
{
fuelId: 'lh2',
oxidiserId: 'lox',
isHypergolic: false,
vacuumIsp: 455,
flameTemp: 3250,
optimalOF: 6.0,
gamma: 1.21,
R: 934,
chamberPressureRef: '6.9 MPa',
expansionRatioRef: 40,
energeticCategory: 'Cryogenic Bipropellant',
notes: 'Highest Isp liquid bipropellant. Extremely demanding cryogenic handling for both propellants.',
engines: ['RL-10', 'J-2', 'RS-68', 'Vulcain 2', 'LE-7A', 'HM7B'],
},
{
fuelId: 'rp1',
oxidiserId: 'lox',
isHypergolic: false,
vacuumIsp: 363,
flameTemp: 3570,
optimalOF: 2.6,
gamma: 1.24,
R: 361,
chamberPressureRef: '6.9 MPa',
expansionRatioRef: 40,
energeticCategory: 'Cryogenic Bipropellant',
notes: 'High density, high performance. Proven across dozens of vehicles and engines over 60+ years.',
engines: ['Merlin', 'RD-180', 'F-1', 'NK-33', 'Rutherford'],
},
{
fuelId: 'lch4',
oxidiserId: 'lox',
isHypergolic: false,
vacuumIsp: 380,
flameTemp: 3500,
optimalOF: 3.5,
gamma: 1.20,
R: 416,
chamberPressureRef: '6.9 MPa',
expansionRatioRef: 40,
energeticCategory: 'Cryogenic Bipropellant',
notes: 'Reusable-friendly; clean combustion reduces coking. Potential ISRU on Mars.',
engines: ['Raptor', 'BE-4', 'Prometheus'],
},
{
fuelId: 'lc2h6',
oxidiserId: 'lox',
isHypergolic: false,
vacuumIsp: 375,
flameTemp: 3480,
optimalOF: 3.2,
gamma: 1.21,
R: 390,
chamberPressureRef: '6.9 MPa',
expansionRatioRef: 40,
energeticCategory: 'Cryogenic Bipropellant',
notes: 'Higher density than methane with comparable performance. Primarily in research.',
engines: ['Research engines'],
},
{
fuelId: 'udmh',
oxidiserId: 'n2o4',
isHypergolic: true,
vacuumIsp: 340,
flameTemp: 3040,
optimalOF: 2.6,
gamma: 1.25,
R: 320,
chamberPressureRef: '6.9 MPa',
expansionRatioRef: 40,
energeticCategory: 'Storable Bipropellant',
notes: 'Hypergolic storable combination. Highly toxic but reliable; widely used in Russian and Chinese vehicles.',
engines: ['RD-253 (Proton)', 'YF-20', 'Viking (Ariane 14)'],
},
{
fuelId: 'mmh',
oxidiserId: 'n2o4',
isHypergolic: true,
vacuumIsp: 341,
flameTemp: 3000,
optimalOF: 1.65,
gamma: 1.25,
R: 318,
chamberPressureRef: '6.9 MPa',
expansionRatioRef: 40,
energeticCategory: 'Storable Bipropellant',
notes: 'Primary spacecraft upper-stage and RCS propellant. Decades of heritage.',
engines: ['Shuttle OMS/RCS', 'Orion SM', 'AJ10', 'R-4D', 'Aestus'],
},
{
fuelId: 'aerozine50',
oxidiserId: 'n2o4',
isHypergolic: true,
vacuumIsp: 342,
flameTemp: 3070,
optimalOF: 1.9,
gamma: 1.25,
R: 316,
chamberPressureRef: '6.9 MPa',
expansionRatioRef: 40,
energeticCategory: 'Storable Bipropellant',
notes: 'Better stability than pure hydrazine. Used in Titan launch vehicles and the Apollo Lunar Module.',
engines: ['Titan II/III/IV', 'Lunar Module DPS', 'TR-201'],
},
{
fuelId: 'rp1',
oxidiserId: 'htp',
isHypergolic: false,
vacuumIsp: 328,
flameTemp: 2900,
optimalOF: 7.0,
gamma: 1.26,
R: 285,
chamberPressureRef: '2 MPa',
expansionRatioRef: 40,
energeticCategory: 'Pressure-fed Bipropellant',
notes: 'Relatively non-toxic combination. HTP decomposes catalytically before combustion.',
engines: ['Gamma (Black Arrow)', 'Spectre', 'Bloodhound LSR powerplant'],
},
{
fuelId: 'ethanol',
oxidiserId: 'n2o',
isHypergolic: false,
vacuumIsp: 295,
flameTemp: 2700,
optimalOF: 4.5,
gamma: 1.24,
R: 310,
chamberPressureRef: '~3 MPa',
expansionRatioRef: 10,
energeticCategory: 'Pressure-fed Bipropellant',
notes: 'Popular amateur/experimental combination; N₂O self-pressurises. Much safer than hypergolics.',
engines: ['Amateur N₂O/ethanol motors'],
},
{
fuelId: 'htpb',
oxidiserId: 'n2o',
isHypergolic: false,
vacuumIsp: 280,
flameTemp: 2700,
optimalOF: 7.0,
gamma: 1.23,
R: 300,
chamberPressureRef: '~3.5 MPa',
expansionRatioRef: 10,
energeticCategory: 'Hybrid',
notes: 'Simple and safe hybrid combination. O/F varies with regression rate and geometry.',
engines: ['SpaceShipOne/Two', 'NAMMO hybrid', 'Amateur hybrids'],
},
{
fuelId: 'htpb',
oxidiserId: 'lox',
isHypergolic: false,
vacuumIsp: 350,
flameTemp: 3200,
optimalOF: 2.3,
gamma: 1.22,
R: 360,
chamberPressureRef: '6.9 MPa',
expansionRatioRef: 40,
energeticCategory: 'Hybrid',
notes: 'Higher performance hybrid. Requires cryogenic LOX handling alongside solid grain.',
engines: ['Research vehicles', 'Peregrine (small launcher studies)'],
},
{
fuelId: 'htpb',
oxidiserId: 'ap',
isHypergolic: false,
vacuumIsp: 275,
flameTemp: 3300,
optimalOF: null,
gamma: 1.24,
R: 320,
chamberPressureRef: '6.9 MPa',
expansionRatioRef: 10,
energeticCategory: 'Solid',
notes: 'Standard APCP composite solid propellant. Al powder added for density/performance. O/F is premixed; no separate ratio.',
engines: ['Space Shuttle SRB', 'Ariane 5 P230', 'Hobby APCP motors'],
},
{
fuelId: 'sucrose',
oxidiserId: 'kno3',
isHypergolic: false,
vacuumIsp: 164,
flameTemp: 1720,
optimalOF: 0.65,
gamma: 1.30,
R: 290,
chamberPressureRef: '~7 MPa',
expansionRatioRef: 10,
energeticCategory: 'Solid',
notes: 'KNSU: low cost, low performance candy propellant. Ideal for educational motors.',
engines: ['Amateur KNSU motors'],
},
{
fuelId: 'dextrose',
oxidiserId: 'kno3',
isHypergolic: false,
vacuumIsp: 160,
flameTemp: 1700,
optimalOF: 0.63,
gamma: 1.31,
R: 288,
chamberPressureRef: '~7 MPa',
expansionRatioRef: 10,
energeticCategory: 'Solid',
notes: 'KNDX: slightly lower flame temperature than KNSU. Comparable accessibility.',
engines: ['Amateur KNDX motors'],
},
]
// ── Derived solver data ──────────────────────────────────────────────────────
// PROPELLANTS and PROPELLANT_TYPES replace propellants.js as the single source
// of truth consumed by PropellantModal / useSolver.
const substanceMap = Object.fromEntries(SUBSTANCES.map(s => [s.id, s]))
export const PROPELLANT_TYPES = [
'All',
'Cryogenic Bipropellant',
'Storable Bipropellant',
'Pressure-fed Bipropellant',
'Hybrid',
'Solid',
'Monopropellant',
]
export const PROPELLANTS = [
// Bipropellant combinations
...COMBINATIONS.map(c => {
const fuel = substanceMap[c.fuelId]
const ox = substanceMap[c.oxidiserId]
const values = { gamma: c.gamma, R: c.R, T0: c.flameTemp }
if (c.optimalOF != null) values.OF = c.optimalOF
if (fuel?.density != null) values.rhoFuel = fuel.density
if (ox?.density != null) values.rhoOx = ox.density
return {
id: `${c.oxidiserId}_${c.fuelId}`,
name: `${ox?.symbol ?? c.oxidiserId} / ${fuel?.symbol ?? c.fuelId}`,
oxidizer: ox?.name ?? c.oxidiserId,
fuel: fuel?.name ?? c.fuelId,
type: c.energeticCategory,
description: c.notes,
vacuumIsp: c.vacuumIsp,
values,
notes: `Theoretical vacuum Isp at ε = ${c.expansionRatioRef}:1, pc = ${c.chamberPressureRef}`,
}
}),
// Monopropellants
...SUBSTANCES
.filter(s => s.role === 'monopropellant')
.map(s => ({
id: s.id,
name: s.name,
oxidizer: null,
fuel: s.name,
type: 'Monopropellant',
description: s.description,
vacuumIsp: s.catalyticIsp,
values: { gamma: s.gamma, R: s.R, T0: s.T0 },
notes: 'Catalytic decomposition; vacuum Isp',
})),
]

42
src/engine/numerics.js Normal file
View File

@@ -0,0 +1,42 @@
// Numerical methods for transcendental equations
/**
* Bisection method — finds root of f in [lo, hi] within tolerance.
* Returns null if no sign change is found.
*/
export function bisect(f, lo, hi, tol = 1e-10, maxIter = 200) {
let flo = f(lo)
let fhi = f(hi)
if (!isFinite(flo) || !isFinite(fhi)) return null
if (flo * fhi > 0) return null // no sign change
for (let i = 0; i < maxIter; i++) {
const mid = (lo + hi) / 2
if ((hi - lo) / 2 < tol) return mid
const fmid = f(mid)
if (fmid === 0) return mid
if (flo * fmid < 0) { hi = mid; fhi = fmid }
else { lo = mid; flo = fmid }
}
return (lo + hi) / 2
}
/**
* Isentropic area-ratio function A/A* as a function of Mach M and gamma.
* A/A* = (1/M) * [(2/(γ+1)) * (1 + (γ-1)/2 * M²)]^((γ+1)/(2(γ-1)))
*/
export function areaRatioFromMach(M, gamma) {
const exp = (gamma + 1) / (2 * (gamma - 1))
const base = (2 / (gamma + 1)) * (1 + (gamma - 1) / 2 * M * M)
return (1 / M) * Math.pow(base, exp)
}
/**
* Solve Mach number from area ratio and gamma using bisection.
* supersonic=true searches M > 1, false searches M < 1.
* Returns null if unsolvable.
*/
export function machFromAreaRatio(eps, gamma, supersonic = true) {
const f = (M) => areaRatioFromMach(M, gamma) - eps
if (supersonic) return bisect(f, 1.0001, 200, 1e-9)
else return bisect(f, 0.0001, 0.9999, 1e-9)
}

View File

@@ -0,0 +1,234 @@
// Pure calculation functions for rocket vehicle design (no React)
const G0 = 9.80665 // m/s² standard gravity
/**
* Calculate full rocket geometry and performance from engine data and rocket inputs.
*
* @param {object} engineData — results section from engine JSON (allThermo, nozzleGeometry, etc.)
* @param {object} rocketInputs — {
* outerRadius, // m — vehicle outer radius
* burnTime, // s
* arrangement, // 'coaxial' | 'tandem'
* innerPropellant, // 'fuel' | 'ox' (coaxial only — which propellant goes in inner tank)
* rhoFuel, // kg/m³
* rhoOx, // kg/m³
* payloadMass, // kg
* payloadBayLength, // m
* structMassFraction, // 01 (dry mass fraction of propellant mass)
* }
* @returns {object|null}
*/
export function calcRocketGeometry(engineData, rocketInputs) {
if (!engineData || !rocketInputs) return null
const {
outerRadius,
burnTime,
arrangement,
innerPropellant,
rhoFuel,
rhoOx,
payloadMass,
payloadBayLength,
structMassFraction,
} = rocketInputs
const Isp = engineData?.allThermo?.Isp
const F = engineData?.allThermo?.F
const mdot = engineData?.allThermo?.mdot
const OF = engineData?.allThermo?.OF
// Flow rates: use direct values if solved, otherwise derive from mdot + OF
let mdot_f = engineData?.allThermo?.mdot_f
let mdot_ox = engineData?.allThermo?.mdot_ox
if (mdot && OF && isFinite(mdot) && isFinite(OF) && OF > 0) {
if (!mdot_f || !isFinite(mdot_f)) mdot_f = mdot / (1 + OF)
if (!mdot_ox || !isFinite(mdot_ox)) mdot_ox = mdot * OF / (1 + OF)
}
// Require at minimum: radius, propellant densities, and flow rates from an engine
if (
!outerRadius || outerRadius <= 0 ||
!rhoFuel || !rhoOx ||
!mdot_f || !mdot_ox ||
!isFinite(mdot_f) || !isFinite(mdot_ox)
) return null
const R = outerRadius
const tb = (burnTime && burnTime > 0) ? burnTime : 30
// ── Propellant volumes ──────────────────────────────────────────────
const m_fuel = mdot_f * tb
const m_ox = mdot_ox * tb
const V_fuel = m_fuel / rhoFuel
const V_ox = m_ox / rhoOx
const V_prop = V_fuel + V_ox
// ── Nose cone (ogive approximation: height = 2R) ────────────────────
const L_nose = 2 * R
// ── Payload bay ─────────────────────────────────────────────────────
const L_payload = payloadBayLength ?? 0
// ── Engine section (from nozzle geometry if available) ──────────────
const ng = engineData?.nozzleGeometry
const cg = engineData?.chamberGeometry
const L_engine = (ng?.Ln ?? 0) + (cg?.Lc ?? 0)
// ── Tank geometry ───────────────────────────────────────────────────
let L_tank_fuel, L_tank_ox, L_tank, r_inner
if (arrangement === 'coaxial') {
// Both tanks share the same axial section; total volume in annulus + inner cylinder
// Inner tank radius from the smaller propellant volume
// Outer tank occupies the remaining annular area
const V_inner = innerPropellant === 'fuel' ? V_fuel : V_ox
const V_outer = innerPropellant === 'fuel' ? V_ox : V_fuel
// Solve for tank length using outer volume first (conservative — outer is usually larger)
// V_outer = π (R² - r_inner²) L_tank and V_inner = π r_inner² L_tank
// From V_inner / (V_inner + V_outer) = r_inner² / R²
// → r_inner = R √(V_inner / V_prop_total)
r_inner = R * Math.sqrt(V_inner / V_prop)
// Guard: inner radius must be smaller than outer
if (r_inner >= R) r_inner = R * 0.7
L_tank = V_prop / (Math.PI * R * R)
L_tank_fuel = arrangement === 'coaxial' ? L_tank : null
L_tank_ox = arrangement === 'coaxial' ? L_tank : null
} else {
// Tandem: stacked cylinders
L_tank_fuel = V_fuel / (Math.PI * R * R)
L_tank_ox = V_ox / (Math.PI * R * R)
L_tank = L_tank_fuel + L_tank_ox
r_inner = null
}
// ── Total vehicle length ────────────────────────────────────────────
const totalLength = L_nose + L_payload + L_tank + L_engine
// ── Mass budget ─────────────────────────────────────────────────────
const m_prop = m_fuel + m_ox
const m_struct = m_prop * (structMassFraction ?? 0.10)
const m_payload = payloadMass ?? 0
const m_dry = m_struct + m_payload
const m_wet = m_prop + m_dry
const massRatio = m_wet / m_dry
// ── Tsiolkovsky delta-v ─────────────────────────────────────────────
const deltaV = Isp && isFinite(Isp) && massRatio > 1
? Isp * G0 * Math.log(massRatio)
: null
// ── TWR at liftoff ──────────────────────────────────────────────────
const TWR = F && isFinite(F) && m_wet > 0
? F / (m_wet * G0)
: null
return {
// Dimensions
outerRadius: R,
L_nose,
L_payload,
L_tank,
L_tank_fuel,
L_tank_ox,
L_engine,
totalLength,
r_inner,
// Mass
m_fuel,
m_ox,
V_fuel,
V_ox,
m_prop,
m_struct,
m_payload,
m_dry,
m_wet,
// Performance
massRatio,
deltaV,
TWR,
// Config
arrangement,
innerPropellant: arrangement === 'coaxial' ? innerPropellant : null,
// Nozzle geometry for 3D model (null → fall back to a generic flare)
nozzleExitRadius: ng?.re ?? null,
nozzleThroatRadius: ng?.rt ?? null,
chamberLength: cg?.Lc ?? 0,
nozzleLength: ng?.Ln ?? 0,
}
}
/**
* Returns a list of { key, label, ok, value } requirement objects so the UI
* can show exactly what is missing before the calc can run.
*/
export function diagnoseRocketInputs(engineCalcData, rocketInputs) {
const t = engineCalcData?.allThermo ?? {}
const mdot = t.mdot
const OF = t.OF
const mdot_f_raw = t.mdot_f
const mdot_ox_raw = t.mdot_ox
// Derived flow rates (same logic as calcRocketGeometry)
let mdot_f = mdot_f_raw
let mdot_ox = mdot_ox_raw
if (mdot && OF && isFinite(mdot) && isFinite(OF) && OF > 0) {
if (!mdot_f || !isFinite(mdot_f)) mdot_f = mdot / (1 + OF)
if (!mdot_ox || !isFinite(mdot_ox)) mdot_ox = mdot * OF / (1 + OF)
}
const R = rocketInputs?.outerRadius
const bt = rocketInputs?.burnTime
const fmt = v => (v != null && isFinite(v)) ? v.toPrecision(4) : null
return [
{
key: 'mdot',
label: 'Total mass flow (ṁ)',
ok: !!(mdot && isFinite(mdot)),
value: fmt(mdot),
hint: 'Enter ṁ or Thrust + Isp on the Engine page',
},
{
key: 'OF',
label: 'O/F ratio',
ok: !!(OF && isFinite(OF) && OF > 0),
value: fmt(OF),
hint: 'Enter O/F ratio on the Engine page',
},
{
key: 'mdot_f',
label: 'Fuel flow rate (ṁ_f)',
ok: !!(mdot_f && isFinite(mdot_f)),
value: fmt(mdot_f),
hint: mdot && OF ? 'Derived from ṁ + O/F' : 'Needs ṁ and O/F both solved',
},
{
key: 'mdot_ox',
label: 'Oxidizer flow rate (ṁ_ox)',
ok: !!(mdot_ox && isFinite(mdot_ox)),
value: fmt(mdot_ox),
hint: mdot && OF ? 'Derived from ṁ + O/F' : 'Needs ṁ and O/F both solved',
},
{
key: 'outerRadius',
label: 'Outer diameter',
ok: !!(R && R > 0),
value: R ? `${(R * 2000).toFixed(0)} mm` : null,
hint: 'Enter outer diameter in Vehicle Geometry section',
},
{
key: 'burnTime',
label: 'Burn time',
ok: true, // always ok — falls back to 30 s
value: bt ? `${bt} s` : '30 s (default)',
hint: null,
},
]
}

View File

@@ -0,0 +1,56 @@
import { downloadBlob } from './exportImport.js'
export { downloadBlob }
/**
* Build a rocket design JSON Blob for download.
* Schema version 1: inputs section is re-importable; results is reference-only.
*/
export function exportRocketJSON({ outerRadius, tankConfig, propDensities, payload, structure, engineData, geometry }) {
const payload_ = {
version: 1,
type: 'rocket_design',
exportedAt: new Date().toISOString(),
inputs: {
outerRadius,
tankConfig,
propDensities,
payload,
structure,
},
engineData: engineData ?? null,
results: geometry ?? null,
}
return new Blob([JSON.stringify(payload_, null, 2)], { type: 'application/json' })
}
/**
* Parse an imported rocket design JSON string and return the inputs section.
* Throws a descriptive Error if the file is invalid.
*/
export function parseRocketImport(jsonString) {
let data
try {
data = JSON.parse(jsonString)
} catch {
throw new Error('File is not valid JSON.')
}
if (data.type !== 'rocket_design') {
throw new Error('This file is not a rocket design export.')
}
if (data.version !== 1) {
throw new Error(`Unsupported export version: ${data.version}`)
}
const { outerRadius, tankConfig, propDensities, payload, structure } = data.inputs ?? {}
return {
outerRadius: outerRadius ?? null,
tankConfig: tankConfig ?? null,
propDensities: propDensities ?? null,
payload: payload ?? null,
structure: structure ?? null,
engineData: data.engineData ?? null,
}
}

127
src/engine/solver.js Normal file
View File

@@ -0,0 +1,127 @@
import { EQUATIONS } from './equations.js'
import { VARIABLES } from './variables.js'
/**
* Run the constraint-propagation solver.
*
* @param {Record<string, number>} knownValues user-entered { varId: value }
* @returns {{
* solved: Record<string, { value: number, equationId: string, equationName: string }>,
* missing: Record<string, string[][]> varId → list of variable-sets that would unlock it
* }}
*/
export function solve(knownValues) {
const known = { ...knownValues }
const solved = {}
let progress = true
while (progress) {
progress = false
for (const eq of EQUATIONS) {
for (const target of eq.variables) {
if (target in known) continue
if (!eq.solvers[target]) continue
const solver = eq.solvers[target]
const others = solver.requires
? solver.requires(known)
: eq.variables.filter(v => v !== target)
if (others.every(v => v in known)) {
try {
const val = solver(known)
if (isFinite(val) && !isNaN(val)) {
known[target] = val
solved[target] = { value: val, equationId: eq.id, equationName: eq.name }
progress = true
break // restart inner loop after new solve
}
} catch (_) {}
}
}
}
}
// Figure out what's missing: for each unsolved variable in the workspace,
// find all equations that could solve it and report what each needs.
const missing = {}
const allWorkspaceVars = Object.keys(knownValues)
for (const varId of allWorkspaceVars) {
if (varId in solved || varId in knownValues) continue
// This shouldn't happen (workspace vars that are neither known nor solved)
// but keep for symmetry.
}
// For all variables NOT in known at all, find what each equation needs
for (const eq of EQUATIONS) {
for (const target of eq.variables) {
if (target in known) continue
if (!eq.solvers[target]) continue
const allRequired = eq.solvers[target]?.requires
? eq.solvers[target].requires(known)
: eq.variables.filter(v => v !== target)
const needed = allRequired.filter(v => !(v in known))
if (needed.length > 0) {
if (!missing[target]) missing[target] = []
// Avoid duplicate entries
const key = needed.sort().join(',')
if (!missing[target].some(arr => arr.slice().sort().join(',') === key)) {
missing[target].push(needed)
}
}
}
}
return { solved, missing }
}
/**
* Given the current known+solved values, return a summary of
* what each unsolvable workspace variable still needs.
*
* @param {string[]} workspaceVarIds ids of vars on the workspace
* @param {Record<string, number>} knownValues
* @param {Record<string, any>} solvedValues
* @returns {Array<{ varId, name, symbol, options: string[][] }>}
*/
export function getMissingReport(workspaceVarIds, knownValues, solvedValues) {
const allKnown = {
...knownValues,
...Object.fromEntries(Object.entries(solvedValues).map(([k, v]) => [k, v.value])),
}
const report = []
for (const varId of workspaceVarIds) {
if (varId in allKnown) continue // it's known or solved, skip
const options = []
for (const eq of EQUATIONS) {
if (!eq.variables.includes(varId)) continue
if (!eq.solvers[varId]) continue
const allRequired = eq.solvers[varId]?.requires
? eq.solvers[varId].requires(allKnown)
: eq.variables.filter(v => v !== varId)
const needed = allRequired.filter(v => !(v in allKnown))
if (needed.length > 0) {
const key = needed.slice().sort().join(',')
if (!options.some(o => o.slice().sort().join(',') === key)) {
options.push(needed)
}
}
}
if (options.length > 0) {
const varDef = VARIABLES[varId]
report.push({
varId,
name: varDef?.name ?? varId,
symbol: varDef?.symbol ?? varId,
options,
})
}
}
return report
}

122
src/engine/units.js Normal file
View File

@@ -0,0 +1,122 @@
// Unit families for display/input conversion.
// The solver always works in SI internally.
// Each unit: { id, label, toSI(v), fromSI(v) }
export const UNIT_FAMILIES = {
force: {
units: [
{ id: 'N', label: 'N', toSI: v => v, fromSI: v => v },
{ id: 'kN', label: 'kN', toSI: v => v * 1e3, fromSI: v => v / 1e3 },
{ id: 'MN', label: 'MN', toSI: v => v * 1e6, fromSI: v => v / 1e6 },
{ id: 'lbf', label: 'lbf', toSI: v => v * 4.44822, fromSI: v => v / 4.44822 },
],
},
pressure: {
units: [
{ id: 'Pa', label: 'Pa', toSI: v => v, fromSI: v => v },
{ id: 'kPa', label: 'kPa', toSI: v => v * 1e3, fromSI: v => v / 1e3 },
{ id: 'MPa', label: 'MPa', toSI: v => v * 1e6, fromSI: v => v / 1e6 },
{ id: 'bar', label: 'bar', toSI: v => v * 1e5, fromSI: v => v / 1e5 },
{ id: 'psi', label: 'psi', toSI: v => v * 6894.76, fromSI: v => v / 6894.76 },
{ id: 'atm', label: 'atm', toSI: v => v * 101325, fromSI: v => v / 101325 },
],
},
mass: {
units: [
{ id: 'kg', label: 'kg', toSI: v => v, fromSI: v => v },
{ id: 'g', label: 'g', toSI: v => v * 0.001, fromSI: v => v * 1000 },
{ id: 't', label: 't', toSI: v => v * 1000, fromSI: v => v * 0.001 },
{ id: 'lb', label: 'lb', toSI: v => v * 0.453592, fromSI: v => v / 0.453592 },
{ id: 'slug', label: 'slug', toSI: v => v * 14.5939, fromSI: v => v / 14.5939 },
],
},
massflow: {
units: [
{ id: 'kg/s', label: 'kg/s', toSI: v => v, fromSI: v => v },
{ id: 'g/s', label: 'g/s', toSI: v => v * 0.001, fromSI: v => v * 1000 },
{ id: 'lb/s', label: 'lb/s', toSI: v => v * 0.453592, fromSI: v => v / 0.453592 },
{ id: 'lb/min', label: 'lb/min', toSI: v => v * 0.453592 / 60, fromSI: v => v / 0.453592 * 60 },
],
},
velocity: {
units: [
{ id: 'm/s', label: 'm/s', toSI: v => v, fromSI: v => v },
{ id: 'km/s', label: 'km/s', toSI: v => v * 1000, fromSI: v => v / 1000 },
{ id: 'ft/s', label: 'ft/s', toSI: v => v * 0.3048, fromSI: v => v / 0.3048 },
],
},
acceleration: {
units: [
{ id: 'm/s²', label: 'm/s²', toSI: v => v, fromSI: v => v },
{ id: 'ft/s²', label: 'ft/s²', toSI: v => v * 0.3048, fromSI: v => v / 0.3048 },
],
},
temperature: {
units: [
{ id: 'K', label: 'K', toSI: v => v, fromSI: v => v },
{ id: '°C', label: '°C', toSI: v => v + 273.15, fromSI: v => v - 273.15 },
{ id: '°F', label: '°F', toSI: v => (v - 32) * 5 / 9 + 273.15, fromSI: v => (v - 273.15) * 9 / 5 + 32 },
{ id: '°R', label: '°R', toSI: v => v * 5 / 9, fromSI: v => v * 9 / 5 },
],
},
area: {
units: [
{ id: 'm²', label: 'm²', toSI: v => v, fromSI: v => v },
{ id: 'cm²', label: 'cm²', toSI: v => v * 1e-4, fromSI: v => v * 1e4 },
{ id: 'mm²', label: 'mm²', toSI: v => v * 1e-6, fromSI: v => v * 1e6 },
{ id: 'in²', label: 'in²', toSI: v => v * 6.4516e-4, fromSI: v => v / 6.4516e-4 },
],
},
time: {
units: [
{ id: 's', label: 's', toSI: v => v, fromSI: v => v },
{ id: 'ms', label: 'ms', toSI: v => v * 0.001, fromSI: v => v * 1000 },
{ id: 'min', label: 'min', toSI: v => v * 60, fromSI: v => v / 60 },
],
},
impulse: {
units: [
{ id: 'N·s', label: 'N·s', toSI: v => v, fromSI: v => v },
{ id: 'kN·s', label: 'kN·s', toSI: v => v * 1e3, fromSI: v => v / 1e3 },
{ id: 'lbf·s', label: 'lbf·s', toSI: v => v * 4.44822, fromSI: v => v / 4.44822 },
],
},
density: {
units: [
{ id: 'kg/m³', label: 'kg/m³', toSI: v => v, fromSI: v => v },
{ id: 'g/cm³', label: 'g/cm³', toSI: v => v * 1000, fromSI: v => v / 1000 },
{ id: 'lb/ft³', label: 'lb/ft³', toSI: v => v * 16.0185, fromSI: v => v / 16.0185 },
],
},
spec_gas: {
units: [
{ id: 'J/(kg·K)', label: 'J/(kg·K)', toSI: v => v, fromSI: v => v },
],
},
length: {
units: [
{ id: 'm', label: 'm', toSI: v => v, fromSI: v => v },
{ id: 'mm', label: 'mm', toSI: v => v * 0.001, fromSI: v => v * 1000 },
{ id: 'cm', label: 'cm', toSI: v => v * 0.01, fromSI: v => v * 100 },
{ id: 'in', label: 'in', toSI: v => v * 0.0254, fromSI: v => v / 0.0254 },
{ id: 'ft', label: 'ft', toSI: v => v * 0.3048, fromSI: v => v / 0.3048 },
],
},
volume: {
units: [
{ id: 'm³', label: 'm³', toSI: v => v, fromSI: v => v },
{ id: 'L', label: 'L', toSI: v => v * 0.001, fromSI: v => v * 1000 },
{ id: 'cm³', label: 'cm³', toSI: v => v * 1e-6, fromSI: v => v * 1e6 },
{ id: 'in³', label: 'in³', toSI: v => v * 1.6387e-5, fromSI: v => v / 1.6387e-5 },
],
},
dimensionless: {
units: [
{ id: '—', label: '—', toSI: v => v, fromSI: v => v },
],
},
}
export function getUnitsForFamily(familyId) {
return UNIT_FAMILIES[familyId]?.units ?? UNIT_FAMILIES.dimensionless.units
}

272
src/engine/variables.js Normal file
View File

@@ -0,0 +1,272 @@
// All rocketry variable definitions
// id: internal key used in equations
// symbol: display symbol (unicode)
// name: human-readable name
// units: SI units
// description: tooltip text
// category: grouping in palette
// unitFamily: key into UNIT_FAMILIES for conversion
export const VARIABLES = {
// ── Thrust ────────────────────────────────────────────────────────────
F: {
id: 'F', symbol: 'F', name: 'Thrust', units: 'N',
description: 'Net thrust force produced by the engine',
category: 'Thrust',
unitFamily: 'force',
},
mdot: {
id: 'mdot', symbol: 'ṁ', name: 'Mass Flow Rate', units: 'kg/s',
description: 'Total propellant mass flow rate through the engine',
category: 'Thrust',
unitFamily: 'massflow',
},
Ve: {
id: 'Ve', symbol: 'Vₑ', name: 'Exhaust Velocity', units: 'm/s',
description: 'Actual kinematic exit velocity at the nozzle exit plane',
category: 'Thrust',
unitFamily: 'velocity',
},
ceff: {
id: 'ceff', symbol: 'cₑ', name: 'Effective Exhaust Velocity', units: 'm/s',
description: 'F/ṁ = Isp·g₀; includes both momentum and pressure thrust',
category: 'Thrust',
unitFamily: 'velocity',
},
pe: {
id: 'pe', symbol: 'pₑ', name: 'Exit Pressure', units: 'Pa',
description: 'Static pressure at the nozzle exit plane',
category: 'Thrust',
unitFamily: 'pressure',
},
pa: {
id: 'pa', symbol: 'pₐ', name: 'Ambient Pressure', units: 'Pa',
description: 'Surrounding ambient static pressure',
category: 'Thrust',
unitFamily: 'pressure',
},
Ae: {
id: 'Ae', symbol: 'Aₑ', name: 'Exit Area', units: 'm²',
description: 'Cross-sectional area of the nozzle exit',
category: 'Thrust',
unitFamily: 'area',
},
// ── Specific Impulse ──────────────────────────────────────────────────
Isp: {
id: 'Isp', symbol: 'Isp', name: 'Specific Impulse', units: 's',
description: 'Thrust produced per unit weight flow of propellant',
category: 'Specific Impulse',
unitFamily: 'time',
},
g0: {
id: 'g0', symbol: 'g₀', name: 'Standard Gravity', units: 'm/s²',
description: 'Standard gravitational acceleration (9.80665 m/s²)',
category: 'Specific Impulse',
unitFamily: 'acceleration',
},
// ── Characteristic Velocity / Thrust Coefficient ──────────────────────
cstar: {
id: 'cstar', symbol: 'c*', name: 'Characteristic Velocity', units: 'm/s',
description: 'Measure of combustion efficiency; c* = p₀·Aₜ/ṁ',
category: 'Nozzle Performance',
unitFamily: 'velocity',
},
CF: {
id: 'CF', symbol: 'Cꜰ', name: 'Thrust Coefficient', units: '—',
description: 'Dimensionless nozzle performance factor; Cꜰ = F/(p₀·Aₜ)',
category: 'Nozzle Performance',
unitFamily: 'dimensionless',
},
p0: {
id: 'p0', symbol: 'p₀', name: 'Chamber Pressure', units: 'Pa',
description: 'Stagnation (total) pressure in the combustion chamber',
category: 'Nozzle Performance',
unitFamily: 'pressure',
},
At: {
id: 'At', symbol: 'Aₜ', name: 'Throat Area', units: 'm²',
description: 'Cross-sectional area at the nozzle throat',
category: 'Nozzle Performance',
unitFamily: 'area',
},
// ── Tsiolkovsky Rocket Equation ───────────────────────────────────────
dv: {
id: 'dv', symbol: 'Δv', name: 'Delta-v', units: 'm/s',
description: 'Total velocity change budget for a manoeuvre',
category: 'Rocket Equation',
unitFamily: 'velocity',
},
m0: {
id: 'm0', symbol: 'm₀', name: 'Initial Mass', units: 'kg',
description: 'Total vehicle mass at ignition (wet mass)',
category: 'Rocket Equation',
unitFamily: 'mass',
},
mf: {
id: 'mf', symbol: 'mf', name: 'Final Mass', units: 'kg',
description: 'Vehicle mass after propellant is expended (dry mass)',
category: 'Rocket Equation',
unitFamily: 'mass',
},
mp: {
id: 'mp', symbol: 'mₚ', name: 'Propellant Mass', units: 'kg',
description: 'Mass of propellant consumed: mₚ = m₀ mf',
category: 'Rocket Equation',
unitFamily: 'mass',
},
MR: {
id: 'MR', symbol: 'MR', name: 'Mass Ratio', units: '—',
description: 'Ratio of initial to final mass: MR = m₀/mf',
category: 'Rocket Equation',
unitFamily: 'dimensionless',
},
zeta: {
id: 'zeta', symbol: 'ζ', name: 'Propellant Mass Fraction', units: '—',
description: 'Fraction of initial mass that is propellant: ζ = mₚ/m₀',
category: 'Rocket Equation',
unitFamily: 'dimensionless',
},
tb: {
id: 'tb', symbol: 'tᵦ', name: 'Burn Time', units: 's',
description: 'Duration of engine burn',
category: 'Rocket Equation',
unitFamily: 'time',
},
// ── Isentropic Flow ───────────────────────────────────────────────────
M: {
id: 'M', symbol: 'M', name: 'Mach Number', units: '—',
description: 'Local Mach number at a cross-section',
category: 'Isentropic Flow',
unitFamily: 'dimensionless',
},
gamma: {
id: 'gamma', symbol: 'γ', name: 'Ratio of Specific Heats', units: '—',
description: 'Ratio of specific heats cₚ/cᵥ for the gas (≈1.4 for air, ~1.2 for rocket exhaust)',
category: 'Isentropic Flow',
unitFamily: 'dimensionless',
},
R: {
id: 'R', symbol: 'R', name: 'Specific Gas Constant', units: 'J/(kg·K)',
description: 'Gas constant for the propellant gas: R = R̄/M_mol',
category: 'Isentropic Flow',
unitFamily: 'spec_gas',
},
T: {
id: 'T', symbol: 'T', name: 'Static Temperature', units: 'K',
description: 'Local static temperature at a cross-section',
category: 'Isentropic Flow',
unitFamily: 'temperature',
},
T0: {
id: 'T0', symbol: 'T₀', name: 'Stagnation Temperature', units: 'K',
description: 'Total (stagnation) temperature, equal to chamber temperature',
category: 'Isentropic Flow',
unitFamily: 'temperature',
},
p_static: {
id: 'p_static', symbol: 'p', name: 'Static Pressure', units: 'Pa',
description: 'Local static pressure at a cross-section',
category: 'Isentropic Flow',
unitFamily: 'pressure',
},
rho: {
id: 'rho', symbol: 'ρ', name: 'Density', units: 'kg/m³',
description: 'Local gas density at a cross-section',
category: 'Isentropic Flow',
unitFamily: 'density',
},
rho0: {
id: 'rho0', symbol: 'ρ₀', name: 'Stagnation Density', units: 'kg/m³',
description: 'Total (stagnation) density',
category: 'Isentropic Flow',
unitFamily: 'density',
},
a_sound: {
id: 'a_sound', symbol: 'a', name: 'Speed of Sound', units: 'm/s',
description: 'Local speed of sound: a = √(γRT)',
category: 'Isentropic Flow',
unitFamily: 'velocity',
},
v_flow: {
id: 'v_flow', symbol: 'v', name: 'Flow Velocity', units: 'm/s',
description: 'Local bulk flow velocity: v = M·a',
category: 'Isentropic Flow',
unitFamily: 'velocity',
},
// ── Nozzle Geometry ───────────────────────────────────────────────────
Me: {
id: 'Me', symbol: 'Mₑ', name: 'Exit Mach Number', units: '—',
description: 'Mach number at the nozzle exit plane',
category: 'Nozzle Geometry',
unitFamily: 'dimensionless',
},
eps: {
id: 'eps', symbol: 'ε', name: 'Expansion Ratio', units: '—',
description: 'Nozzle area expansion ratio: ε = Aₑ/Aₜ',
category: 'Nozzle Geometry',
unitFamily: 'dimensionless',
},
Te: {
id: 'Te', symbol: 'Tₑ', name: 'Exit Temperature', units: 'K',
description: 'Static temperature at the nozzle exit plane',
category: 'Nozzle Geometry',
unitFamily: 'temperature',
},
// ── Performance Metrics ───────────────────────────────────────────────
TWR: {
id: 'TWR', symbol: 'TWR', name: 'Thrust-to-Weight Ratio', units: '—',
description: 'Ratio of thrust to vehicle weight at ignition',
category: 'Performance',
unitFamily: 'dimensionless',
},
m_vehicle: {
id: 'm_vehicle', symbol: 'mᵥ', name: 'Vehicle Mass', units: 'kg',
description: 'Vehicle mass used for TWR calculation',
category: 'Performance',
unitFamily: 'mass',
},
J: {
id: 'J', symbol: 'J', name: 'Total Impulse', units: 'N·s',
description: 'Integral of thrust over burn time: J = F·tᵦ (constant thrust)',
category: 'Performance',
unitFamily: 'impulse',
},
// ── Mass & Propellant ─────────────────────────────────────────────────
OF: {
id: 'OF', symbol: 'O/F', name: 'Oxidiser/Fuel Ratio', units: '—',
description: 'Mass ratio of oxidiser to fuel flow rates',
category: 'Propellant',
unitFamily: 'dimensionless',
},
mdot_ox: {
id: 'mdot_ox', symbol: 'ṁₒₓ', name: 'Oxidiser Flow Rate', units: 'kg/s',
description: 'Mass flow rate of oxidiser',
category: 'Propellant',
unitFamily: 'massflow',
},
mdot_f: {
id: 'mdot_f', symbol: 'ṁf', name: 'Fuel Flow Rate', units: 'kg/s',
description: 'Mass flow rate of fuel',
category: 'Propellant',
unitFamily: 'massflow',
},
}
// All categories in desired display order
export const CATEGORIES = [
'Thrust',
'Specific Impulse',
'Nozzle Performance',
'Rocket Equation',
'Isentropic Flow',
'Nozzle Geometry',
'Performance',
'Propellant',
]

View File

@@ -0,0 +1,90 @@
import { useState, useMemo } from 'react'
import { solve } from '../engine/solver.js'
import {
calcChamber,
calcNozzle,
calcInjector,
calcCooling,
calcFeedSystem,
} from '../engine/engineDesignCalcs.js'
export function useEngineDesign() {
// Thermodynamic inputs — SI values, null means not provided
const [thermoInputs, setThermoInputs] = useState({
p0: null, T0: null, gamma: null, R: null,
mdot: null, F: null, OF: null, At: null, eps: null, pa: null,
})
// Engineering design choices
const [chamber, setChamber] = useState({ Lstar: 1.0, contractionRatio: 8, convAngleDeg: 30 })
const [nozzle, setNozzle] = useState({ type: 'conical', divAngleDeg: 15 })
const [injector, setInjector] = useState({ type: 'doublet', N: 20, dpFraction: 0.2, Cd: 0.7, rhoFuel: 800, rhoOx: 1140 })
const [cooling, setCooling] = useState({ method: 'regenerative', channelCount: 40, filmFraction: 0.05 })
const [feedSystem, setFeedSystem] = useState({
type: 'pressure_fed', feedFactor: 1.3,
rhoFuel: 800, rhoOx: 1140, pressurantR: 2077, pressurantT: 300,
})
const [burnTime, setBurnTime] = useState(30)
// Run the existing constraint-propagation solver on the thermodynamic inputs
const thermoResults = useMemo(() => {
const known = { g0: 9.80665 }
for (const [k, v] of Object.entries(thermoInputs)) {
if (v !== null && v !== '' && isFinite(Number(v))) known[k] = Number(v)
}
if (Object.keys(known).length === 1) return { solved: {}, missing: {} }
return solve(known)
}, [thermoInputs])
// Merge user-provided and solver-derived values into a flat map
const allThermo = useMemo(() => {
const result = {}
for (const [k, v] of Object.entries(thermoInputs)) {
if (v !== null && isFinite(Number(v))) result[k] = Number(v)
}
for (const [k, info] of Object.entries(thermoResults.solved)) {
result[k] = info.value
}
return result
}, [thermoInputs, thermoResults])
// Derived geometry and system calculations
const chamberGeometry = useMemo(() => calcChamber(allThermo, chamber), [allThermo, chamber])
const nozzleGeometry = useMemo(() => calcNozzle(allThermo, nozzle), [allThermo, nozzle])
const injectorGeometry = useMemo(() => calcInjector(allThermo, injector), [allThermo, injector])
const coolingResults = useMemo(() => calcCooling(allThermo, cooling, chamberGeometry), [allThermo, cooling, chamberGeometry])
const feedResults = useMemo(() => calcFeedSystem(allThermo, feedSystem, burnTime), [allThermo, feedSystem, burnTime])
function setThermoInput(key, value) {
setThermoInputs(prev => ({ ...prev, [key]: value }))
}
/** Restore all design state from a parsed import (inputs section). */
function loadDesign(inputs) {
if (inputs.thermodynamics) {
setThermoInputs(prev => ({ ...prev, ...inputs.thermodynamics }))
}
if (inputs.chamber) setChamber(inputs.chamber)
if (inputs.nozzle) setNozzle(inputs.nozzle)
if (inputs.injector) setInjector(inputs.injector)
if (inputs.cooling) setCooling(inputs.cooling)
if (inputs.feedSystem) setFeedSystem(inputs.feedSystem)
if (inputs.burnTime != null) setBurnTime(inputs.burnTime)
}
return {
// State
thermoInputs, setThermoInput,
chamber, setChamber,
nozzle, setNozzle,
injector, setInjector,
cooling, setCooling,
feedSystem, setFeedSystem,
burnTime, setBurnTime,
// Derived
thermoResults, allThermo,
chamberGeometry, nozzleGeometry, injectorGeometry, coolingResults, feedResults,
// Actions
loadDesign,
}
}

View File

@@ -0,0 +1,146 @@
import { useState, useMemo } from 'react'
import { calcRocketGeometry, diagnoseRocketInputs } from '../engine/rocketDesignCalcs.js'
export function useRocketDesign() {
// Engine imported from JSON export
const [engineData, setEngineData] = useState(null)
// Outer body diameter (m)
const [outerRadius, setOuterRadius] = useState(null)
// Tank configuration
const [tankConfig, setTankConfig] = useState({
arrangement: 'tandem', // 'coaxial' | 'tandem'
innerPropellant: 'fuel', // coaxial only
})
// Propellant densities (kg/m³) — pre-filled from engine injector if available
const [propDensities, setPropDensities] = useState({
rhoFuel: 800,
rhoOx: 1140,
})
// Payload
const [payload, setPayload] = useState({
mass: 0,
bayLength: 0,
})
// Structure
const [structure, setStructure] = useState({
structMassFraction: 0.10,
burnTime: null, // null → use engine feedSystem.burnTime if available
})
// Effective burn time (prefer explicit override, fallback to engine data)
const effectiveBurnTime = useMemo(() => {
if (structure.burnTime != null && structure.burnTime > 0) return structure.burnTime
return engineData?.inputs?.burnTime ?? null
}, [structure.burnTime, engineData])
// Auto-suggest outer radius from nozzle exit diameter when engine is loaded
// (caller can override via setOuterRadius)
const suggestedRadius = useMemo(() => {
const De = engineData?.results?.nozzleGeometry?.De
if (De && isFinite(De)) return De / 2 * 1.5 // body ~1.5× nozzle exit radius
return null
}, [engineData])
const rocketInputs = useMemo(() => ({
outerRadius: outerRadius ?? suggestedRadius,
burnTime: effectiveBurnTime,
arrangement: tankConfig.arrangement,
innerPropellant: tankConfig.innerPropellant,
rhoFuel: propDensities.rhoFuel,
rhoOx: propDensities.rhoOx,
payloadMass: payload.mass,
payloadBayLength: payload.bayLength,
structMassFraction: structure.structMassFraction,
}), [outerRadius, suggestedRadius, effectiveBurnTime, tankConfig, propDensities, payload, structure])
// Flatten engine results for calcs
const engineCalcData = useMemo(() => {
if (!engineData) return null
return {
allThermo: engineData.results?.thermodynamics ?? {},
nozzleGeometry: engineData.results?.nozzleGeometry ?? null,
chamberGeometry: engineData.results?.chamberGeometry ?? null,
}
}, [engineData])
const geometry = useMemo(
() => calcRocketGeometry(engineCalcData, rocketInputs),
[engineCalcData, rocketInputs],
)
const diagnosis = useMemo(
() => diagnoseRocketInputs(engineCalcData, rocketInputs),
[engineCalcData, rocketInputs],
)
/** Bulk-restore all state from a parsed rocket design import. */
function loadRocketDesign({ outerRadius, tankConfig, propDensities, payload, structure, engineData }) {
if (outerRadius != null) setOuterRadius(outerRadius)
if (tankConfig) setTankConfig(tankConfig)
if (propDensities) setPropDensities(propDensities)
if (payload) setPayload(payload)
if (structure) setStructure(structure)
if (engineData) setEngineData(engineData)
}
/** Parse and store an engine JSON (the full exported object). */
function loadEngine(jsonString) {
let data
try {
data = JSON.parse(jsonString)
} catch {
throw new Error('File is not valid JSON.')
}
if (data.type !== 'engine_design') {
throw new Error('This file is not an engine design export.')
}
setEngineData(data)
// Pre-fill propellant densities from injector inputs if available
const inj = data.inputs?.injector
if (inj) {
setPropDensities(prev => ({
rhoFuel: inj.rhoFuel ?? prev.rhoFuel,
rhoOx: inj.rhoOx ?? prev.rhoOx,
}))
}
}
// Convenience accessor for display chips in the import section
const engineSummary = useMemo(() => {
if (!engineData) return null
const t = engineData.results?.thermodynamics ?? {}
return {
F: t.F,
Isp: t.Isp,
mdot: t.mdot,
OF: t.OF ?? engineData.inputs?.thermodynamics?.OF,
}
}, [engineData])
return {
// Engine
engineData, loadEngine, engineSummary,
loadRocketDesign,
// Geometry inputs
outerRadius, setOuterRadius,
suggestedRadius,
// Tank
tankConfig, setTankConfig,
// Propellants
propDensities, setPropDensities,
// Payload
payload, setPayload,
// Structure
structure, setStructure,
effectiveBurnTime,
// Output
geometry,
diagnosis,
}
}

159
src/hooks/useSolver.js Normal file
View File

@@ -0,0 +1,159 @@
import { useState, useMemo, useCallback, useRef } from 'react'
import { solve, getMissingReport } from '../engine/solver.js'
import { EQUATIONS, EQUATION_PRESETS } from '../engine/equations.js'
import { VARIABLES } from '../engine/variables.js'
import { UNIT_FAMILIES, getUnitsForFamily } from '../engine/units.js'
export function useSolver() {
// Variable ids currently on the workspace
const [workspaceVarIds, setWorkspaceVarIds] = useState([])
// User-entered values stored in SI: { varId: number }
const [userValues, setUserValues] = useState({})
// Selected unit per variable: { varId: unitId }
const [unitSelections, setUnitSelections] = useState({})
const [sciNotation, setSciNotation] = useState(false)
// Ref so getUnit/setValue always see the latest selections without stale closures
const unitSelectionsRef = useRef(unitSelections)
unitSelectionsRef.current = unitSelections
const getUnitId = useCallback((varId) => {
const variable = VARIABLES[varId]
const familyId = variable?.unitFamily ?? 'dimensionless'
const units = getUnitsForFamily(familyId)
return unitSelectionsRef.current[varId] ?? units[0].id
}, [])
const getUnit = useCallback((varId) => {
const variable = VARIABLES[varId]
const familyId = variable?.unitFamily ?? 'dimensionless'
const units = getUnitsForFamily(familyId)
const unitId = unitSelectionsRef.current[varId] ?? units[0].id
return units.find(u => u.id === unitId) ?? units[0]
}, [])
const setUnit = useCallback((varId, unitId) => {
setUnitSelections(prev => ({ ...prev, [varId]: unitId }))
}, [])
const toggleSciNotation = useCallback(() => {
setSciNotation(prev => !prev)
}, [])
// Run solver whenever userValues or workspace changes
const { solved, missingReport } = useMemo(() => {
const { solved, missing: _missing } = solve(userValues)
const report = getMissingReport(workspaceVarIds, userValues, solved)
return { solved, missingReport: report }
}, [userValues, workspaceVarIds])
const addVariable = useCallback((varId) => {
setWorkspaceVarIds(prev => prev.includes(varId) ? prev : [...prev, varId])
}, [])
const addVariables = useCallback((varIds) => {
setWorkspaceVarIds(prev => {
const next = [...prev]
for (const id of varIds) {
if (!next.includes(id)) next.push(id)
}
return next
})
}, [])
const removeVariable = useCallback((varId) => {
setWorkspaceVarIds(prev => prev.filter(id => id !== varId))
setUserValues(prev => {
const next = { ...prev }
delete next[varId]
return next
})
}, [])
// rawDisplayValue is in the currently-selected display unit; we convert to SI for storage
const setValue = useCallback((varId, rawDisplayValue) => {
setUserValues(prev => {
if (rawDisplayValue === '' || rawDisplayValue === null || rawDisplayValue === undefined) {
const next = { ...prev }
delete next[varId]
return next
}
const unit = getUnit(varId)
return { ...prev, [varId]: unit.toSI(Number(rawDisplayValue)) }
})
}, [getUnit])
const addPreset = useCallback((presetId) => {
const preset = EQUATION_PRESETS.find(p => p.id === presetId)
if (!preset) return
const vars = new Set()
for (const eqId of preset.equationIds) {
const eq = EQUATIONS.find(e => e.id === eqId)
if (eq) eq.variables.forEach(v => vars.add(v))
}
setWorkspaceVarIds(prev => {
const next = [...prev]
for (const v of vars) {
if (!next.includes(v)) next.push(v)
}
return next
})
}, [])
const applyPropellant = useCallback((propellant) => {
// Propellant values are already in SI
const varIds = Object.keys(propellant.values)
setWorkspaceVarIds(prev => {
const next = [...prev]
for (const id of varIds) {
if (!next.includes(id)) next.push(id)
}
return next
})
setUserValues(prev => ({
...prev,
...Object.fromEntries(
Object.entries(propellant.values).map(([k, v]) => [k, Number(v)])
),
}))
}, [])
const clearWorkspace = useCallback(() => {
setWorkspaceVarIds([])
setUserValues({})
}, [])
const importWorkspace = useCallback(({ variableIds, userValues: uv, unitSelections: us }) => {
setWorkspaceVarIds(variableIds)
setUserValues(uv)
setUnitSelections(us ?? {})
}, [])
// Combine for easy component access
const allKnown = useMemo(() => ({
...userValues,
...Object.fromEntries(Object.entries(solved).map(([k, v]) => [k, v.value])),
}), [userValues, solved])
return {
workspaceVarIds,
userValues,
solved,
missingReport,
allKnown,
unitSelections,
sciNotation,
getUnitId,
getUnit,
setUnit,
toggleSciNotation,
addVariable,
addVariables,
removeVariable,
setValue,
addPreset,
applyPropellant,
clearWorkspace,
importWorkspace,
}
}

10
src/index.css Normal file
View File

@@ -0,0 +1,10 @@
@import "tailwindcss";
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: system-ui, -apple-system, sans-serif;
}

13
src/main.jsx Normal file
View File

@@ -0,0 +1,13 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { BrowserRouter } from 'react-router-dom'
import './index.css'
import App from './App.jsx'
createRoot(document.getElementById('root')).render(
<StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</StrictMode>,
)

687
src/pages/EnginePage.jsx Normal file
View File

@@ -0,0 +1,687 @@
import { useRef, useState, useEffect } from 'react'
import { createPortal } from 'react-dom'
import { useEngineDesign } from '../hooks/useEngineDesign.js'
import {
exportEngineJSON,
exportEngineOdt,
parseEngineImport,
downloadBlob,
} from '../engine/engineExportImport.js'
import DesignSection from '../components/engine/DesignSection.jsx'
import EngineModel3D from '../components/engine/EngineModel3D.jsx'
import { formatValue } from '../engine/format.js'
import { getUnitsForFamily } from '../engine/units.js'
import { ENGINE_FIELD_INFO } from '../engine/engineFieldInfo.js'
/* ── Info popup ───────────────────────────────────────────────────── */
function InfoPopup({ infoKey, anchorRef, onClose }) {
const popupRef = useRef(null)
const info = ENGINE_FIELD_INFO[infoKey]
const [pos, setPos] = useState({ top: 0, left: 0 })
useEffect(() => {
if (anchorRef.current) {
const r = anchorRef.current.getBoundingClientRect()
setPos({
top: r.bottom + 6,
left: Math.min(r.left, window.innerWidth - 230),
})
}
}, [anchorRef])
useEffect(() => {
function onKey(e) { if (e.key === 'Escape') onClose() }
function onDown(e) {
if (!popupRef.current?.contains(e.target) && !anchorRef.current?.contains(e.target)) {
onClose()
}
}
document.addEventListener('keydown', onKey)
document.addEventListener('mousedown', onDown)
return () => {
document.removeEventListener('keydown', onKey)
document.removeEventListener('mousedown', onDown)
}
}, [onClose, anchorRef])
if (!info) return null
return createPortal(
<div
ref={popupRef}
style={{ position: 'fixed', top: pos.top, left: pos.left, zIndex: 9999 }}
className="bg-slate-800 border border-slate-600 rounded-lg p-3 shadow-2xl w-56 text-xs"
>
<p className="font-semibold text-slate-100 mb-1">{info.name}</p>
<p className="text-slate-300 mb-2 leading-relaxed">{info.description}</p>
{info.higher && <p className="text-emerald-400 mb-0.5"> {info.higher}</p>}
{info.lower && <p className="text-amber-400"> {info.lower}</p>}
</div>,
document.body,
)
}
/* ── Small reusable input components ──────────────────────────────── */
function NumInput({ label, value, onChange, units, step, placeholder, unitFamily, defaultUnitId, infoKey }) {
const unitList = unitFamily ? getUnitsForFamily(unitFamily) : null
const [selectedUnitId, setSelectedUnitId] = useState(
() => defaultUnitId ?? unitList?.[0]?.id ?? null,
)
const [popupOpen, setPopupOpen] = useState(false)
const infoRef = useRef(null)
const selectedUnit = unitList?.find(u => u.id === selectedUnitId) ?? unitList?.[0] ?? null
const displayValue = value == null
? ''
: selectedUnit ? selectedUnit.fromSI(value) : value
function handleChange(str) {
if (str === '') { onChange(null); return }
const typed = parseFloat(str)
if (isNaN(typed)) return
onChange(selectedUnit ? selectedUnit.toSI(typed) : typed)
}
return (
<div className="flex items-center gap-2">
<label className="text-xs text-slate-400 w-44 shrink-0">{label}</label>
<input
type="number"
value={displayValue}
step={step}
placeholder={placeholder ?? ''}
onChange={e => handleChange(e.target.value)}
className="flex-1 min-w-0 w-24 px-2 py-1 bg-slate-800 border border-slate-600 rounded text-sm text-slate-100
focus:border-blue-500 focus:outline-none placeholder-slate-600"
/>
{unitList && unitList.length > 1 ? (
<select
value={selectedUnit?.id ?? ''}
onChange={e => setSelectedUnitId(e.target.value)}
className="px-1 py-0.5 bg-slate-700 border border-slate-600 rounded text-xs text-slate-300
focus:outline-none cursor-pointer shrink-0"
>
{unitList.map(u => <option key={u.id} value={u.id}>{u.label}</option>)}
</select>
) : unitList?.[0] ? (
<span className="text-xs text-slate-500 shrink-0">{unitList[0].label}</span>
) : units ? (
<span className="text-xs text-slate-500 shrink-0 whitespace-nowrap">{units}</span>
) : null}
{infoKey && (
<>
<button
ref={infoRef}
onClick={() => setPopupOpen(v => !v)}
className="text-slate-500 hover:text-slate-300 text-xs shrink-0 leading-none"
title="Info"
></button>
{popupOpen && (
<InfoPopup infoKey={infoKey} anchorRef={infoRef} onClose={() => setPopupOpen(false)} />
)}
</>
)}
</div>
)
}
function SelectInput({ label, value, onChange, options, infoKey }) {
const [popupOpen, setPopupOpen] = useState(false)
const infoRef = useRef(null)
return (
<div className="flex items-center gap-2">
<label className="text-xs text-slate-400 w-44 shrink-0">{label}</label>
<select
value={value}
onChange={e => onChange(e.target.value)}
className="px-2 py-1 bg-slate-800 border border-slate-600 rounded text-sm text-slate-100
focus:border-blue-500 focus:outline-none"
>
{options.map(o => (
<option key={o.value} value={o.value}>{o.label}</option>
))}
</select>
{infoKey && (
<>
<button
ref={infoRef}
onClick={() => setPopupOpen(v => !v)}
className="text-slate-500 hover:text-slate-300 text-xs shrink-0 leading-none"
title="Info"
></button>
{popupOpen && (
<InfoPopup infoKey={infoKey} anchorRef={infoRef} onClose={() => setPopupOpen(false)} />
)}
</>
)}
</div>
)
}
/* ── Result display helpers ───────────────────────────────────────── */
function ResultRow({ label, value, unit, unitFamily, defaultUnitId, infoKey }) {
const unitList = unitFamily ? getUnitsForFamily(unitFamily) : null
const [selectedUnitId, setSelectedUnitId] = useState(
() => defaultUnitId ?? unitList?.[0]?.id ?? null,
)
const [popupOpen, setPopupOpen] = useState(false)
const infoRef = useRef(null)
const selectedUnit = unitList?.find(u => u.id === selectedUnitId) ?? unitList?.[0] ?? null
const rawDisplay = value !== null && value !== undefined && isFinite(value)
? (selectedUnit ? selectedUnit.fromSI(value) : value)
: null
const display = rawDisplay !== null ? formatValue(rawDisplay) : '—'
const unitLabel = selectedUnit?.label ?? unit
return (
<div className="flex items-center gap-2 text-sm">
<span className="text-slate-400 w-48 shrink-0">{label}</span>
<span className={`font-mono ${display === '—' ? 'text-slate-600' : 'text-green-400'}`}>
{display}
</span>
{unitList && unitList.length > 1 ? (
<select
value={selectedUnit?.id ?? ''}
onChange={e => setSelectedUnitId(e.target.value)}
className="px-1 py-0 bg-slate-700 border border-slate-600 rounded text-xs text-slate-400
focus:outline-none cursor-pointer"
>
{unitList.map(u => <option key={u.id} value={u.id}>{u.label}</option>)}
</select>
) : (
unitLabel && <span className="text-slate-500 text-xs">{unitLabel}</span>
)}
{infoKey && (
<>
<button
ref={infoRef}
onClick={() => setPopupOpen(v => !v)}
className="text-slate-500 hover:text-slate-300 text-xs leading-none shrink-0"
title="Info"
></button>
{popupOpen && (
<InfoPopup infoKey={infoKey} anchorRef={infoRef} onClose={() => setPopupOpen(false)} />
)}
</>
)}
</div>
)
}
function ResultSection({ title, children }) {
return (
<div className="mb-5">
<h3 className="text-xs font-semibold text-slate-400 uppercase tracking-wider mb-2 pb-1 border-b border-slate-800">
{title}
</h3>
<div className="space-y-1">{children}</div>
</div>
)
}
/* ── Main page ────────────────────────────────────────────────────── */
export default function EnginePage() {
const importRef = useRef(null)
const {
thermoInputs, setThermoInput,
chamber, setChamber,
nozzle, setNozzle,
injector, setInjector,
cooling, setCooling,
feedSystem, setFeedSystem,
burnTime, setBurnTime,
allThermo,
chamberGeometry: cg,
nozzleGeometry: ng,
injectorGeometry: ig,
coolingResults: cr,
feedResults: fr,
loadDesign,
} = useEngineDesign()
function handleExportJSON() {
const blob = exportEngineJSON({
thermoInputs, chamber, nozzle, injector, cooling, feedSystem, burnTime,
allThermo, chamberGeometry: cg, nozzleGeometry: ng,
injectorGeometry: ig, coolingResults: cr, feedResults: fr,
})
downloadBlob(blob, 'engine-design.json')
}
async function handleExportODT() {
const blob = await exportEngineOdt({
allThermo, chamberGeometry: cg, nozzleGeometry: ng,
injectorGeometry: ig, coolingResults: cr, feedResults: fr,
})
downloadBlob(blob, 'engine-design.odt')
}
function handleImport(e) {
const file = e.target.files[0]
if (!file) return
const reader = new FileReader()
reader.onload = ev => {
try {
const inputs = parseEngineImport(ev.target.result)
loadDesign(inputs)
} catch (err) {
alert('Import failed: ' + err.message)
}
}
reader.readAsText(file)
e.target.value = ''
}
return (
<div className="flex flex-col flex-1 overflow-hidden">
{/* Sub-header */}
<div className="flex items-center justify-between px-5 py-2 bg-slate-900 border-b border-slate-700 shrink-0">
<h1 className="text-sm font-semibold text-slate-200">Engine Design</h1>
<div className="flex gap-2">
<button
onClick={handleExportJSON}
className="px-3 py-1 text-xs bg-slate-700 hover:bg-slate-600 rounded text-slate-200 transition-colors"
>
JSON
</button>
<button
onClick={handleExportODT}
className="px-3 py-1 text-xs bg-slate-700 hover:bg-slate-600 rounded text-slate-200 transition-colors"
>
ODT
</button>
<button
onClick={() => importRef.current?.click()}
className="px-3 py-1 text-xs bg-slate-700 hover:bg-slate-600 rounded text-slate-200 transition-colors"
>
Import
</button>
<input
ref={importRef}
type="file"
accept=".json"
className="hidden"
onChange={handleImport}
/>
</div>
</div>
{/* Three-column layout */}
<div className="flex flex-1 overflow-hidden">
{/* ── Left: Inputs ── */}
<div className="w-[420px] shrink-0 overflow-y-auto p-4 border-r border-slate-700">
<DesignSection title="Thermodynamic Inputs">
<NumInput
label="Chamber Pressure (p₀)"
value={thermoInputs.p0}
onChange={v => setThermoInput('p0', v)}
unitFamily="pressure"
defaultUnitId="MPa"
infoKey="p0"
placeholder="3"
/>
<NumInput
label="Chamber Temp (T₀)"
value={thermoInputs.T0}
onChange={v => setThermoInput('T0', v)}
unitFamily="temperature"
defaultUnitId="K"
infoKey="T0"
placeholder="3500"
/>
<NumInput
label="Ratio of Specific Heats (γ)"
value={thermoInputs.gamma}
onChange={v => setThermoInput('gamma', v)}
infoKey="gamma"
placeholder="1.2"
step="0.01"
/>
<NumInput
label="Specific Gas Constant (R)"
value={thermoInputs.R}
onChange={v => setThermoInput('R', v)}
units="J/(kg·K)"
infoKey="R_gas"
placeholder="360"
/>
<NumInput
label="Mass Flow Rate (ṁ)"
value={thermoInputs.mdot}
onChange={v => setThermoInput('mdot', v)}
unitFamily="massflow"
defaultUnitId="kg/s"
infoKey="mdot"
placeholder="5"
/>
<NumInput
label="Thrust (F)"
value={thermoInputs.F}
onChange={v => setThermoInput('F', v)}
unitFamily="force"
defaultUnitId="kN"
infoKey="F_input"
placeholder="or enter ṁ"
/>
<NumInput
label="O/F Ratio"
value={thermoInputs.OF}
onChange={v => setThermoInput('OF', v)}
infoKey="OF"
placeholder="2.2"
step="0.1"
/>
<NumInput
label="Throat Area (Aₜ)"
value={thermoInputs.At}
onChange={v => setThermoInput('At', v)}
unitFamily="area"
defaultUnitId="cm²"
infoKey="At_input"
placeholder="auto-solved"
/>
<NumInput
label="Expansion Ratio (ε)"
value={thermoInputs.eps}
onChange={v => setThermoInput('eps', v)}
infoKey="eps_input"
placeholder="auto-solved"
step="0.5"
/>
<NumInput
label="Ambient Pressure (pₐ)"
value={thermoInputs.pa}
onChange={v => setThermoInput('pa', v)}
unitFamily="pressure"
defaultUnitId="kPa"
infoKey="pa"
placeholder="101.325"
/>
</DesignSection>
<DesignSection title="Combustion Chamber">
<NumInput
label="Characteristic Length (L*)"
value={chamber.Lstar}
onChange={v => setChamber(c => ({ ...c, Lstar: v }))}
unitFamily="length"
defaultUnitId="m"
infoKey="Lstar"
step="0.05"
/>
<NumInput
label="Contraction Ratio"
value={chamber.contractionRatio}
onChange={v => setChamber(c => ({ ...c, contractionRatio: v }))}
infoKey="contractionRatio"
step="0.5"
/>
<NumInput
label="Convergent Half-Angle"
value={chamber.convAngleDeg}
onChange={v => setChamber(c => ({ ...c, convAngleDeg: v }))}
units="°"
infoKey="convAngleDeg"
step="1"
/>
</DesignSection>
<DesignSection title="Nozzle">
<SelectInput
label="Nozzle Type"
value={nozzle.type}
onChange={v => setNozzle(n => ({ ...n, type: v }))}
options={[
{ value: 'conical', label: 'Conical' },
{ value: 'bell', label: 'Bell (80% length)' },
]}
infoKey="nozzleType"
/>
{nozzle.type === 'conical' && (
<NumInput
label="Divergence Half-Angle"
value={nozzle.divAngleDeg}
onChange={v => setNozzle(n => ({ ...n, divAngleDeg: v }))}
units="°"
infoKey="divAngleDeg"
step="1"
/>
)}
</DesignSection>
<DesignSection title="Injector">
<SelectInput
label="Injector Type"
value={injector.type}
onChange={v => setInjector(i => ({ ...i, type: v }))}
options={[
{ value: 'doublet', label: 'Impinging Doublet' },
{ value: 'triplet', label: 'Impinging Triplet' },
{ value: 'coaxial', label: 'Coaxial' },
{ value: 'pintle', label: 'Pintle' },
]}
infoKey="injectorType"
/>
<NumInput
label="Number of Elements (N)"
value={injector.N}
onChange={v => setInjector(i => ({ ...i, N: Math.max(1, Math.round(v)) }))}
infoKey="injectorN"
step="1"
/>
<NumInput
label="ΔP/p₀ Fraction"
value={injector.dpFraction}
onChange={v => setInjector(i => ({ ...i, dpFraction: v }))}
infoKey="dpFraction"
step="0.01"
/>
<NumInput
label="Discharge Coefficient (Cd)"
value={injector.Cd}
onChange={v => setInjector(i => ({ ...i, Cd: v }))}
infoKey="Cd"
step="0.01"
/>
<NumInput
label="Fuel Density (ρ_f)"
value={injector.rhoFuel}
onChange={v => setInjector(i => ({ ...i, rhoFuel: v }))}
unitFamily="density"
defaultUnitId="kg/m³"
infoKey="rhoFuel_inj"
/>
<NumInput
label="Oxidiser Density (ρ_ox)"
value={injector.rhoOx}
onChange={v => setInjector(i => ({ ...i, rhoOx: v }))}
unitFamily="density"
defaultUnitId="kg/m³"
infoKey="rhoOx_inj"
/>
</DesignSection>
<DesignSection title="Cooling">
<SelectInput
label="Cooling Method"
value={cooling.method}
onChange={v => setCooling(c => ({ ...c, method: v }))}
options={[
{ value: 'regenerative', label: 'Regenerative' },
{ value: 'film', label: 'Film Cooling' },
{ value: 'ablative', label: 'Ablative' },
{ value: 'uncooled', label: 'Uncooled' },
]}
infoKey="coolingMethod"
/>
{cooling.method === 'regenerative' && (
<NumInput
label="Channel Count"
value={cooling.channelCount}
onChange={v => setCooling(c => ({ ...c, channelCount: Math.max(1, Math.round(v)) }))}
infoKey="channelCount"
step="1"
/>
)}
{cooling.method === 'film' && (
<NumInput
label="Film Mass Fraction"
value={cooling.filmFraction}
onChange={v => setCooling(c => ({ ...c, filmFraction: v }))}
infoKey="filmFraction"
step="0.01"
/>
)}
</DesignSection>
<DesignSection title="Feed System">
<SelectInput
label="Feed System"
value={feedSystem.type}
onChange={v => setFeedSystem(f => ({ ...f, type: v }))}
options={[
{ value: 'pressure_fed', label: 'Pressure-Fed' },
{ value: 'pump_fed', label: 'Pump-Fed' },
]}
infoKey="feedType"
/>
{feedSystem.type === 'pressure_fed' && (
<NumInput
label="Feed Pressure Factor"
value={feedSystem.feedFactor}
onChange={v => setFeedSystem(f => ({ ...f, feedFactor: v }))}
infoKey="feedFactor"
step="0.05"
/>
)}
<NumInput
label="Fuel Density (ρ_f)"
value={feedSystem.rhoFuel}
onChange={v => setFeedSystem(f => ({ ...f, rhoFuel: v }))}
unitFamily="density"
defaultUnitId="kg/m³"
infoKey="rhoFuel_feed"
/>
<NumInput
label="Oxidiser Density (ρ_ox)"
value={feedSystem.rhoOx}
onChange={v => setFeedSystem(f => ({ ...f, rhoOx: v }))}
unitFamily="density"
defaultUnitId="kg/m³"
infoKey="rhoOx_feed"
/>
<NumInput
label="Burn Time"
value={burnTime}
onChange={v => setBurnTime(v ?? 30)}
unitFamily="time"
defaultUnitId="s"
infoKey="burnTime"
step="1"
/>
</DesignSection>
</div>
{/* ── Centre: 3D Model ── */}
<div className="flex-1 relative border-r border-slate-700 bg-slate-950/50">
<EngineModel3D chamberGeometry={cg} nozzleGeometry={ng} />
</div>
{/* ── Right: Results ── */}
<div className="w-[380px] shrink-0 overflow-y-auto p-4">
<ResultSection title="Thermodynamic Performance">
<ResultRow label="Throat Area (Aₜ)" value={allThermo.At} unitFamily="area" defaultUnitId="cm²" infoKey="At_result" />
<ResultRow label="Exit Area (Aₑ)" value={allThermo.Ae} unitFamily="area" defaultUnitId="cm²" infoKey="Ae_result" />
<ResultRow label="Expansion Ratio (ε)" value={allThermo.eps} unit="—" infoKey="eps_result" />
<ResultRow label="Exit Mach (Mₑ)" value={allThermo.Me} unit="—" infoKey="Me_result" />
<ResultRow label="Exit Temp (Tₑ)" value={allThermo.Te} unitFamily="temperature" defaultUnitId="K" infoKey="Te_result" />
<ResultRow label="Exit Pressure (pₑ)" value={allThermo.pe} unitFamily="pressure" defaultUnitId="kPa" infoKey="pe_result" />
<ResultRow label="Exhaust Velocity (Vₑ)" value={allThermo.Ve} unitFamily="velocity" defaultUnitId="m/s" infoKey="Ve_result" />
<ResultRow label="Thrust (F)" value={allThermo.F} unitFamily="force" defaultUnitId="kN" infoKey="F_result" />
<ResultRow label="Specific Impulse (Isp)" value={allThermo.Isp} unit="s" infoKey="Isp_result" />
<ResultRow label="c*" value={allThermo.cstar} unitFamily="velocity" defaultUnitId="m/s" infoKey="cstar_result" />
<ResultRow label="Thrust Coefficient (CF)" value={allThermo.CF} unit="—" infoKey="CF_result" />
<ResultRow label="Fuel Flow (ṁ_f)" value={allThermo.mdot_f} unitFamily="massflow" defaultUnitId="kg/s" infoKey="mdot_f_result" />
<ResultRow label="Ox Flow (ṁ_ox)" value={allThermo.mdot_ox} unitFamily="massflow" defaultUnitId="kg/s" infoKey="mdot_ox_result" />
</ResultSection>
<ResultSection title="Chamber Geometry">
<ResultRow label="Chamber Diameter (Dc)" value={cg?.Dc} unitFamily="length" defaultUnitId="mm" infoKey="Dc_result" />
<ResultRow label="Throat Diameter (Dt)" value={cg?.Dt} unitFamily="length" defaultUnitId="mm" infoKey="Dt_result" />
<ResultRow label="Contraction Ratio" value={cg?.contractionRatio} unit="—" infoKey="contractionRatio_result" />
<ResultRow label="Total Length (Lc)" value={cg?.Lc} unitFamily="length" defaultUnitId="mm" infoKey="Lc_result" />
<ResultRow label="Cylindrical Length" value={cg?.L_cyl} unitFamily="length" defaultUnitId="mm" infoKey="L_cyl_result" />
<ResultRow label="Convergent Length" value={cg?.L_conv} unitFamily="length" defaultUnitId="mm" infoKey="L_conv_result" />
<ResultRow label="Chamber Volume" value={cg?.Vc} unitFamily="volume" defaultUnitId="cm³" infoKey="Vc_result" />
</ResultSection>
<ResultSection title="Nozzle Geometry">
<ResultRow label="Throat Diameter (Dt)" value={ng?.Dt} unitFamily="length" defaultUnitId="mm" infoKey="Dt_result" />
<ResultRow label="Exit Diameter (De)" value={ng?.De} unitFamily="length" defaultUnitId="mm" infoKey="De_result" />
<ResultRow label="Nozzle Length (Ln)" value={ng?.Ln} unitFamily="length" defaultUnitId="mm" infoKey="Ln_result" />
</ResultSection>
<ResultSection title="Injector">
<ResultRow label="Pressure Drop (ΔP)" value={ig?.deltaP} unitFamily="pressure" defaultUnitId="kPa" infoKey="deltaP_result" />
<ResultRow label="Fuel Jet Velocity" value={ig?.v_f} unitFamily="velocity" defaultUnitId="m/s" infoKey="v_f_result" />
<ResultRow label="Oxidiser Jet Velocity" value={ig?.v_ox} unitFamily="velocity" defaultUnitId="m/s" infoKey="v_ox_result" />
<ResultRow label="Fuel Orifice Diameter" value={ig?.d_f} unitFamily="length" defaultUnitId="mm" infoKey="d_f_result" />
<ResultRow label="Oxidiser Orifice Diameter" value={ig?.d_ox} unitFamily="length" defaultUnitId="mm" infoKey="d_ox_result" />
</ResultSection>
<ResultSection title="Cooling">
{cr ? (
<>
{cr.method === 'regenerative' && (
<>
<ResultRow label="Est. Heat Flux" value={cr.q_est} unit="W/m²" infoKey="q_est_result" />
<ResultRow label="Total Heat Load" value={cr.q_total} unit="W" infoKey="q_total_result" />
<ResultRow label="Channel Count" value={cr.channelCount} unit="—" infoKey="channelCount_result" />
<ResultRow label="Channel Area (each)" value={cr.channelArea} unitFamily="area" defaultUnitId="mm²" infoKey="channelArea_result" />
</>
)}
{cr.method === 'film' && (
<>
<ResultRow label="Film Mass Flow" value={cr.mdot_film} unitFamily="massflow" defaultUnitId="g/s" infoKey="mdot_film_result" />
<ResultRow label="Est. Isp Penalty" value={cr.ispPenalty} unit="%" infoKey="ispPenalty_result" />
</>
)}
{cr.note && (
<p className="text-xs text-slate-400 italic mt-1">{cr.note}</p>
)}
</>
) : (
<p className="text-xs text-slate-600"></p>
)}
</ResultSection>
<ResultSection title="Feed System">
{fr ? (
<>
<ResultRow label="Tank Pressure" value={fr.p_tank} unitFamily="pressure" defaultUnitId="MPa" infoKey="p_tank_result" />
<ResultRow label="Fuel Volume" value={fr.V_fuel} unitFamily="volume" defaultUnitId="L" infoKey="V_fuel_result" />
<ResultRow label="Oxidiser Volume" value={fr.V_ox} unitFamily="volume" defaultUnitId="L" infoKey="V_ox_result" />
<ResultRow label="Total Propellant Volume" value={fr.V_prop} unitFamily="volume" defaultUnitId="L" infoKey="V_prop_result" />
{fr.m_press != null && <ResultRow label="Pressurant Mass" value={fr.m_press} unitFamily="mass" defaultUnitId="kg" infoKey="m_press_result" />}
{fr.dP_pump != null && <ResultRow label="Pump ΔP" value={fr.dP_pump} unitFamily="pressure" defaultUnitId="MPa" infoKey="dP_pump_result" />}
{fr.P_turbine != null && <ResultRow label="Est. Turbine Power" value={fr.P_turbine / 1000} unit="kW" infoKey="P_turbine_result" />}
</>
) : (
<p className="text-xs text-slate-600"></p>
)}
</ResultSection>
</div>
</div>
</div>
)
}

58
src/pages/Home.jsx Normal file
View File

@@ -0,0 +1,58 @@
import { Link } from 'react-router-dom'
export default function Home() {
return (
<div className="flex flex-1 min-h-0 overflow-y-auto bg-slate-950 text-slate-100">
<div className="max-w-4xl mx-auto px-6 py-16 w-full">
{/* Hero */}
<div className="text-center mb-16">
<div className="text-6xl mb-4">🚀</div>
<h1 className="text-4xl font-bold text-white mb-3">RocketTools</h1>
<p className="text-lg text-slate-400 max-w-xl mx-auto">
Engineering calculators and tools for rocket propulsion, designed for students and professionals.
</p>
</div>
{/* Tool cards */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
<Link
to="/solver"
className="group block p-6 rounded-xl border border-slate-700 bg-slate-900 hover:border-blue-500 hover:bg-slate-800 transition-colors"
>
<div className="text-3xl mb-3"></div>
<h2 className="text-xl font-semibold text-white mb-2 group-hover:text-blue-400 transition-colors">
Equation Solver
</h2>
<p className="text-sm text-slate-400">
Drag-and-drop rocketry variables. Enter known values and unknowns solve automatically using constraint propagation.
</p>
<div className="mt-4 text-sm text-blue-400 font-medium">Open solver </div>
</Link>
<Link
to="/design/engine"
className="group block p-6 rounded-xl border border-slate-700 bg-slate-900 hover:border-blue-500 hover:bg-slate-800 transition-colors"
>
<div className="text-3xl mb-3">🔧</div>
<h2 className="text-xl font-semibold text-white mb-2 group-hover:text-blue-400 transition-colors">
Engine Designer
</h2>
<p className="text-sm text-slate-400">
Configure chamber, nozzle, and feed system. Visualize a live 3D engine model.
</p>
<div className="mt-4 text-sm text-blue-400 font-medium">Open designer </div>
</Link>
<div className="p-6 rounded-xl border border-slate-800 bg-slate-900/50 opacity-60 cursor-not-allowed">
<div className="text-3xl mb-3">📊</div>
<h2 className="text-xl font-semibold text-slate-500 mb-2">Trajectory Plotter</h2>
<p className="text-sm text-slate-500">
Simulate flight trajectories with drag and gravity losses.
</p>
<div className="mt-4 text-sm text-slate-600 font-medium">Coming soon</div>
</div>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,309 @@
import { useState, useMemo } from 'react'
import { SUBSTANCES, COMBINATIONS } from '../engine/knowledgebaseData.js'
// ── Helpers ──────────────────────────────────────────────────────────────────
const ROLE_FILTERS = ['All', 'Fuels', 'Oxidisers', 'Monopropellants']
const SUBCATEGORY_FILTERS = ['Cryogenic', 'Storable', 'Hypergolic', 'Green', 'Solid', 'Hybrid']
function roleBadgeClass(role) {
if (role === 'fuel') return 'bg-blue-900 text-blue-200 border border-blue-700'
if (role === 'oxidiser') return 'bg-orange-900 text-orange-200 border border-orange-700'
return 'bg-yellow-900 text-yellow-200 border border-yellow-700'
}
function roleLabel(role) {
if (role === 'fuel') return 'FUEL'
if (role === 'oxidiser') return 'OXIDISER'
return 'MONOPROPELLANT'
}
function subcategoryBadgeClass(sub) {
const map = {
Cryogenic: 'bg-cyan-900 text-cyan-200 border border-cyan-700',
Storable: 'bg-slate-700 text-slate-200 border border-slate-500',
Hypergolic: 'bg-red-900 text-red-200 border border-red-700',
Green: 'bg-green-900 text-green-200 border border-green-700',
Solid: 'bg-amber-900 text-amber-200 border border-amber-700',
Hybrid: 'bg-purple-900 text-purple-200 border border-purple-700',
}
return map[sub] ?? 'bg-slate-700 text-slate-200 border border-slate-500'
}
function PropRow({ label, value }) {
if (value === null || value === undefined) return null
return (
<div className="grid grid-cols-[180px_1fr] gap-x-3 py-1.5 border-b border-slate-800">
<span className="text-slate-400 text-sm">{label}</span>
<span className="text-slate-100 text-sm">{value}</span>
</div>
)
}
// ── Main page ────────────────────────────────────────────────────────────────
export default function KnowledgebaseFuelsPage() {
const [search, setSearch] = useState('')
const [roleFilter, setRoleFilter] = useState('All')
const [subcatFilter, setSubcatFilter] = useState(null)
const [selectedId, setSelectedId] = useState(SUBSTANCES[0].id)
// Filtered list
const filtered = useMemo(() => {
const q = search.toLowerCase()
return SUBSTANCES.filter(s => {
if (q && !s.name.toLowerCase().includes(q) && !s.symbol.toLowerCase().includes(q) && !s.formula.toLowerCase().includes(q)) return false
if (roleFilter === 'Fuels' && s.role !== 'fuel') return false
if (roleFilter === 'Oxidisers' && s.role !== 'oxidiser') return false
if (roleFilter === 'Monopropellants' && s.role !== 'monopropellant') return false
if (subcatFilter && s.subcategory !== subcatFilter) return false
return true
})
}, [search, roleFilter, subcatFilter])
const selected = SUBSTANCES.find(s => s.id === selectedId) ?? SUBSTANCES[0]
// Combinations for the selected substance
const combinations = useMemo(() => {
if (selected.role === 'monopropellant') return []
return COMBINATIONS.filter(c =>
(selected.role === 'fuel' && c.fuelId === selected.id) ||
(selected.role === 'oxidiser' && c.oxidiserId === selected.id)
)
}, [selected])
// Get partner substance for each combination
function getPartner(combo) {
const partnerId = selected.role === 'fuel' ? combo.oxidiserId : combo.fuelId
return SUBSTANCES.find(s => s.id === partnerId)
}
// Ensure selected item is still in filtered list; if not, show first filtered
const effectiveSelected =
filtered.find(s => s.id === selectedId)
? selected
: (filtered[0] ?? selected)
return (
<div className="flex flex-1 overflow-hidden">
{/* ── Left panel ────────────────────────────────────────────────────── */}
<div className="w-72 shrink-0 flex flex-col border-r border-slate-700 bg-slate-900">
{/* Search */}
<div className="p-3 border-b border-slate-700">
<input
type="text"
placeholder="Search substances…"
value={search}
onChange={e => setSearch(e.target.value)}
className="w-full px-3 py-1.5 rounded-md bg-slate-800 border border-slate-600 text-sm text-slate-100 placeholder-slate-500 focus:outline-none focus:border-blue-500"
/>
</div>
{/* Role filter pills */}
<div className="px-3 pt-2 pb-1 border-b border-slate-700 flex flex-wrap gap-1">
{ROLE_FILTERS.map(r => (
<button
key={r}
onClick={() => setRoleFilter(r)}
className={`px-2 py-0.5 rounded text-xs font-medium transition-colors ${
roleFilter === r
? 'bg-blue-600 text-white'
: 'bg-slate-700 text-slate-300 hover:bg-slate-600'
}`}
>
{r}
</button>
))}
</div>
{/* Subcategory filter pills */}
<div className="px-3 pt-2 pb-2 border-b border-slate-700 flex flex-wrap gap-1">
{SUBCATEGORY_FILTERS.map(sc => (
<button
key={sc}
onClick={() => setSubcatFilter(subcatFilter === sc ? null : sc)}
className={`px-2 py-0.5 rounded text-xs font-medium transition-colors ${
subcatFilter === sc
? 'bg-slate-500 text-white'
: 'bg-slate-800 text-slate-400 hover:bg-slate-700'
}`}
>
{sc}
</button>
))}
</div>
{/* Substance list */}
<div className="flex-1 overflow-y-auto">
{filtered.length === 0 && (
<p className="px-4 py-6 text-sm text-slate-500 text-center">No substances match your filters.</p>
)}
{filtered.map(s => (
<button
key={s.id}
onClick={() => setSelectedId(s.id)}
className={`w-full text-left px-3 py-2.5 border-b border-slate-800 transition-colors ${
effectiveSelected.id === s.id
? 'bg-blue-900/40 border-l-2 border-l-blue-500'
: 'hover:bg-slate-800'
}`}
>
<div className="flex items-center gap-2">
<span className={`text-xs font-mono font-bold px-1.5 py-0.5 rounded ${roleBadgeClass(s.role)}`}>
{s.symbol}
</span>
<span className="text-sm text-slate-100 font-medium truncate">{s.name}</span>
</div>
<div className="mt-1 flex gap-1">
<span className={`text-[10px] px-1.5 rounded ${subcategoryBadgeClass(s.subcategory)}`}>
{s.subcategory}
</span>
</div>
</button>
))}
</div>
</div>
{/* ── Right panel ───────────────────────────────────────────────────── */}
<div className="flex-1 overflow-y-auto bg-slate-950 p-6">
<SubstanceDetail substance={effectiveSelected} combinations={combinations} getPartner={getPartner} />
</div>
</div>
)
}
// ── Substance detail view ────────────────────────────────────────────────────
function SubstanceDetail({ substance: s, combinations, getPartner }) {
return (
<div className="max-w-3xl">
{/* Header */}
<div className="mb-6">
<div className="flex flex-wrap items-center gap-2 mb-1">
<h1 className="text-2xl font-bold text-white">{s.name}</h1>
<span className="font-mono text-slate-400 text-lg">{s.symbol}</span>
</div>
<div className="flex flex-wrap gap-2">
<span className={`text-xs font-bold px-2 py-1 rounded ${roleBadgeClass(s.role)}`}>
{roleLabel(s.role)}
</span>
<span className={`text-xs font-medium px-2 py-1 rounded ${subcategoryBadgeClass(s.subcategory)}`}>
{s.subcategory}
</span>
</div>
</div>
{/* Description */}
<p className="text-slate-300 text-sm leading-relaxed mb-8">{s.description}</p>
{/* Technical specs */}
<section className="mb-8">
<h2 className="text-sm font-semibold text-slate-400 uppercase tracking-wider mb-3">
Technical Specifications
</h2>
<div className="bg-slate-900 rounded-lg border border-slate-700 px-4 py-2">
<PropRow label="Formula" value={s.formula} />
<PropRow label="Molecular Weight" value={s.molecularWeight != null ? `${s.molecularWeight} g/mol` : null} />
<PropRow
label="Density"
value={s.density != null ? `${s.density} kg/m³${s.densityNote ? ` (${s.densityNote})` : ''}` : null}
/>
<PropRow
label="Boiling Point"
value={s.boilingPoint != null ? `${s.boilingPoint} °C (at 1 atm)` : null}
/>
<PropRow
label="Melting Point"
value={s.meltingPoint != null ? `${s.meltingPoint} °C` : null}
/>
<PropRow label="Storage Pressure" value={s.storagePressure} />
<PropRow
label="Autoignition Temp."
value={s.autoignitionTemp != null ? `${s.autoignitionTemp} °C` : 'N/A'}
/>
<PropRow label="Flammability Range" value={s.flammabilityRange} />
<PropRow
label="Hazards"
value={s.hazards?.length ? s.hazards.join(' · ') : null}
/>
<PropRow label="Toxicity" value={s.toxicity} />
{s.catalyticIsp != null && (
<PropRow label="Catalytic Isp (vac.)" value={`${s.catalyticIsp} s`} />
)}
</div>
</section>
{/* Engine examples */}
{s.engineExamples?.length > 0 && (
<section className="mb-8">
<h2 className="text-sm font-semibold text-slate-400 uppercase tracking-wider mb-3">
Engine / Motor Examples
</h2>
<p className="text-slate-300 text-sm">{s.engineExamples.join(', ')}</p>
</section>
)}
{/* Combinations */}
{s.role !== 'monopropellant' && (
<section>
<h2 className="text-sm font-semibold text-slate-400 uppercase tracking-wider mb-3">
Bipropellant Combinations
</h2>
{combinations.length === 0 ? (
<p className="text-slate-500 text-sm">No combination data available for this substance.</p>
) : (
<div className="space-y-3">
{combinations.map((combo, i) => {
const partner = getPartner(combo)
const partnerLabel = s.role === 'fuel'
? `with ${partner?.name ?? combo.oxidiserId} (oxidiser)`
: `with ${partner?.name ?? combo.fuelId} (fuel)`
return (
<div key={i} className="bg-slate-900 rounded-lg border border-slate-700 p-4">
<div className="flex flex-wrap items-center gap-2 mb-3">
<span className="text-white font-medium text-sm">{partnerLabel}</span>
<span className={`text-xs px-1.5 py-0.5 rounded border ${subcategoryBadgeClass(combo.energeticCategory.split(' ')[0])}`}>
{combo.energeticCategory}
</span>
{combo.isHypergolic && (
<span className="text-xs px-1.5 py-0.5 rounded bg-orange-800 text-orange-200 border border-orange-600 font-semibold">
Hypergolic
</span>
)}
</div>
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3 mb-3">
<Stat label="Vacuum Isp" value={combo.vacuumIsp != null ? `${combo.vacuumIsp} s` : '—'} />
<Stat label="Flame Temp" value={combo.flameTemp != null ? `${combo.flameTemp} K` : '—'} />
<Stat label="Optimal O/F" value={combo.optimalOF != null ? combo.optimalOF : '—'} />
<Stat label="Ref. Chamber P" value={combo.chamberPressureRef ?? '—'} />
</div>
{combo.notes && (
<p className="text-slate-400 text-xs mb-2">{combo.notes}</p>
)}
{combo.engines?.length > 0 && (
<p className="text-slate-500 text-xs">
<span className="text-slate-400 font-medium">Engines: </span>
{combo.engines.join(', ')}
</p>
)}
</div>
)
})}
</div>
)}
</section>
)}
</div>
)
}
function Stat({ label, value }) {
return (
<div className="bg-slate-800 rounded px-3 py-2">
<div className="text-[10px] text-slate-500 uppercase tracking-wider mb-0.5">{label}</div>
<div className="text-sm font-semibold text-slate-100">{value}</div>
</div>
)
}

456
src/pages/RocketPage.jsx Normal file
View File

@@ -0,0 +1,456 @@
import { useRef, useState } from 'react'
import { PropellantModal } from '../components/PropellantModal.jsx'
import { useRocketDesign } from '../hooks/useRocketDesign.js'
import DesignSection from '../components/engine/DesignSection.jsx'
import RocketModel3D from '../components/rocket/RocketModel3D.jsx'
import { formatValue } from '../engine/format.js'
import { exportRocketJSON, parseRocketImport, downloadBlob } from '../engine/rocketExportImport.js'
/* ── Tiny input / result primitives (self-contained, no unit dropdown) ── */
function NumInput({ label, value, onChange, units, step, placeholder }) {
const display = value == null ? '' : value
function handleChange(str) {
if (str === '') { onChange(null); return }
const n = parseFloat(str)
if (!isNaN(n)) onChange(n)
}
return (
<div className="flex items-center gap-2">
<label className="text-xs text-slate-400 w-44 shrink-0">{label}</label>
<input
type="number"
value={display}
step={step ?? 'any'}
placeholder={placeholder ?? ''}
onChange={e => handleChange(e.target.value)}
className="flex-1 min-w-0 w-24 px-2 py-1 bg-slate-800 border border-slate-600 rounded text-sm text-slate-100
focus:border-blue-500 focus:outline-none placeholder-slate-600"
/>
{units && <span className="text-xs text-slate-500 shrink-0 whitespace-nowrap">{units}</span>}
</div>
)
}
function SelectInput({ label, value, onChange, options }) {
return (
<div className="flex items-center gap-2">
<label className="text-xs text-slate-400 w-44 shrink-0">{label}</label>
<select
value={value}
onChange={e => onChange(e.target.value)}
className="px-2 py-1 bg-slate-800 border border-slate-600 rounded text-sm text-slate-100
focus:border-blue-500 focus:outline-none"
>
{options.map(o => <option key={o.value} value={o.value}>{o.label}</option>)}
</select>
</div>
)
}
function ResultRow({ label, value, unit, decimals }) {
const display = value !== null && value !== undefined && isFinite(value)
? formatValue(value)
: '—'
return (
<div className="flex items-center gap-2 text-sm">
<span className="text-slate-400 w-48 shrink-0">{label}</span>
<span className={`font-mono ${display === '—' ? 'text-slate-600' : 'text-green-400'}`}>
{display}
</span>
{unit && <span className="text-slate-500 text-xs">{unit}</span>}
</div>
)
}
function ResultSection({ title, children }) {
return (
<div className="mb-5">
<h3 className="text-xs font-semibold text-slate-400 uppercase tracking-wider mb-2 pb-1 border-b border-slate-800">
{title}
</h3>
<div className="space-y-1">{children}</div>
</div>
)
}
/* ── Engine summary chip ─────────────────────────────────────────────── */
function SummaryChip({ label, value, unit }) {
const display = value !== null && value !== undefined && isFinite(value)
? formatValue(value)
: '—'
return (
<div className="flex flex-col items-center px-3 py-1.5 bg-slate-800 rounded-lg border border-slate-700 min-w-[72px]">
<span className="text-xs text-slate-400">{label}</span>
<span className="font-mono text-sm text-green-400">{display}</span>
{unit && <span className="text-[10px] text-slate-500">{unit}</span>}
</div>
)
}
/* ── Main page ───────────────────────────────────────────────────────── */
export default function RocketPage() {
const fileRef = useRef(null)
const importRef = useRef(null)
const [showPropModal, setShowPropModal] = useState(false)
const [selectedPropName, setSelectedPropName] = useState(null)
const {
engineData, loadEngine, engineSummary,
outerRadius, setOuterRadius,
suggestedRadius,
tankConfig, setTankConfig,
propDensities, setPropDensities,
payload, setPayload,
structure, setStructure,
effectiveBurnTime,
geometry,
diagnosis,
loadRocketDesign,
} = useRocketDesign()
function handlePropellantApply(propellant) {
const { rhoFuel, rhoOx } = propellant.values
setPropDensities(prev => ({
rhoFuel: rhoFuel ?? prev.rhoFuel,
rhoOx: rhoOx ?? prev.rhoOx,
}))
setSelectedPropName(propellant.name)
}
function handleFileImport(e) {
const file = e.target.files[0]
if (!file) return
const reader = new FileReader()
reader.onload = ev => {
try {
loadEngine(ev.target.result)
} catch (err) {
alert('Import failed: ' + err.message)
}
}
reader.readAsText(file)
e.target.value = ''
}
function handleExportJSON() {
const blob = exportRocketJSON({ outerRadius, tankConfig, propDensities, payload, structure, engineData, geometry })
downloadBlob(blob, 'rocket-design.json')
}
function handleImportRocket(e) {
const file = e.target.files[0]
if (!file) return
const reader = new FileReader()
reader.onload = ev => {
try {
const parsed = parseRocketImport(ev.target.result)
loadRocketDesign(parsed)
} catch (err) {
alert('Import failed: ' + err.message)
}
}
reader.readAsText(file)
e.target.value = ''
}
// Displayed outer radius in mm
const displayRadius = outerRadius ?? suggestedRadius
return (
<div className="flex flex-col flex-1 overflow-hidden">
{/* Sub-header */}
<div className="flex items-center justify-between px-5 py-2 bg-slate-900 border-b border-slate-700 shrink-0">
<h1 className="text-sm font-semibold text-slate-200">Rocket Design</h1>
<div className="flex items-center gap-3">
{geometry && (
<span className="text-xs text-slate-500">
L = {formatValue(geometry.totalLength * 1000)} mm
&nbsp;|&nbsp;
D = {formatValue((geometry.outerRadius ?? 0) * 2000)} mm
</span>
)}
<div className="flex gap-2">
<button
onClick={handleExportJSON}
className="px-3 py-1 text-xs bg-slate-700 hover:bg-slate-600 rounded text-slate-200 transition-colors"
>
JSON
</button>
<button
onClick={() => importRef.current?.click()}
className="px-3 py-1 text-xs bg-slate-700 hover:bg-slate-600 rounded text-slate-200 transition-colors"
>
Import
</button>
<input
ref={importRef}
type="file"
accept=".json"
className="hidden"
onChange={handleImportRocket}
/>
</div>
</div>
</div>
{/* Three-column layout */}
<div className="flex flex-1 overflow-hidden">
{/* ── Left: Inputs ── */}
<div className="w-[420px] shrink-0 overflow-y-auto p-4 border-r border-slate-700">
<DesignSection title="Engine Import">
<div className="flex flex-col gap-3">
<button
onClick={() => fileRef.current?.click()}
className="w-full px-3 py-2 text-xs bg-slate-700 hover:bg-slate-600 rounded text-slate-200 transition-colors border border-slate-600"
>
Import Engine JSON
</button>
<input
ref={fileRef}
type="file"
accept=".json"
className="hidden"
onChange={handleFileImport}
/>
{engineSummary ? (
<div className="flex flex-wrap gap-2">
<SummaryChip label="Thrust" value={engineSummary.F != null ? engineSummary.F / 1000 : null} unit="kN" />
<SummaryChip label="Isp" value={engineSummary.Isp} unit="s" />
<SummaryChip label="ṁ" value={engineSummary.mdot} unit="kg/s" />
<SummaryChip label="O/F" value={engineSummary.OF} unit="—" />
</div>
) : (
<p className="text-xs text-slate-600 italic">No engine loaded export from the Engine Design page</p>
)}
</div>
</DesignSection>
<DesignSection title="Vehicle Geometry">
<NumInput
label="Outer Diameter"
value={displayRadius != null ? parseFloat((displayRadius * 2000).toPrecision(6)) : null}
onChange={v => setOuterRadius(v != null ? v / 2000 : null)}
units="mm"
step="10"
placeholder={suggestedRadius ? `${(suggestedRadius * 2000).toFixed(0)}` : 'e.g. 300'}
/>
{suggestedRadius && !outerRadius && (
<p className="text-[11px] text-slate-500 pl-[11.5rem]">
Suggested from nozzle exit diameter edit to override
</p>
)}
{!suggestedRadius && !outerRadius && engineData && (
<p className="text-[11px] text-amber-500 pl-[11.5rem]">
Enter outer diameter to generate model
</p>
)}
</DesignSection>
<DesignSection title="Tank Configuration">
<SelectInput
label="Arrangement"
value={tankConfig.arrangement}
onChange={v => setTankConfig(c => ({ ...c, arrangement: v }))}
options={[
{ value: 'tandem', label: 'Tandem (stacked)' },
{ value: 'coaxial', label: 'Coaxial (concentric)' },
]}
/>
{tankConfig.arrangement === 'coaxial' && (
<SelectInput
label="Inner Propellant"
value={tankConfig.innerPropellant}
onChange={v => setTankConfig(c => ({ ...c, innerPropellant: v }))}
options={[
{ value: 'fuel', label: 'Fuel' },
{ value: 'ox', label: 'Oxidizer' },
]}
/>
)}
</DesignSection>
<DesignSection title="Propellant">
<div className="flex items-center gap-2">
<button
onClick={() => setShowPropModal(true)}
className="px-3 py-1.5 text-xs bg-slate-700 hover:bg-slate-600 rounded text-slate-200 transition-colors border border-slate-600"
>
Select from Database
</button>
{selectedPropName && (
<span className="text-xs text-blue-300 truncate">{selectedPropName}</span>
)}
</div>
<NumInput
label="Fuel Density (ρ_f)"
value={propDensities.rhoFuel}
onChange={v => setPropDensities(p => ({ ...p, rhoFuel: v ?? 800 }))}
units="kg/m³"
step="10"
placeholder="800"
/>
<NumInput
label="Oxidizer Density (ρ_ox)"
value={propDensities.rhoOx}
onChange={v => setPropDensities(p => ({ ...p, rhoOx: v ?? 1140 }))}
units="kg/m³"
step="10"
placeholder="1140"
/>
<NumInput
label="Burn Time"
value={structure.burnTime}
onChange={v => setStructure(s => ({ ...s, burnTime: v }))}
units="s"
step="1"
placeholder={engineData?.inputs?.burnTime ?? '30'}
/>
{effectiveBurnTime != null && !structure.burnTime && (
<p className="text-[11px] text-slate-500 pl-[11.5rem]">
Using engine burn time: {effectiveBurnTime} s
</p>
)}
</DesignSection>
<DesignSection title="Payload">
<NumInput
label="Payload Mass"
value={payload.mass}
onChange={v => setPayload(p => ({ ...p, mass: v ?? 0 }))}
units="kg"
step="1"
placeholder="0"
/>
<NumInput
label="Payload Bay Length"
value={payload.bayLength != null ? parseFloat((payload.bayLength * 1000).toPrecision(6)) : null}
onChange={v => setPayload(p => ({ ...p, bayLength: v != null ? v / 1000 : 0 }))}
units="mm"
step="10"
placeholder="0"
/>
</DesignSection>
<DesignSection title="Structure">
<NumInput
label="Structural Mass Fraction"
value={structure.structMassFraction}
onChange={v => setStructure(s => ({ ...s, structMassFraction: v ?? 0.10 }))}
units="—"
step="0.01"
placeholder="0.10"
/>
</DesignSection>
</div>
{/* ── Centre: 3D Model ── */}
<div className="flex-1 relative border-r border-slate-700 bg-slate-950/50">
<RocketModel3D geometry={geometry} />
{/* Requirements checklist — shown when model can't render */}
{!geometry && engineData && (
<div className="absolute inset-0 flex flex-col items-center justify-center pointer-events-none z-10">
<div className="bg-slate-900 border border-slate-700 rounded-xl p-5 shadow-2xl min-w-[320px]">
<p className="text-xs font-semibold text-slate-400 uppercase tracking-wider mb-3">
Missing requirements
</p>
<div className="space-y-2">
{diagnosis.map(item => (
<div key={item.key} className="flex items-start gap-2 text-sm">
<span className={`mt-0.5 shrink-0 ${item.ok ? 'text-green-400' : 'text-red-400'}`}>
{item.ok ? '✓' : '✗'}
</span>
<div className="flex-1 min-w-0">
<span className={item.ok ? 'text-slate-300' : 'text-slate-200'}>
{item.label}
</span>
{item.value && (
<span className="ml-2 font-mono text-xs text-green-400">{item.value}</span>
)}
{!item.ok && item.hint && (
<p className="text-xs text-slate-500 mt-0.5">{item.hint}</p>
)}
</div>
</div>
))}
</div>
</div>
</div>
)}
{/* Legend */}
{geometry && (
<div className="absolute bottom-4 left-1/2 -translate-x-1/2 flex gap-3 text-[10px] text-slate-400 bg-slate-900/80 rounded-lg px-3 py-1.5 border border-slate-700">
<span className="flex items-center gap-1"><span className="inline-block w-2.5 h-2.5 rounded-sm bg-indigo-500 opacity-70"/> Payload</span>
<span className="flex items-center gap-1"><span className="inline-block w-2.5 h-2.5 rounded-sm bg-cyan-500 opacity-70"/> Oxidizer</span>
<span className="flex items-center gap-1"><span className="inline-block w-2.5 h-2.5 rounded-sm bg-amber-500 opacity-70"/> Fuel</span>
<span className="flex items-center gap-1"><span className="inline-block w-2.5 h-2.5 rounded-sm bg-blue-500 opacity-70"/> Engine</span>
</div>
)}
</div>
{/* ── Right: Results ── */}
<div className="w-[380px] shrink-0 overflow-y-auto p-4">
<ResultSection title="Propellant & Tanks">
<ResultRow label="Fuel Mass" value={geometry?.m_fuel} unit="kg" />
<ResultRow label="Oxidizer Mass" value={geometry?.m_ox} unit="kg" />
<ResultRow label="Total Propellant Mass" value={geometry?.m_prop} unit="kg" />
<ResultRow label="Fuel Volume" value={geometry?.V_fuel != null ? geometry.V_fuel * 1000 : null} unit="L" />
<ResultRow label="Oxidizer Volume" value={geometry?.V_ox != null ? geometry.V_ox * 1000 : null} unit="L" />
</ResultSection>
<ResultSection title="Tank Geometry">
{geometry?.arrangement === 'tandem' ? (
<>
<ResultRow label="Fuel Tank Length" value={geometry?.L_tank_fuel != null ? geometry.L_tank_fuel * 1000 : null} unit="mm" />
<ResultRow label="Oxidizer Tank Length" value={geometry?.L_tank_ox != null ? geometry.L_tank_ox * 1000 : null} unit="mm" />
</>
) : (
<>
<ResultRow label="Combined Tank Length" value={geometry?.L_tank != null ? geometry.L_tank * 1000 : null} unit="mm" />
<ResultRow label="Inner Tank Radius" value={geometry?.r_inner != null ? geometry.r_inner * 1000 : null} unit="mm" />
</>
)}
<ResultRow label="Total Tank Length" value={geometry?.L_tank != null ? geometry.L_tank * 1000 : null} unit="mm" />
</ResultSection>
<ResultSection title="Mass Budget">
<ResultRow label="Structural Mass" value={geometry?.m_struct} unit="kg" />
<ResultRow label="Payload Mass" value={geometry?.m_payload} unit="kg" />
<ResultRow label="Dry Mass" value={geometry?.m_dry} unit="kg" />
<ResultRow label="Wet Mass" value={geometry?.m_wet} unit="kg" />
<ResultRow label="Mass Ratio (Wet/Dry)" value={geometry?.massRatio} unit="—" />
</ResultSection>
<ResultSection title="Performance">
<ResultRow label="Delta-v" value={geometry?.deltaV} unit="m/s" />
<ResultRow label="TWR (liftoff)" value={geometry?.TWR} unit="—" />
</ResultSection>
<ResultSection title="Vehicle Dimensions">
<ResultRow label="Outer Diameter" value={geometry?.outerRadius != null ? geometry.outerRadius * 2000 : null} unit="mm" />
<ResultRow label="Nose Length" value={geometry?.L_nose != null ? geometry.L_nose * 1000 : null} unit="mm" />
<ResultRow label="Payload Bay Length" value={geometry?.L_payload != null ? geometry.L_payload * 1000 : null} unit="mm" />
<ResultRow label="Engine Length" value={geometry?.L_engine != null ? geometry.L_engine * 1000 : null} unit="mm" />
<ResultRow label="Total Length" value={geometry?.totalLength != null ? geometry.totalLength * 1000 : null} unit="mm" />
</ResultSection>
</div>
</div>
{showPropModal && (
<PropellantModal
onClose={() => setShowPropModal(false)}
onApply={handlePropellantApply}
description="Select a propellant to pre-fill fuel and oxidizer densities for tank sizing."
/>
)}
</div>
)
}

173
src/pages/Solver.jsx Normal file
View File

@@ -0,0 +1,173 @@
import { DndContext, DragOverlay, PointerSensor, useSensor, useSensors } from '@dnd-kit/core'
import { useState } from 'react'
import { VariablePalette } from '../components/VariablePalette.jsx'
import { Workspace } from '../components/Workspace.jsx'
import { ResultsPanel } from '../components/ResultsPanel.jsx'
import { PaletteCard } from '../components/VariableCard.jsx'
import { PropellantModal } from '../components/PropellantModal.jsx'
import { EquationBrowser } from '../components/EquationBrowser.jsx'
import { useSolver } from '../hooks/useSolver.js'
import { EQUATIONS } from '../engine/equations.js'
import { buildExportData, exportJSON, parseImport, downloadBlob } from '../engine/exportImport.js'
import { generateOdt } from '../engine/exportOdt.js'
export default function Solver() {
const {
workspaceVarIds,
userValues,
solved,
missingReport,
allKnown,
unitSelections,
getUnit,
setUnit,
sciNotation,
toggleSciNotation,
addVariable,
addVariables,
removeVariable,
setValue,
addPreset,
applyPropellant,
clearWorkspace,
importWorkspace,
} = useSolver()
const [activeVarId, setActiveVarId] = useState(null)
const [showPropellants, setShowPropellants] = useState(false)
const [showEquations, setShowEquations] = useState(false)
const sensors = useSensors(
useSensor(PointerSensor, { activationConstraint: { distance: 6 } })
)
function handleDragStart(event) {
const { varId } = event.active.data.current ?? {}
if (varId) setActiveVarId(varId)
}
function handleDragEnd(event) {
const { over, active } = event
if (over?.id === 'workspace') {
const { varId } = active.data.current ?? {}
if (varId) addVariable(varId)
}
setActiveVarId(null)
}
async function handleExportOdt() {
const data = buildExportData(workspaceVarIds, userValues, solved, unitSelections, getUnit, sciNotation)
const blob = await generateOdt(data)
downloadBlob(blob, 'rocketry-workspace.odt')
}
function handleExportJSON() {
const data = buildExportData(workspaceVarIds, userValues, solved, unitSelections, getUnit, sciNotation)
const blob = exportJSON(data, workspaceVarIds, userValues, unitSelections)
downloadBlob(blob, 'rocketry-workspace.json')
}
function handleImportJSON(jsonString) {
try {
const workspace = parseImport(jsonString)
importWorkspace(workspace)
} catch (err) {
alert(`Import failed: ${err.message}`)
}
}
return (
<DndContext sensors={sensors} onDragStart={handleDragStart} onDragEnd={handleDragEnd}>
<div className="flex flex-1 min-h-0 overflow-hidden flex-col">
{/* Solver sub-header */}
<div className="flex items-center gap-2 px-5 py-2 border-b border-slate-700 bg-slate-900 shrink-0">
<p className="text-xs text-slate-400">
Add variables, enter known values, and unknowns solve automatically.
</p>
<div className="ml-auto flex items-center gap-2">
<button
onClick={toggleSciNotation}
title="Toggle scientific notation"
className={`flex items-center gap-1.5 px-3 py-2 rounded-lg border text-sm font-medium transition-colors ${
sciNotation
? 'bg-blue-700 border-blue-500 text-white'
: 'bg-slate-700 border-slate-600 text-slate-300 hover:bg-slate-600 hover:border-slate-500'
}`}
>
10ˣ
</button>
<button
onClick={() => setShowEquations(true)}
className="flex items-center gap-2 px-4 py-2 rounded-lg bg-slate-700 hover:bg-slate-600 border border-slate-600 hover:border-slate-500 text-sm text-slate-200 font-medium transition-colors"
>
<span></span> Equations
</button>
<button
onClick={() => setShowPropellants(true)}
className="flex items-center gap-2 px-4 py-2 rounded-lg bg-slate-700 hover:bg-slate-600 border border-slate-600 hover:border-slate-500 text-sm text-slate-200 font-medium transition-colors"
>
<span></span> Propellants
</button>
</div>
</div>
{/* Three-panel layout */}
<div className="flex flex-1 min-h-0 overflow-hidden">
<VariablePalette
workspaceVarIds={workspaceVarIds}
onAddVariable={addVariable}
/>
<Workspace
workspaceVarIds={workspaceVarIds}
userValues={userValues}
solved={solved}
onRemove={removeVariable}
onValueChange={setValue}
onAddPreset={addPreset}
onClear={clearWorkspace}
getUnit={getUnit}
setUnit={setUnit}
sciNotation={sciNotation}
/>
<ResultsPanel
workspaceVarIds={workspaceVarIds}
userValues={userValues}
solved={solved}
missingReport={missingReport}
getUnit={getUnit}
sciNotation={sciNotation}
onExportOdt={handleExportOdt}
onExportJSON={handleExportJSON}
onImportJSON={handleImportJSON}
/>
</div>
</div>
{showPropellants && (
<PropellantModal
onClose={() => setShowPropellants(false)}
onApply={applyPropellant}
existingVarIds={workspaceVarIds}
/>
)}
{showEquations && (
<EquationBrowser
equations={EQUATIONS}
allKnown={allKnown}
workspaceVarIds={workspaceVarIds}
onAddVariables={varIds => addVariables(varIds)}
onClose={() => setShowEquations(false)}
/>
)}
<DragOverlay dropAnimation={null}>
{activeVarId ? (
<div className="opacity-90 rotate-2 scale-105">
<PaletteCard varId={activeVarId} isInWorkspace={false} />
</div>
) : null}
</DragOverlay>
</DndContext>
)
}

7
vite.config.js Normal file
View File

@@ -0,0 +1,7 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [react(), tailwindcss()],
})