mirror of
https://github.com/Azgaar/Fantasy-Map-Generator.git
synced 2025-12-16 17:31:24 +01:00
Merge pull request #1 from n8k99/claude/claude-md-mhy85sj7tlvzwb5w-01QzBpdgGJXE5Qk3JaNupuxM
Claude/claude md mhy85sj7tlvzwb5w 01 qz bpdg gjxe5 qk3 ja nupux m
This commit is contained in:
commit
fe15bd0cf0
11 changed files with 2211 additions and 7 deletions
89
.github/copilot-instructions.md
vendored
Normal file
89
.github/copilot-instructions.md
vendored
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
# Fantasy Map Generator
|
||||
|
||||
Azgaar's Fantasy Map Generator is a client-side JavaScript web application for creating fantasy maps. It generates detailed fantasy worlds with countries, cities, rivers, biomes, and cultural elements.
|
||||
|
||||
Always reference these instructions first and fallback to search or bash commands only when you encounter unexpected information that does not match the info here.
|
||||
|
||||
## Working Effectively
|
||||
|
||||
- **CRITICAL**: This is a static web application - NO build process needed. No npm install, no compilation, no bundling.
|
||||
- Run the application using HTTP server (required - cannot run with file:// protocol):
|
||||
- `python3 -m http.server 8000` - takes 2-3 seconds to start
|
||||
- Access at: `http://localhost:8000`
|
||||
|
||||
## Validation
|
||||
|
||||
- Always manually validate any changes by:
|
||||
1. Starting the HTTP server (NEVER CANCEL - wait for full startup)
|
||||
2. Navigate to the application in browser
|
||||
3. Click the "►" button to open the menu and generate a new map
|
||||
4. **CRITICAL VALIDATION**: Verify the map generates with countries, cities, roads, and geographic features
|
||||
5. Test UI interaction: click "Layers" button, verify layer controls work
|
||||
6. Test regeneration: click "New Map!" button, verify new map generates correctly
|
||||
- **Known Issues**: Google Analytics and font loading errors are normal (blocked external resources)
|
||||
|
||||
## Repository Structure
|
||||
|
||||
### Core Files
|
||||
|
||||
- `index.html` - Main application entry point
|
||||
- `main.js` - Core application logic
|
||||
- `versioning.js` - Version management and update handling
|
||||
|
||||
### Key Directories
|
||||
|
||||
- `modules/` - core functionality modules:
|
||||
- `modules/ui/` - UI components (editors, tools, style management)
|
||||
- `modules/dynamic/` - runtime modules (export, installation)
|
||||
- `modules/renderers/` - drawing and rendering logic
|
||||
- `utils/` - utility libraries (math, arrays, strings, etc.)
|
||||
- `styles/` - visual style presets (JSON files)
|
||||
- `libs/` - Third-party libraries (D3.js, TinyMCE, etc.)
|
||||
- `images/` - backgrounds, UI elements
|
||||
- `charges/` - heraldic symbols and coat of arms elements
|
||||
- `config/` - Heightmap templates and configurations
|
||||
- `heightmaps/` - Terrain generation data
|
||||
|
||||
## Common Tasks
|
||||
|
||||
### Making Code Changes
|
||||
|
||||
1. Edit JavaScript files directly (no compilation needed)
|
||||
2. Refresh browser to see changes immediately
|
||||
3. **ALWAYS test map generation** after making changes
|
||||
4. Update version in `versioning.js` for all changes
|
||||
5. Update file hashes in `index.html` for changed files (format: `file.js?v=1.108.1`)
|
||||
|
||||
### Debugging Map Generation
|
||||
|
||||
- Open browser developer tools console
|
||||
- Look for timing logs, e.g. "TOTAL: ~0.76s"
|
||||
- Map generation logs show each step (heightmap, rivers, states, etc.)
|
||||
- Error messages will indicate specific generation failures
|
||||
|
||||
### Testing Different Map Types
|
||||
|
||||
- Use "New Map!" button for quick regeneration
|
||||
- Access "Layers" menu to change map visualization
|
||||
- Available presets: Political, Cultural, Religions, Biomes, Heightmap, Physical, Military
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Application Won't Load
|
||||
|
||||
- Ensure using HTTP server (not file://)
|
||||
- Check console for JavaScript errors
|
||||
- Verify all files are present in repository
|
||||
|
||||
### Map Generation Fails
|
||||
|
||||
- Check browser console for error messages
|
||||
- Look for specific module failures in generation logs
|
||||
- Try refreshing page and generating new map
|
||||
|
||||
### Performance Issues
|
||||
|
||||
- Map generation should complete in ~1 second for standard configurations
|
||||
- If slower, check browser console for errors
|
||||
|
||||
Remember: This is a sophisticated client-side application that generates complete fantasy worlds with political systems, geography, cultures, and detailed cartographic elements. Always validate that your changes preserve the core map generation functionality.
|
||||
832
CLAUDE.md
Normal file
832
CLAUDE.md
Normal file
|
|
@ -0,0 +1,832 @@
|
|||
# CLAUDE.md - AI Assistant Guide for Fantasy Map Generator
|
||||
|
||||
## Project Overview
|
||||
|
||||
**Fantasy Map Generator** (FMG) is a free web-based application for creating and editing fantasy maps. It's a massive client-side JavaScript application designed for fantasy writers, game masters, and cartographers.
|
||||
|
||||
- **Repository**: https://github.com/Azgaar/Fantasy-Map-Generator
|
||||
- **Live App**: https://azgaar.github.io/Fantasy-Map-Generator
|
||||
- **Language**: Pure JavaScript (ES6+)
|
||||
- **License**: MIT
|
||||
- **Primary Author**: Azgaar (azgaar.fmg@yandex.com)
|
||||
|
||||
## Architecture & Technology Stack
|
||||
|
||||
### Core Technologies
|
||||
|
||||
- **Pure JavaScript (ES6+)**: NO build system, transpilation, or bundling
|
||||
- **D3.js v7**: SVG manipulation, data visualization, zoom/pan interactions
|
||||
- **jQuery 3.1.1 + jQuery UI**: DOM manipulation, dialogs, UI components
|
||||
- **Progressive Web App (PWA)**: Service worker caching, offline support, IndexedDB storage
|
||||
|
||||
### Key Libraries (`/libs/`)
|
||||
|
||||
```
|
||||
d3.min.js # Main visualization library
|
||||
delaunator.min.js # Delaunay triangulation for Voronoi mesh
|
||||
alea.min.js # Seedable random number generator
|
||||
three.min.js # 3D rendering support
|
||||
jszip.min.js # ZIP file creation for exports
|
||||
polylabel.min.js # Label placement optimization
|
||||
tinymce/ # Rich text editor for notes
|
||||
```
|
||||
|
||||
### Architecture Pattern
|
||||
|
||||
**Global Object Pattern**: No module bundler, everything attached to `window` object:
|
||||
|
||||
```javascript
|
||||
// Global data structures (from main.js:151-158)
|
||||
let grid = {}; // Initial Voronoi graph
|
||||
let pack = {}; // Main packed data structure
|
||||
let seed; // Map generation seed
|
||||
let mapId; // Unique map identifier
|
||||
let mapHistory = []; // Undo/redo history
|
||||
let modules = {}; // Module initialization flags
|
||||
let notes = []; // User notes
|
||||
```
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
Fantasy-Map-Generator/
|
||||
├── index.html # 8,184-line monolithic HTML (entire UI)
|
||||
├── main.js # 1,288 lines - initialization, globals, SVG setup
|
||||
├── versioning.js # Version management (currently v1.108.11)
|
||||
├── sw.js # Service worker for PWA caching
|
||||
│
|
||||
├── modules/ # Core application logic (232 JS files)
|
||||
│ ├── dynamic/ # Dynamically imported modules
|
||||
│ │ ├── editors/ # Advanced editors (states, religions, cultures)
|
||||
│ │ ├── overview/ # Data visualization tools
|
||||
│ │ └── *.js # Auto-update, installation, hierarchy-tree
|
||||
│ ├── io/ # Input/Output operations
|
||||
│ │ ├── cloud.js # Cloud storage (Dropbox)
|
||||
│ │ ├── export.js # Map export functionality
|
||||
│ │ ├── load.js # Map loading
|
||||
│ │ └── save.js # Map saving
|
||||
│ ├── renderers/ # SVG rendering (12 files)
|
||||
│ │ ├── draw-borders.js
|
||||
│ │ ├── draw-heightmap.js
|
||||
│ │ ├── draw-markers.js
|
||||
│ │ └── ...
|
||||
│ └── ui/ # UI editors and tools (~35 files)
|
||||
│ ├── editors.js # Common editor functions
|
||||
│ ├── options.js # Map configuration
|
||||
│ └── ...
|
||||
│
|
||||
├── utils/ # Utility functions (15 files)
|
||||
│ ├── arrayUtils.js
|
||||
│ ├── colorUtils.js
|
||||
│ ├── commonUtils.js # debounce, throttle
|
||||
│ ├── graphUtils.js # Graph algorithms (297 lines)
|
||||
│ ├── pathUtils.js # SVG path operations (222 lines)
|
||||
│ └── ...
|
||||
│
|
||||
├── charges/ # 400+ SVG heraldic symbols
|
||||
├── components/ # Reusable UI components
|
||||
├── config/ # Heightmap template configurations
|
||||
├── heightmaps/ # Precreated heightmap resources
|
||||
├── images/ # Application images/icons
|
||||
├── styles/ # 12 JSON theme presets (default, night, etc.)
|
||||
│
|
||||
└── .github/
|
||||
├── pull_request_template.md
|
||||
└── ISSUE_TEMPLATE/
|
||||
```
|
||||
|
||||
## Key Files
|
||||
|
||||
### Entry Points
|
||||
|
||||
- **index.html** (line 1-8184): Entire UI structure, loads all scripts
|
||||
- **main.js** (line 1-1288): Initializes globals, SVG layers, data structures
|
||||
- **versioning.js**: Current version: `1.108.11` (semantic versioning)
|
||||
|
||||
### Core Generators
|
||||
|
||||
```javascript
|
||||
modules/burgs-and-states.js # City/state generation (1,018 lines)
|
||||
modules/cultures-generator.js # Culture system (1,039 lines)
|
||||
modules/names-generator.js # Name generation (3,371 lines)
|
||||
modules/heightmap-generator.js # Terrain generation (445 lines)
|
||||
modules/river-generator.js # River systems (507 lines)
|
||||
modules/routes-generator.js # Road/trail generation (537 lines)
|
||||
modules/religions-generator.js # Religion system (757 lines)
|
||||
modules/military-generator.js # Military units (405 lines)
|
||||
modules/markers-generator.js # Map markers (1,163 lines)
|
||||
modules/coa-generator.js # Coat of arms logic (1,015 lines)
|
||||
modules/coa-renderer.js # COA SVG rendering (2,527 lines)
|
||||
modules/provinces-generator.js # Province subdivision (302 lines)
|
||||
modules/zones-generator.js # Zone assignment (430 lines)
|
||||
```
|
||||
|
||||
## Data Model
|
||||
|
||||
### Primary Data Structure: `pack` Object
|
||||
|
||||
The `pack` object is the heart of FMG's data model. It contains the entire map state using **typed arrays** for performance:
|
||||
|
||||
```javascript
|
||||
pack = {
|
||||
cells: {
|
||||
i: Uint32Array, // Cell indices
|
||||
v: Array, // Adjacent vertices
|
||||
c: Array, // Adjacent cells
|
||||
b: Uint8Array, // Border flags
|
||||
h: Uint8Array, // Height (0-100)
|
||||
temp: Int8Array, // Temperature
|
||||
prec: Uint8Array, // Precipitation
|
||||
f: Uint16Array, // Feature (biome) ID
|
||||
t: Int8Array, // Terrain type
|
||||
haven: Uint16Array, // Harbor ID
|
||||
harbor: Uint8Array, // Harbor presence
|
||||
fl: Uint16Array, // Flux (river flow)
|
||||
r: Uint16Array, // River ID
|
||||
conf: Uint8Array, // River confluence
|
||||
pop: Float32Array, // Population density
|
||||
culture: Uint16Array, // Culture ID
|
||||
burg: Uint16Array, // Settlement ID
|
||||
road: Uint16Array, // Road ID
|
||||
route: Uint16Array, // Route ID
|
||||
crossroad: Uint16Array,
|
||||
province: Uint16Array,
|
||||
state: Uint16Array, // State ownership
|
||||
religion: Uint16Array
|
||||
},
|
||||
vertices: {
|
||||
p: Array, // Point coordinates [x, y]
|
||||
v: Array, // Adjacent vertices
|
||||
c: Array // Adjacent cells
|
||||
},
|
||||
features: Array, // Biome/terrain features
|
||||
cultures: Array, // Culture definitions
|
||||
states: Array, // Political states
|
||||
burgs: Array, // Cities/towns/settlements
|
||||
religions: Array, // Religion definitions
|
||||
provinces: Array, // Province data
|
||||
rivers: Array, // River definitions
|
||||
markers: Array, // Map markers
|
||||
notes: Array // User notes
|
||||
}
|
||||
```
|
||||
|
||||
### Typed Array Constants
|
||||
|
||||
```javascript
|
||||
// From main.js:16-20
|
||||
const INT8_MAX = 127;
|
||||
const UINT8_MAX = 255;
|
||||
const UINT16_MAX = 65535;
|
||||
const UINT32_MAX = 4294967295;
|
||||
```
|
||||
|
||||
### Map Generation Pipeline
|
||||
|
||||
```
|
||||
Heightmap Generation
|
||||
↓
|
||||
Voronoi/Delaunay Mesh
|
||||
↓
|
||||
Biomes & Climate
|
||||
↓
|
||||
Rivers & Water Features
|
||||
↓
|
||||
Cultures Assignment
|
||||
↓
|
||||
States & Capitals
|
||||
↓
|
||||
Burgs (Cities/Towns)
|
||||
↓
|
||||
Provinces Subdivision
|
||||
↓
|
||||
Routes (Roads/Trails)
|
||||
↓
|
||||
Religions Distribution
|
||||
↓
|
||||
Military Units
|
||||
↓
|
||||
Final Rendering
|
||||
```
|
||||
|
||||
## SVG Layer Organization
|
||||
|
||||
FMG uses 70+ predefined SVG layer groups in specific render order (from main.js:40-94):
|
||||
|
||||
```javascript
|
||||
// Background to foreground rendering order
|
||||
ocean → oceanLayers → oceanPattern → lakes → landmass →
|
||||
texture → terrs → biomes → cells → rivers → terrain →
|
||||
regions → borders → routes → temperature → coastline →
|
||||
ice → population → emblems → labels → icons → armies →
|
||||
markers → fogging → ruler → debug
|
||||
```
|
||||
|
||||
**Important**: When modifying rendering, respect this layer order to avoid z-index issues.
|
||||
|
||||
## Code Conventions & Style
|
||||
|
||||
### General Style
|
||||
|
||||
1. **Strict Mode**: Every file starts with `"use strict";`
|
||||
2. **No Semicolons**: Generally omitted (but inconsistent)
|
||||
3. **Naming Conventions**:
|
||||
- `camelCase` for variables and functions
|
||||
- `PascalCase` for classes
|
||||
- `SCREAMING_SNAKE_CASE` for constants
|
||||
4. **Comments**: Minimal; code is mostly self-documenting
|
||||
5. **String Quotes**: Mixed single and double quotes (no standard)
|
||||
|
||||
### Module Patterns
|
||||
|
||||
**IIFE (Immediately Invoked Function Expression)**:
|
||||
|
||||
```javascript
|
||||
// From burgs-and-states.js:3
|
||||
window.BurgsAndStates = (() => {
|
||||
const generate = () => {
|
||||
// Implementation
|
||||
};
|
||||
|
||||
return { generate };
|
||||
})();
|
||||
|
||||
// Usage elsewhere:
|
||||
BurgsAndStates.generate();
|
||||
```
|
||||
|
||||
**Global Function Pattern**:
|
||||
|
||||
```javascript
|
||||
// From draw-borders.js:3
|
||||
function drawBorders() {
|
||||
// Direct global function
|
||||
}
|
||||
```
|
||||
|
||||
**Utility Module Pattern**:
|
||||
|
||||
```javascript
|
||||
// From utils/commonUtils.js
|
||||
function debounce(func, ms) { /* ... */ }
|
||||
function throttle(func, ms) { /* ... */ }
|
||||
// Directly callable globally
|
||||
```
|
||||
|
||||
### Loading Strategy
|
||||
|
||||
**index.html** loads scripts in three phases:
|
||||
|
||||
1. **Libraries** (D3, jQuery, etc.)
|
||||
2. **Core Modules** (synchronous, no `defer`)
|
||||
3. **UI Modules** (with `defer` attribute)
|
||||
4. **Dynamic Imports** (ES6 `import()` for lazy loading)
|
||||
|
||||
Example from index.html:
|
||||
```html
|
||||
<!-- Core generators loaded first -->
|
||||
<script src="modules/names-generator.js?v=1.108.11"></script>
|
||||
<script src="modules/cultures-generator.js?v=1.108.11"></script>
|
||||
<script src="modules/burgs-and-states.js?v=1.108.11"></script>
|
||||
<!-- ... -->
|
||||
<script src="main.js?v=1.108.11"></script>
|
||||
|
||||
<!-- UI modules loaded with defer -->
|
||||
<script defer src="modules/ui/editors.js?v=1.108.11"></script>
|
||||
<script defer src="modules/ui/heightmap-editor.js?v=1.108.11"></script>
|
||||
```
|
||||
|
||||
### Debug Flags
|
||||
|
||||
```javascript
|
||||
// From main.js:5-11
|
||||
const PRODUCTION = location.hostname &&
|
||||
location.hostname !== "localhost" &&
|
||||
location.hostname !== "127.0.0.1";
|
||||
const DEBUG = JSON.safeParse(localStorage.getItem("debug")) || {};
|
||||
const INFO = true;
|
||||
const TIME = true; // Performance timing
|
||||
const WARN = true;
|
||||
const ERROR = true;
|
||||
|
||||
// Usage:
|
||||
TIME && console.time("placeCapitals");
|
||||
// ... expensive operation ...
|
||||
TIME && console.timeEnd("placeCapitals");
|
||||
```
|
||||
|
||||
## Development Workflow
|
||||
|
||||
### No Build System
|
||||
|
||||
- **Zero build configuration**: No webpack, rollup, or bundlers
|
||||
- **No package.json**: No npm dependencies
|
||||
- **No transpilation**: No Babel or TypeScript
|
||||
- **Direct file editing**: Edit JS → Refresh browser → See changes
|
||||
|
||||
### Local Development
|
||||
|
||||
```bash
|
||||
# Python simple server
|
||||
python -m http.server 8080
|
||||
|
||||
# PHP built-in server
|
||||
php -S localhost:8080
|
||||
|
||||
# Then visit: http://localhost:8080
|
||||
```
|
||||
|
||||
### Versioning Process
|
||||
|
||||
**Manual 3-step versioning** (from pull_request_template.md):
|
||||
|
||||
1. **Update VERSION** in `versioning.js`:
|
||||
```javascript
|
||||
const VERSION = "1.108.12"; // Increment using semver
|
||||
```
|
||||
|
||||
2. **Update file hashes** in `index.html` for all changed files:
|
||||
```html
|
||||
<script src="modules/burgs-and-states.js?v=1.108.12"></script>
|
||||
```
|
||||
|
||||
3. **Update changelog** in `showUpdateWindow()` function (versioning.js) if user-facing
|
||||
|
||||
### Git Workflow
|
||||
|
||||
**Commit Message Format** (inferred from git log):
|
||||
|
||||
```
|
||||
<type>(<scope>): <description>
|
||||
|
||||
Examples:
|
||||
fix(v1.108.11): add external icons to export in base64 format
|
||||
feat(ai-generator): update supported AI models list
|
||||
refactor: drawReliefIcons, v1.108.4
|
||||
perf: set text-rendering to optimizeSpeed, v1.108.1
|
||||
chore: update version to 1.108.8
|
||||
```
|
||||
|
||||
**Types**:
|
||||
- `feat`: New feature
|
||||
- `fix`: Bug fix
|
||||
- `refactor`: Code restructuring
|
||||
- `perf`: Performance improvement
|
||||
- `chore`: Maintenance tasks
|
||||
- `docs`: Documentation changes
|
||||
|
||||
**Scopes**: Optional, often includes version number or component name
|
||||
|
||||
### Pull Request Requirements
|
||||
|
||||
From `.github/pull_request_template.md`:
|
||||
|
||||
**Required**:
|
||||
- [ ] Description of change and motivation
|
||||
- [ ] Type of change (bug fix, feature, refactor, docs, other)
|
||||
- [ ] Version updated in `versioning.js`
|
||||
- [ ] Changed files hash updated in `index.html`
|
||||
|
||||
**Before submitting**:
|
||||
1. Test locally (no automated tests)
|
||||
2. Check console for errors
|
||||
3. Verify map generation still works
|
||||
4. Update version following semver
|
||||
|
||||
## Common Development Tasks
|
||||
|
||||
### Adding a New Feature
|
||||
|
||||
1. **Identify the module type**:
|
||||
- Generator? → `modules/`
|
||||
- UI Editor? → `modules/ui/`
|
||||
- Renderer? → `modules/renderers/`
|
||||
- Utility? → `utils/`
|
||||
|
||||
2. **Create the module file**:
|
||||
```javascript
|
||||
"use strict";
|
||||
|
||||
window.MyNewFeature = (() => {
|
||||
const generate = () => {
|
||||
TIME && console.time("myNewFeature");
|
||||
// Implementation
|
||||
TIME && console.timeEnd("myNewFeature");
|
||||
};
|
||||
|
||||
return { generate };
|
||||
})();
|
||||
```
|
||||
|
||||
3. **Add script tag to index.html**:
|
||||
```html
|
||||
<script src="modules/my-new-feature.js?v=1.108.12"></script>
|
||||
```
|
||||
|
||||
4. **Update versioning**:
|
||||
- Increment VERSION in `versioning.js`
|
||||
- Update hash in `index.html`
|
||||
- Add changelog entry if user-facing
|
||||
|
||||
### Modifying the Data Model
|
||||
|
||||
1. **Update pack structure** in relevant generator
|
||||
2. **Update save/load** in `modules/io/save.js` and `modules/io/load.js`
|
||||
3. **Test with existing .map files** to ensure backward compatibility
|
||||
4. **Update any renderers** that use the new data
|
||||
|
||||
### Adding a New Renderer
|
||||
|
||||
1. **Create renderer file** in `modules/renderers/`:
|
||||
```javascript
|
||||
"use strict";
|
||||
|
||||
function drawMyFeature() {
|
||||
const { cells, myFeatures } = pack;
|
||||
const container = svg.select("#myFeatureLayer");
|
||||
|
||||
// D3 rendering logic
|
||||
container.selectAll("path")
|
||||
.data(myFeatures)
|
||||
.join("path")
|
||||
.attr("d", d => d.path)
|
||||
.attr("fill", d => d.color);
|
||||
}
|
||||
```
|
||||
|
||||
2. **Create SVG layer** in main.js:
|
||||
```javascript
|
||||
let myFeatureLayer = viewbox.append("g")
|
||||
.attr("id", "myFeatureLayer");
|
||||
```
|
||||
|
||||
3. **Add to render pipeline** in appropriate location
|
||||
|
||||
### Debugging Tips
|
||||
|
||||
1. **Use DEBUG flags**:
|
||||
```javascript
|
||||
DEBUG && console.log("Debug info:", data);
|
||||
TIME && console.time("expensiveOperation");
|
||||
```
|
||||
|
||||
2. **Check the debug SVG layer**:
|
||||
```javascript
|
||||
debug.append("circle")
|
||||
.attr("cx", x)
|
||||
.attr("cy", y)
|
||||
.attr("r", 5)
|
||||
.attr("fill", "red");
|
||||
```
|
||||
|
||||
3. **Use browser DevTools**:
|
||||
- Network tab: Check script loading
|
||||
- Console: Look for TIME logs
|
||||
- Sources: Set breakpoints
|
||||
|
||||
4. **Test map generation**:
|
||||
- Generate → Verify no console errors
|
||||
- Save → Load → Verify data integrity
|
||||
- Export → Check output quality
|
||||
|
||||
## Testing Approach
|
||||
|
||||
**No formal testing framework**:
|
||||
- No Jest, Mocha, or automated tests
|
||||
- Manual testing only
|
||||
- User reports via GitHub issues
|
||||
|
||||
**Manual testing checklist**:
|
||||
1. Generate new map with default settings
|
||||
2. Generate with various custom settings
|
||||
3. Load existing .map files
|
||||
4. Test all editors (heightmap, states, cultures, etc.)
|
||||
5. Export in all formats (SVG, PNG, JSON)
|
||||
6. Check console for errors/warnings
|
||||
7. Test on multiple browsers (Chrome, Firefox, Safari)
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
### Optimization Strategies
|
||||
|
||||
1. **Typed Arrays**: Use for large datasets
|
||||
```javascript
|
||||
cells.h = new Uint8Array(n); // Heights 0-255
|
||||
cells.pop = new Float32Array(n); // Population
|
||||
```
|
||||
|
||||
2. **D3 Data Binding**: Efficient DOM updates
|
||||
```javascript
|
||||
container.selectAll("path")
|
||||
.data(features, d => d.id) // Key function
|
||||
.join("path") // Efficient enter/update/exit
|
||||
```
|
||||
|
||||
3. **Debouncing/Throttling**: For frequent events
|
||||
```javascript
|
||||
const onMouseMove = debounce(handleMouseMove, 100);
|
||||
```
|
||||
|
||||
4. **Quadtree for Spatial Queries**:
|
||||
```javascript
|
||||
let burgsTree = d3.quadtree();
|
||||
burgsTree.add([x, y]);
|
||||
const nearest = burgsTree.find(x, y, radius);
|
||||
```
|
||||
|
||||
5. **IndexedDB**: For large map storage
|
||||
```javascript
|
||||
// See libs/indexedDB.js
|
||||
```
|
||||
|
||||
## Important Patterns & Anti-Patterns
|
||||
|
||||
### DO:
|
||||
|
||||
✅ Use typed arrays for cell data
|
||||
✅ Respect SVG layer rendering order
|
||||
✅ Use TIME flags for performance monitoring
|
||||
✅ Follow IIFE pattern for new modules
|
||||
✅ Update version numbers consistently
|
||||
✅ Test backward compatibility with old .map files
|
||||
✅ Use D3 data binding for DOM updates
|
||||
✅ Check for null/undefined before accessing pack data
|
||||
|
||||
### DON'T:
|
||||
|
||||
❌ Add npm dependencies (no build system)
|
||||
❌ Use ES6 modules (not supported in current architecture)
|
||||
❌ Modify global data structures directly without updating renderers
|
||||
❌ Add large libraries (keep bundle size manageable)
|
||||
❌ Break backward compatibility without migration logic
|
||||
❌ Add features without updating save/load functionality
|
||||
❌ Forget to update version hash in index.html
|
||||
❌ Mix rendering layers (respect z-order)
|
||||
|
||||
## Common Patterns in Codebase
|
||||
|
||||
### Random Number Generation
|
||||
|
||||
```javascript
|
||||
// Use seeded random for reproducibility
|
||||
const rand = aleaPRNG(seed);
|
||||
const value = rand(); // 0-1
|
||||
|
||||
// Gaussian distribution
|
||||
const value = gauss(mean, deviation, min, max, rounds);
|
||||
```
|
||||
|
||||
### Cell Iteration
|
||||
|
||||
```javascript
|
||||
const { cells } = pack;
|
||||
const n = cells.i.length;
|
||||
|
||||
for (let i = 0; i < n; i++) {
|
||||
if (cells.h[i] < 20) continue; // Skip water
|
||||
// Process land cells
|
||||
}
|
||||
```
|
||||
|
||||
### D3 SVG Path Creation
|
||||
|
||||
```javascript
|
||||
const path = d3.line()
|
||||
.x(d => d[0])
|
||||
.y(d => d[1])
|
||||
.curve(d3.curveBasis);
|
||||
|
||||
const pathString = path(points);
|
||||
```
|
||||
|
||||
### Graph Traversal
|
||||
|
||||
```javascript
|
||||
// BFS example from graphUtils.js
|
||||
const queue = [startCell];
|
||||
const visited = new Uint8Array(cells.i.length);
|
||||
|
||||
while (queue.length) {
|
||||
const cell = queue.shift();
|
||||
if (visited[cell]) continue;
|
||||
visited[cell] = 1;
|
||||
|
||||
cells.c[cell].forEach(neighbor => {
|
||||
if (!visited[neighbor]) queue.push(neighbor);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## Key Concepts
|
||||
|
||||
### Voronoi/Delaunay Mesh
|
||||
|
||||
FMG uses a Voronoi diagram as the base map structure:
|
||||
|
||||
- **Cells**: Voronoi regions representing map areas
|
||||
- **Vertices**: Points where 3+ cells meet
|
||||
- **Edges**: Borders between cells
|
||||
- **Delaunay Triangulation**: Dual graph for efficient pathfinding
|
||||
|
||||
**Implementation**: Custom `Voronoi` class in `modules/voronoi.js` using Delaunator library
|
||||
|
||||
### Heightmap System
|
||||
|
||||
Heights stored as `Uint8Array` (0-255):
|
||||
- **0-19**: Ocean depths
|
||||
- **20**: Sea level
|
||||
- **21-99**: Land elevations
|
||||
- **100+**: Mountains/peaks (clamped to 255)
|
||||
|
||||
### Culture & State System
|
||||
|
||||
**Cultures**: Linguistic/ethnic groups with naming patterns
|
||||
**States**: Political entities with territories, capitals, military
|
||||
|
||||
Both use expansion algorithms based on cell scoring and distance from capitals.
|
||||
|
||||
### Name Generation
|
||||
|
||||
Sophisticated system in `modules/names-generator.js` (3,371 lines):
|
||||
- Cultural naming patterns
|
||||
- Procedural phoneme generation
|
||||
- Linguistic rules for realistic names
|
||||
- Separate generators for burgs, states, cultures, features
|
||||
|
||||
## File Modification Guidelines
|
||||
|
||||
### When Editing index.html
|
||||
|
||||
- **Line count**: 8,184 lines - VERY large file
|
||||
- **Structure**: UI components inline (not templated)
|
||||
- **Script tags**: Update version hash when modifying JS files
|
||||
- **Dialogs**: jQuery UI dialogs defined inline
|
||||
- **Be careful**: Easy to break HTML structure
|
||||
|
||||
### When Editing main.js
|
||||
|
||||
- **Global scope**: Everything here is globally accessible
|
||||
- **SVG layers**: Order matters (lines 40-94)
|
||||
- **Constants**: Typed array max values defined here
|
||||
- **Initialization**: Core setup happens here
|
||||
|
||||
### When Editing Generators
|
||||
|
||||
- **Self-contained**: Each generator should be independent
|
||||
- **Timing**: Wrap in TIME && console.time/timeEnd
|
||||
- **Error handling**: Use WARN && console.warn for issues
|
||||
- **Data mutations**: Update pack object directly
|
||||
|
||||
### When Editing Renderers
|
||||
|
||||
- **Layer awareness**: Know which SVG layer you're drawing to
|
||||
- **Clear old content**: Remove previous render before redrawing
|
||||
- **D3 data binding**: Use .join() for efficient updates
|
||||
- **Performance**: Large renders should be optimized
|
||||
|
||||
## Resources
|
||||
|
||||
### Documentation
|
||||
|
||||
- **Project Wiki**: https://github.com/Azgaar/Fantasy-Map-Generator/wiki
|
||||
- **Data Model**: https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Data-model
|
||||
- **Trello Board**: https://trello.com/b/7x832DG4/fantasy-map-generator
|
||||
- **Blog**: https://azgaar.wordpress.com
|
||||
|
||||
### Community
|
||||
|
||||
- **Discord**: https://discordapp.com/invite/X7E84HU
|
||||
- **Reddit**: https://www.reddit.com/r/FantasyMapGenerator
|
||||
- **GitHub Issues**: https://github.com/Azgaar/Fantasy-Map-Generator/issues
|
||||
|
||||
### Support
|
||||
|
||||
- **Patreon**: https://www.patreon.com/azgaar
|
||||
- **Email**: azgaar.fmg@yandex.com
|
||||
|
||||
## Troubleshooting Common Issues
|
||||
|
||||
### Map Won't Generate
|
||||
|
||||
1. Check console for errors
|
||||
2. Verify all scripts loaded (Network tab)
|
||||
3. Check localStorage isn't corrupted
|
||||
4. Try clearing browser cache
|
||||
|
||||
### Save/Load Failures
|
||||
|
||||
1. Check IndexedDB quota
|
||||
2. Verify .map file format (should be valid JSON)
|
||||
3. Check version compatibility
|
||||
4. Look for migration errors in console
|
||||
|
||||
### Performance Issues
|
||||
|
||||
1. Reduce number of cells (Options → Map Size)
|
||||
2. Disable unused layers
|
||||
3. Use simpler rendering styles
|
||||
4. Check for memory leaks in console
|
||||
|
||||
### Rendering Glitches
|
||||
|
||||
1. Verify SVG layer order
|
||||
2. Check for NaN values in coordinates
|
||||
3. Ensure paths are valid SVG syntax
|
||||
4. Test in different browsers
|
||||
|
||||
## Best Practices for AI Assistants
|
||||
|
||||
### When Analyzing Code
|
||||
|
||||
1. **Start with the data model**: Understand `pack` structure
|
||||
2. **Check dependencies**: See what functions/modules are used
|
||||
3. **Look for patterns**: IIFE modules, global functions, D3 usage
|
||||
4. **Trace data flow**: How data moves through generation pipeline
|
||||
|
||||
### When Making Changes
|
||||
|
||||
1. **Test locally first**: No CI/CD, manual testing required
|
||||
2. **Update version properly**: Follow 3-step process
|
||||
3. **Maintain backward compatibility**: Old .map files should still load
|
||||
4. **Document complex logic**: Code is under-commented
|
||||
5. **Check performance**: Use TIME flags to measure
|
||||
|
||||
### When Refactoring
|
||||
|
||||
1. **Small incremental changes**: Large refactors are risky
|
||||
2. **Keep the same API**: Don't break existing integrations
|
||||
3. **Test all features**: Generator, editors, save/load, export
|
||||
4. **Consider deprecation**: Don't remove features abruptly
|
||||
|
||||
### When Adding Features
|
||||
|
||||
1. **Check existing code**: Similar features might exist
|
||||
2. **Follow existing patterns**: IIFE for modules, global for utils
|
||||
3. **Update save/load**: Persist new data properly
|
||||
4. **Add UI if needed**: Users should access the feature
|
||||
5. **Update documentation**: At least in comments/changelog
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### File to Edit By Task
|
||||
|
||||
| Task | Files to Edit |
|
||||
|------|--------------|
|
||||
| Add new generator | `modules/my-generator.js`, `index.html` (script tag) |
|
||||
| Modify UI editor | `modules/ui/my-editor.js` |
|
||||
| Change rendering | `modules/renderers/draw-my-feature.js` |
|
||||
| Add utility function | `utils/myUtils.js` |
|
||||
| Update save format | `modules/io/save.js`, `modules/io/load.js` |
|
||||
| Change map options | `modules/ui/options.js`, `index.html` (UI) |
|
||||
| Add SVG layer | `main.js` (lines 40-94), renderer file |
|
||||
| Update version | `versioning.js`, `index.html` (all script tags) |
|
||||
|
||||
### Common Functions
|
||||
|
||||
| Function | Location | Purpose |
|
||||
|----------|----------|---------|
|
||||
| `rn(value, decimals)` | `utils/numberUtils.js` | Round number |
|
||||
| `rand(min, max)` | Random utilities | Random in range |
|
||||
| `gauss(mean, dev, ...)` | `utils/probabilityUtils.js` | Gaussian distribution |
|
||||
| `debounce(fn, ms)` | `utils/commonUtils.js` | Debounce function |
|
||||
| `P(probability)` | Probability utils | Random boolean |
|
||||
| `ra(array)` | Array utils | Random array element |
|
||||
|
||||
### Common D3 Patterns
|
||||
|
||||
```javascript
|
||||
// Select SVG layer
|
||||
const layer = svg.select("#myLayer");
|
||||
|
||||
// Data binding
|
||||
layer.selectAll("path")
|
||||
.data(features, d => d.id)
|
||||
.join("path")
|
||||
.attr("d", d => d.path);
|
||||
|
||||
// Zoom/pan
|
||||
const zoom = d3.zoom()
|
||||
.scaleExtent([1, 20])
|
||||
.on("zoom", zoomed);
|
||||
|
||||
svg.call(zoom);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
Fantasy Map Generator is a **unique codebase** that prioritizes:
|
||||
|
||||
1. **Accessibility**: Runs anywhere without build tools
|
||||
2. **Simplicity**: Direct file editing, no complex tooling
|
||||
3. **Performance**: Typed arrays, efficient algorithms
|
||||
4. **User experience**: Rich UI with extensive customization
|
||||
|
||||
When working on FMG, embrace its philosophy: **pragmatic simplicity over modern complexity**. The lack of build tools is intentional, making it easy for contributors to jump in without setup overhead.
|
||||
|
||||
---
|
||||
|
||||
*Last Updated: 2025-11-14*
|
||||
*Current Version: 1.108.11*
|
||||
*This guide is for AI assistants working on Fantasy Map Generator codebase.*
|
||||
270
OBSIDIAN_INTEGRATION.md
Normal file
270
OBSIDIAN_INTEGRATION.md
Normal file
|
|
@ -0,0 +1,270 @@
|
|||
# Obsidian Vault Integration for Fantasy Map Generator
|
||||
|
||||
## Overview
|
||||
|
||||
Fantasy Map Generator now supports deep integration with your Obsidian vault for managing map lore and notes! This allows you to:
|
||||
|
||||
- Store all your world lore in Markdown format
|
||||
- Edit notes in a modern Markdown editor (no more Win95-style TinyMCE!)
|
||||
- Automatically link map elements to Obsidian notes by coordinates
|
||||
- Keep your notes in sync between FMG and Obsidian
|
||||
- Use [[wikilinks]] to connect related notes
|
||||
- Edit in either FMG or Obsidian - changes sync both ways
|
||||
|
||||
## Setup
|
||||
|
||||
### 1. Install Obsidian Local REST API Plugin
|
||||
|
||||
1. Open Obsidian
|
||||
2. Go to **Settings** → **Community Plugins**
|
||||
3. Click **Browse** and search for "Local REST API"
|
||||
4. Install and **Enable** the plugin
|
||||
5. Go to **Settings** → **Local REST API**
|
||||
6. Copy your **API Key** (you'll need this!)
|
||||
7. Note the **Server Port** (default: 27123)
|
||||
|
||||
### 2. Configure in Fantasy Map Generator
|
||||
|
||||
1. Open Fantasy Map Generator
|
||||
2. Go to **Menu** → **Tools** → **⚙ Obsidian**
|
||||
3. Enter your settings:
|
||||
- **API URL**: `http://127.0.0.1:27123` (default)
|
||||
- **API Key**: Paste from Obsidian plugin settings
|
||||
- **Vault Name**: Name of your Obsidian vault
|
||||
4. Click **Test Connection** to verify
|
||||
5. Click **Save Configuration**
|
||||
|
||||
## Usage
|
||||
|
||||
### Linking Map Elements to Notes
|
||||
|
||||
When you click on a **burg** or **marker** in FMG:
|
||||
|
||||
1. FMG searches your Obsidian vault for notes with matching coordinates in YAML frontmatter
|
||||
2. Shows you the top 5-8 closest matches
|
||||
3. You select the note you want, or create a new one
|
||||
|
||||
### Note Format
|
||||
|
||||
Notes in your vault should have YAML frontmatter like this:
|
||||
|
||||
```markdown
|
||||
---
|
||||
fmg-id: burg123
|
||||
fmg-type: burg
|
||||
coordinates:
|
||||
x: 234.5
|
||||
y: 456.7
|
||||
lat: 45.23
|
||||
lon: -73.45
|
||||
tags:
|
||||
- capital
|
||||
- settlement
|
||||
- ancient
|
||||
aliases:
|
||||
- Eldoria
|
||||
- The Ancient City
|
||||
---
|
||||
|
||||
# Eldoria
|
||||
|
||||
The ancient capital sits upon the [[River Mystral]], founded in year 1203.
|
||||
|
||||
## History
|
||||
|
||||
The city was established by [[King Aldric the First]]...
|
||||
|
||||
## Notable Locations
|
||||
|
||||
- [[The Grand Library]]
|
||||
- [[Temple of the Seven Stars]]
|
||||
- [[Market Square]]
|
||||
```
|
||||
|
||||
### Coordinate Matching
|
||||
|
||||
Since your burgs/markers may have been imported from PostgreSQL without FMG IDs, the system matches by **X/Y coordinates**:
|
||||
|
||||
- FMG extracts the coordinates from the clicked element
|
||||
- Searches all `.md` files in your vault for matching `x:` and `y:` values
|
||||
- Calculates distance and shows closest matches
|
||||
- You pick the right one!
|
||||
|
||||
### Supported Coordinate Formats
|
||||
|
||||
The system recognizes these formats in YAML frontmatter:
|
||||
|
||||
```yaml
|
||||
# Nested object (recommended)
|
||||
coordinates:
|
||||
x: 123.4
|
||||
y: 567.8
|
||||
|
||||
# Or flat
|
||||
x: 123.4
|
||||
y: 567.8
|
||||
|
||||
# Case insensitive
|
||||
X: 123.4
|
||||
Y: 567.8
|
||||
```
|
||||
|
||||
### Creating New Notes
|
||||
|
||||
If no matches are found:
|
||||
|
||||
1. FMG offers to create a new note
|
||||
2. Enter a name (e.g., "Eldoria")
|
||||
3. Optionally specify a folder (e.g., "Locations/Cities")
|
||||
4. FMG generates a template with coordinates
|
||||
5. Opens in the Markdown editor
|
||||
6. Saved directly to your Obsidian vault!
|
||||
|
||||
### Editing Notes
|
||||
|
||||
The modern Markdown editor includes:
|
||||
|
||||
- **Live preview**: Toggle between edit/preview modes
|
||||
- **[[Wikilinks]]**: Link to other notes in your vault
|
||||
- **Syntax highlighting**: Clean monospace font
|
||||
- **Open in Obsidian**: Button to jump to the note in Obsidian app
|
||||
- **Save to Vault**: Changes sync immediately
|
||||
|
||||
### Using Wikilinks
|
||||
|
||||
Create connections between notes:
|
||||
|
||||
```markdown
|
||||
The [[King Aldric the First]] ruled from [[Eldoria]].
|
||||
The city controls access to [[River Mystral]].
|
||||
```
|
||||
|
||||
When you save in FMG, these links work in Obsidian!
|
||||
|
||||
## Migration from PostgreSQL
|
||||
|
||||
If you have existing lore in PostgreSQL with coordinates:
|
||||
|
||||
1. Export your data to Markdown files with YAML frontmatter
|
||||
2. Include `x`, `y`, `lat`, `lon` in the frontmatter
|
||||
3. Place files in your Obsidian vault
|
||||
4. FMG will auto-match by coordinates!
|
||||
|
||||
Example export script template:
|
||||
|
||||
```python
|
||||
for location in locations:
|
||||
frontmatter = f"""---
|
||||
fmg-type: {location.type}
|
||||
coordinates:
|
||||
x: {location.x}
|
||||
y: {location.y}
|
||||
lat: {location.lat}
|
||||
lon: {location.lon}
|
||||
tags: {location.tags}
|
||||
---
|
||||
|
||||
# {location.name}
|
||||
|
||||
{location.description}
|
||||
"""
|
||||
with open(f"vault/{location.name}.md", "w") as f:
|
||||
f.write(frontmatter)
|
||||
```
|
||||
|
||||
## Tips & Tricks
|
||||
|
||||
### Organize Your Vault
|
||||
|
||||
Create folders for different types:
|
||||
|
||||
```
|
||||
My Vault/
|
||||
├── Locations/
|
||||
│ ├── Cities/
|
||||
│ ├── Landmarks/
|
||||
│ └── Regions/
|
||||
├── Characters/
|
||||
├── History/
|
||||
└── Lore/
|
||||
```
|
||||
|
||||
### Use Templates
|
||||
|
||||
Create Obsidian templates for different element types:
|
||||
|
||||
- `Templates/City.md`
|
||||
- `Templates/Landmark.md`
|
||||
- `Templates/Character.md`
|
||||
|
||||
### Search and Graph
|
||||
|
||||
In Obsidian:
|
||||
|
||||
- Use **Search** (`Ctrl+Shift+F`) to find notes by coordinates
|
||||
- Use **Graph View** to see connections between locations
|
||||
- Use **Tags** to organize by type
|
||||
|
||||
### Sync Across Devices
|
||||
|
||||
Use Obsidian Sync or Git to keep your vault synced across computers!
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Connection Failed
|
||||
|
||||
- Make sure Obsidian is running
|
||||
- Verify the Local REST API plugin is enabled
|
||||
- Check the port number (default 27123)
|
||||
- Try restarting Obsidian
|
||||
|
||||
### No Matches Found
|
||||
|
||||
- Check that your notes have `x:` and `y:` fields in frontmatter
|
||||
- Verify coordinates are numbers, not strings
|
||||
- Try increasing the search radius
|
||||
|
||||
### Changes Not Appearing in Obsidian
|
||||
|
||||
- Obsidian should auto-detect file changes
|
||||
- If not, try switching to another note and back
|
||||
- Or close/reopen the note
|
||||
|
||||
## Advanced
|
||||
|
||||
### Custom Coordinate Systems
|
||||
|
||||
If you use a different coordinate system:
|
||||
|
||||
1. Map your coordinates to FMG's system
|
||||
2. Store both in frontmatter:
|
||||
```yaml
|
||||
coordinates:
|
||||
x: 234.5 # FMG coordinates
|
||||
y: 456.7
|
||||
custom_x: 1000 # Your system
|
||||
custom_y: 2000
|
||||
```
|
||||
|
||||
### Database Bridge
|
||||
|
||||
For the future PostgreSQL migration:
|
||||
|
||||
1. Keep coordinates in both Obsidian and database
|
||||
2. Use coordinates as the join key
|
||||
3. Sync changes via API
|
||||
4. Eventually replace file storage with DB
|
||||
|
||||
## Future Features
|
||||
|
||||
Planned enhancements:
|
||||
|
||||
- [ ] Time slider - view notes across historical periods
|
||||
- [ ] Automatic tagging by region/culture
|
||||
- [ ] Bulk import from database
|
||||
- [ ] Real-time collaboration
|
||||
- [ ] Custom Markdown extensions
|
||||
|
||||
---
|
||||
|
||||
**Enjoy your modern, Markdown-powered world-building! 🗺️✨**
|
||||
123
index.html
123
index.html
|
|
@ -1830,6 +1830,7 @@
|
|||
<td>
|
||||
<select id="onloadBehavior" data-stored="onloadBehavior">
|
||||
<option value="random" selected>Generate random map</option>
|
||||
<option value="default">Open default map</option>
|
||||
<option value="lastSaved">Open last saved map</option>
|
||||
</select>
|
||||
</td>
|
||||
|
|
@ -2061,6 +2062,9 @@
|
|||
Namesbase
|
||||
</button>
|
||||
<button id="editNotesButton" data-tip="Click to open Notes Editor" data-shortcut="Shift + O">Notes</button>
|
||||
<button id="obsidianConfigButton" data-tip="Configure Obsidian vault integration for modern Markdown notes" onclick="openObsidianConfig()">
|
||||
⚙ Obsidian
|
||||
</button>
|
||||
<button id="editProvincesButton" data-tip="Click to open Provinces Editor" data-shortcut="Shift + P">
|
||||
Provinces
|
||||
</button>
|
||||
|
|
@ -4962,6 +4966,107 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Obsidian Notes Editor (Modern Markdown) -->
|
||||
<div id="obsidianNotesEditor" class="dialog stable" style="display: none">
|
||||
<div style="margin-bottom: 0.8em; padding-bottom: 0.8em; border-bottom: 1px solid #ddd">
|
||||
<div style="display: flex; align-items: center; gap: 1em; margin-bottom: 0.5em;">
|
||||
<div style="flex: 1;">
|
||||
<span style="font-size: 0.9em; color: #666;">📁 </span>
|
||||
<span id="obsidianNotePath" style="font-size: 0.9em; color: #666;"></span>
|
||||
</div>
|
||||
<button id="openInObsidian" onclick="openInObsidian()" data-tip="Open this note in Obsidian app" style="padding: 4px 12px;">
|
||||
Open in Obsidian
|
||||
</button>
|
||||
</div>
|
||||
<input id="obsidianNoteName" type="text" placeholder="Note name" style="width: 100%; padding: 8px; font-size: 1.1em; font-weight: bold; border: 1px solid #ddd; border-radius: 4px;"/>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; gap: 12px; height: calc(100% - 120px);">
|
||||
<!-- Editor pane -->
|
||||
<div style="flex: 1; display: flex; flex-direction: column;">
|
||||
<div style="margin-bottom: 8px; display: flex; justify-content: space-between; align-items: center;">
|
||||
<strong style="color: #666;">Markdown</strong>
|
||||
<button id="togglePreview" onclick="togglePreviewMode()" style="padding: 4px 12px;">👁 Preview</button>
|
||||
</div>
|
||||
<textarea
|
||||
id="obsidianMarkdownEditor"
|
||||
oninput="updateMarkdownPreview()"
|
||||
style="flex: 1; font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', monospace; font-size: 14px; padding: 12px; border: 1px solid #ddd; border-radius: 4px; resize: none;"
|
||||
placeholder="Write your markdown here..."
|
||||
></textarea>
|
||||
<div
|
||||
id="obsidianMarkdownPreview"
|
||||
style="display: none; flex: 1; padding: 12px; border: 1px solid #ddd; border-radius: 4px; overflow-y: auto; background: #fafafa;"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 12px; padding-top: 12px; border-top: 1px solid #ddd; display: flex; justify-content: space-between;">
|
||||
<div>
|
||||
<span style="font-size: 0.9em; color: #666;">💡 Tip: Use [[wikilinks]] to link to other notes</span>
|
||||
</div>
|
||||
<div style="display: flex; gap: 8px;">
|
||||
<button onclick="$('#obsidianNotesEditor').dialog('close')" style="padding: 6px 16px;">Cancel</button>
|
||||
<button onclick="saveObsidianNote()" style="padding: 6px 16px; background: #5e81ac; color: white; border: none; border-radius: 4px; cursor: pointer;">
|
||||
💾 Save to Vault
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Obsidian Configuration Dialog -->
|
||||
<div id="obsidianConfig" class="dialog stable" style="display: none">
|
||||
<div style="padding: 1em;">
|
||||
<p style="margin-bottom: 1em;">Configure connection to your Obsidian vault via the Local REST API plugin.</p>
|
||||
|
||||
<div style="margin-bottom: 1em;">
|
||||
<label for="obsidianApiUrl" style="display: block; margin-bottom: 0.5em; font-weight: bold;">API URL:</label>
|
||||
<input
|
||||
id="obsidianApiUrl"
|
||||
type="text"
|
||||
value="http://127.0.0.1:27123"
|
||||
style="width: 100%; padding: 8px; font-family: monospace;"
|
||||
placeholder="http://127.0.0.1:27123"
|
||||
/>
|
||||
<small style="color: #666;">Default: http://127.0.0.1:27123</small>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 1em;">
|
||||
<label for="obsidianApiKey" style="display: block; margin-bottom: 0.5em; font-weight: bold;">API Key:</label>
|
||||
<input
|
||||
id="obsidianApiKey"
|
||||
type="password"
|
||||
style="width: 100%; padding: 8px; font-family: monospace;"
|
||||
placeholder="Enter your API key from Obsidian plugin settings"
|
||||
/>
|
||||
<small style="color: #666;">Get this from Obsidian → Settings → Local REST API</small>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 1em;">
|
||||
<label for="obsidianVaultName" style="display: block; margin-bottom: 0.5em; font-weight: bold;">Vault Name:</label>
|
||||
<input
|
||||
id="obsidianVaultName"
|
||||
type="text"
|
||||
style="width: 100%; padding: 8px;"
|
||||
placeholder="My Vault"
|
||||
/>
|
||||
<small style="color: #666;">Name of your Obsidian vault (for opening links)</small>
|
||||
</div>
|
||||
|
||||
<div style="margin: 1.5em 0; padding: 1em; background: #f0f0f0; border-radius: 4px;">
|
||||
<strong>Status:</strong> <span id="obsidianStatus" style="color: #666;">Not configured</span>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; gap: 8px; justify-content: flex-end;">
|
||||
<button onclick="testObsidianConnection()" style="padding: 8px 16px;">Test Connection</button>
|
||||
<button onclick="$('#obsidianConfig').dialog('close')" style="padding: 8px 16px;">Cancel</button>
|
||||
<button onclick="saveObsidianConfig()" style="padding: 8px 16px; background: #5e81ac; color: white; border: none; border-radius: 4px;">
|
||||
Save Configuration
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="aiGenerator" class="dialog stable" style="display: none">
|
||||
<div style="display: flex; flex-direction: column; gap: 0.3em; width: 100%">
|
||||
<textarea id="aiGeneratorResult" placeholder="Generated text will appear here" cols="30" rows="10"></textarea>
|
||||
|
|
@ -6087,6 +6192,15 @@
|
|||
browser
|
||||
</button>
|
||||
</div>
|
||||
<div style="margin-top: 0.8em; padding-top: 0.8em; border-top: 1px solid #ccc">
|
||||
<strong>Default map</strong>
|
||||
<button onclick="saveAsDefaultMap()" data-tip="Set this map as the default that opens on load (requires 'Open default map' onload behavior)">
|
||||
Set as default
|
||||
</button>
|
||||
<button onclick="clearDefaultMap()" data-tip="Clear the current default map setting">
|
||||
Clear default
|
||||
</button>
|
||||
</div>
|
||||
<p>
|
||||
Maps are saved in <i>.map</i> format, that can be loaded back via the <i>Load</i> in menu. There is no way to
|
||||
restore the progress if file is lost. Please keep old save files on your machine or cloud storage as backups.
|
||||
|
|
@ -8118,7 +8232,7 @@
|
|||
<script src="modules/ui/style-presets.js?v=1.100.00"></script>
|
||||
<script src="modules/ui/general.js?v=1.100.00"></script>
|
||||
<script src="modules/ui/options.js?v=1.106.2"></script>
|
||||
<script src="main.js?v=1.108.1"></script>
|
||||
<script src="main.js?v=1.108.12">
|
||||
|
||||
<script defer src="modules/ui/style.js?v=1.108.4"></script>
|
||||
<script defer src="modules/ui/editors.js?v=1.108.5"></script>
|
||||
|
|
@ -8141,7 +8255,7 @@
|
|||
<script defer src="modules/ui/rivers-creator.js?v=1.106.0"></script>
|
||||
<script defer src="modules/ui/relief-editor.js?v=1.99.00"></script>
|
||||
<script defer src="modules/ui/burg-editor.js?v=1.106.6"></script>
|
||||
<script defer src="modules/ui/units-editor.js?v=1.104.0"></script>
|
||||
<script defer src="modules/ui/units-editor.js?v=1.108.12"></script>
|
||||
<script defer src="modules/ui/notes-editor.js?v=1.107.3"></script>
|
||||
<script defer src="modules/ui/ai-generator.js?v=1.108.8"></script>
|
||||
<script defer src="modules/ui/diplomacy-editor.js?v=1.99.00"></script>
|
||||
|
|
@ -8163,10 +8277,13 @@
|
|||
<script defer src="modules/coa-renderer.js?v=1.99.00"></script>
|
||||
<script defer src="libs/rgbquant.min.js"></script>
|
||||
<script defer src="libs/jquery.ui.touch-punch.min.js"></script>
|
||||
<script defer src="modules/io/save.js?v=1.107.4"></script>
|
||||
<script defer src="modules/io/save.js?v=1.108.12"></script>
|
||||
<script defer src="modules/io/load.js?v=1.108.0"></script>
|
||||
<script defer src="modules/io/cloud.js?v=1.106.0"></script>
|
||||
<script defer src="modules/io/export.js?v=1.108.11"></script>
|
||||
<script defer src="modules/io/obsidian-bridge.js?v=1.108.13"></script>
|
||||
<script defer src="modules/ui/obsidian-notes-editor.js?v=1.108.13"></script>
|
||||
<script defer src="modules/ui/obsidian-config.js?v=1.108.13"></script>
|
||||
|
||||
<script defer src="modules/renderers/draw-features.js?v=1.108.2"></script>
|
||||
<script defer src="modules/renderers/draw-borders.js?v=1.104.0"></script>
|
||||
|
|
|
|||
16
main.js
16
main.js
|
|
@ -291,6 +291,22 @@ async function checkLoadParameters() {
|
|||
return;
|
||||
}
|
||||
|
||||
// check if there is a default map saved to indexedDB
|
||||
if (byId("onloadBehavior").value === "default") {
|
||||
try {
|
||||
const blob = await ldb.get("defaultMap");
|
||||
if (blob) {
|
||||
WARN && console.warn("Loading default map");
|
||||
uploadMap(blob);
|
||||
return;
|
||||
} else {
|
||||
WARN && console.warn("No default map set, generating random map");
|
||||
}
|
||||
} catch (error) {
|
||||
ERROR && console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
// check if there is a map saved to indexedDB
|
||||
if (byId("onloadBehavior").value === "lastSaved") {
|
||||
try {
|
||||
|
|
|
|||
416
modules/io/obsidian-bridge.js
Normal file
416
modules/io/obsidian-bridge.js
Normal file
|
|
@ -0,0 +1,416 @@
|
|||
"use strict";
|
||||
|
||||
// Obsidian Vault Integration for Fantasy Map Generator
|
||||
// Uses Obsidian Local REST API plugin
|
||||
// https://github.com/coddingtonbear/obsidian-local-rest-api
|
||||
|
||||
const ObsidianBridge = (() => {
|
||||
// Configuration
|
||||
const config = {
|
||||
apiUrl: "http://127.0.0.1:27123",
|
||||
apiKey: "", // Set via UI
|
||||
enabled: false,
|
||||
vaultName: ""
|
||||
};
|
||||
|
||||
// Initialize from localStorage
|
||||
function init() {
|
||||
const stored = localStorage.getItem("obsidianConfig");
|
||||
if (stored) {
|
||||
try {
|
||||
const parsed = JSON.parse(stored);
|
||||
Object.assign(config, parsed);
|
||||
} catch (error) {
|
||||
ERROR && console.error("Failed to load Obsidian config:", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Save configuration
|
||||
function saveConfig() {
|
||||
localStorage.setItem("obsidianConfig", JSON.stringify(config));
|
||||
}
|
||||
|
||||
// Test connection to Obsidian
|
||||
async function testConnection() {
|
||||
if (!config.apiKey) {
|
||||
throw new Error("API key not set. Please configure in Options.");
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${config.apiUrl}/`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${config.apiKey}`
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Connection failed: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
INFO && console.log("Obsidian connection successful:", data);
|
||||
config.enabled = true;
|
||||
saveConfig();
|
||||
return true;
|
||||
} catch (error) {
|
||||
ERROR && console.error("Obsidian connection failed:", error);
|
||||
config.enabled = false;
|
||||
saveConfig();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Get all markdown files from vault
|
||||
async function getVaultFiles() {
|
||||
if (!config.enabled) {
|
||||
throw new Error("Obsidian not connected");
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${config.apiUrl}/vault/`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${config.apiKey}`
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch vault files: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data.files || [];
|
||||
} catch (error) {
|
||||
ERROR && console.error("Failed to get vault files:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Get note content by path
|
||||
async function getNote(notePath) {
|
||||
if (!config.enabled) {
|
||||
throw new Error("Obsidian not connected");
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${config.apiUrl}/vault/${encodeURIComponent(notePath)}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${config.apiKey}`,
|
||||
Accept: "text/markdown"
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch note: ${response.status}`);
|
||||
}
|
||||
|
||||
return await response.text();
|
||||
} catch (error) {
|
||||
ERROR && console.error("Failed to get note:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Update note content
|
||||
async function updateNote(notePath, content) {
|
||||
if (!config.enabled) {
|
||||
throw new Error("Obsidian not connected");
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${config.apiUrl}/vault/${encodeURIComponent(notePath)}`, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
Authorization: `Bearer ${config.apiKey}`,
|
||||
"Content-Type": "text/markdown"
|
||||
},
|
||||
body: content
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to update note: ${response.status}`);
|
||||
}
|
||||
|
||||
INFO && console.log("Note updated successfully:", notePath);
|
||||
return true;
|
||||
} catch (error) {
|
||||
ERROR && console.error("Failed to update note:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Create new note
|
||||
async function createNote(notePath, content) {
|
||||
if (!config.enabled) {
|
||||
throw new Error("Obsidian not connected");
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${config.apiUrl}/vault/${encodeURIComponent(notePath)}`, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
Authorization: `Bearer ${config.apiKey}`,
|
||||
"Content-Type": "text/markdown"
|
||||
},
|
||||
body: content
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to create note: ${response.status}`);
|
||||
}
|
||||
|
||||
INFO && console.log("Note created successfully:", notePath);
|
||||
return true;
|
||||
} catch (error) {
|
||||
ERROR && console.error("Failed to create note:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Parse YAML frontmatter from markdown content
|
||||
function parseFrontmatter(content) {
|
||||
const frontmatterRegex = /^---\s*\n([\s\S]*?)\n---/;
|
||||
const match = content.match(frontmatterRegex);
|
||||
|
||||
if (!match) {
|
||||
return {frontmatter: {}, content};
|
||||
}
|
||||
|
||||
const frontmatterText = match[1];
|
||||
const bodyContent = content.slice(match[0].length).trim();
|
||||
|
||||
// Simple YAML parser (handles basic key-value pairs and nested objects)
|
||||
const frontmatter = {};
|
||||
const lines = frontmatterText.split("\n");
|
||||
let currentKey = null;
|
||||
let currentObj = frontmatter;
|
||||
let indentLevel = 0;
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed || trimmed.startsWith("#")) continue;
|
||||
|
||||
const indent = line.search(/\S/);
|
||||
const colonIndex = trimmed.indexOf(":");
|
||||
|
||||
if (colonIndex === -1) continue;
|
||||
|
||||
const key = trimmed.slice(0, colonIndex).trim();
|
||||
const value = trimmed.slice(colonIndex + 1).trim();
|
||||
|
||||
if (indent === 0) {
|
||||
currentObj = frontmatter;
|
||||
currentKey = key;
|
||||
|
||||
if (value) {
|
||||
// Simple value
|
||||
frontmatter[key] = parseValue(value);
|
||||
} else {
|
||||
// Nested object or array
|
||||
frontmatter[key] = {};
|
||||
currentObj = frontmatter[key];
|
||||
}
|
||||
} else if (currentObj && key) {
|
||||
// Nested property
|
||||
currentObj[key] = parseValue(value);
|
||||
}
|
||||
}
|
||||
|
||||
return {frontmatter, content: bodyContent};
|
||||
}
|
||||
|
||||
// Parse YAML value (handle strings, numbers, arrays)
|
||||
function parseValue(value) {
|
||||
if (!value) return null;
|
||||
|
||||
// Array
|
||||
if (value.startsWith("[") && value.endsWith("]")) {
|
||||
return value
|
||||
.slice(1, -1)
|
||||
.split(",")
|
||||
.map(v => v.trim())
|
||||
.filter(v => v);
|
||||
}
|
||||
|
||||
// Number
|
||||
if (!isNaN(value) && value !== "") {
|
||||
return parseFloat(value);
|
||||
}
|
||||
|
||||
// Boolean
|
||||
if (value === "true") return true;
|
||||
if (value === "false") return false;
|
||||
|
||||
// String (remove quotes if present)
|
||||
return value.replace(/^["']|["']$/g, "");
|
||||
}
|
||||
|
||||
// Calculate distance between two coordinates
|
||||
function calculateDistance(x1, y1, x2, y2) {
|
||||
const dx = x2 - x1;
|
||||
const dy = y2 - y1;
|
||||
return Math.sqrt(dx * dx + dy * dy);
|
||||
}
|
||||
|
||||
// Find notes by coordinates
|
||||
async function findNotesByCoordinates(x, y, limit = 8) {
|
||||
TIME && console.time("findNotesByCoordinates");
|
||||
|
||||
try {
|
||||
const files = await getVaultFiles();
|
||||
const mdFiles = files.filter(f => f.endsWith(".md"));
|
||||
|
||||
const matches = [];
|
||||
|
||||
for (const filePath of mdFiles) {
|
||||
try {
|
||||
const content = await getNote(filePath);
|
||||
const {frontmatter} = parseFrontmatter(content);
|
||||
|
||||
// Check for coordinates in frontmatter
|
||||
let noteX, noteY;
|
||||
|
||||
// Support various coordinate formats
|
||||
if (frontmatter.coordinates) {
|
||||
noteX = frontmatter.coordinates.x || frontmatter.coordinates.X;
|
||||
noteY = frontmatter.coordinates.y || frontmatter.coordinates.Y;
|
||||
} else {
|
||||
noteX = frontmatter.x || frontmatter.X;
|
||||
noteY = frontmatter.y || frontmatter.Y;
|
||||
}
|
||||
|
||||
if (noteX !== undefined && noteY !== undefined) {
|
||||
const distance = calculateDistance(x, y, noteX, noteY);
|
||||
|
||||
matches.push({
|
||||
path: filePath,
|
||||
name: filePath.replace(/\.md$/, "").split("/").pop(),
|
||||
frontmatter,
|
||||
distance,
|
||||
coordinates: {x: noteX, y: noteY}
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
// Skip files that can't be read
|
||||
DEBUG && console.debug("Skipping file:", filePath, error);
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by distance and return top matches
|
||||
matches.sort((a, b) => a.distance - b.distance);
|
||||
const results = matches.slice(0, limit);
|
||||
|
||||
TIME && console.timeEnd("findNotesByCoordinates");
|
||||
INFO && console.log(`Found ${results.length} nearby notes for (${x}, ${y})`);
|
||||
|
||||
return results;
|
||||
} catch (error) {
|
||||
ERROR && console.error("Failed to find notes by coordinates:", error);
|
||||
TIME && console.timeEnd("findNotesByCoordinates");
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Find note by FMG ID in frontmatter
|
||||
async function findNoteByFmgId(fmgId) {
|
||||
try {
|
||||
const files = await getVaultFiles();
|
||||
const mdFiles = files.filter(f => f.endsWith(".md"));
|
||||
|
||||
for (const filePath of mdFiles) {
|
||||
try {
|
||||
const content = await getNote(filePath);
|
||||
const {frontmatter} = parseFrontmatter(content);
|
||||
|
||||
if (frontmatter["fmg-id"] === fmgId || frontmatter.fmgId === fmgId) {
|
||||
return {
|
||||
path: filePath,
|
||||
name: filePath.replace(/\.md$/, "").split("/").pop(),
|
||||
content,
|
||||
frontmatter
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
DEBUG && console.debug("Skipping file:", filePath, error);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (error) {
|
||||
ERROR && console.error("Failed to find note by FMG ID:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Generate note template for FMG element
|
||||
function generateNoteTemplate(element, type) {
|
||||
const {x, y} = element;
|
||||
const lat = pack.cells.lat?.[element.cell] || 0;
|
||||
const lon = pack.cells.lon?.[element.cell] || 0;
|
||||
|
||||
const frontmatter = {
|
||||
"fmg-id": element.id || `${type}${element.i}`,
|
||||
"fmg-type": type,
|
||||
coordinates: {x, y, lat, lon},
|
||||
tags: [type],
|
||||
created: new Date().toISOString()
|
||||
};
|
||||
|
||||
if (element.name) {
|
||||
frontmatter.aliases = [element.name];
|
||||
}
|
||||
|
||||
const yaml = Object.entries(frontmatter)
|
||||
.map(([key, value]) => {
|
||||
if (typeof value === "object" && !Array.isArray(value)) {
|
||||
const nested = Object.entries(value)
|
||||
.map(([k, v]) => ` ${k}: ${v}`)
|
||||
.join("\n");
|
||||
return `${key}:\n${nested}`;
|
||||
} else if (Array.isArray(value)) {
|
||||
return `${key}:\n - ${value.join("\n - ")}`;
|
||||
}
|
||||
return `${key}: ${value}`;
|
||||
})
|
||||
.join("\n");
|
||||
|
||||
const title = element.name || `${type} ${element.i}`;
|
||||
|
||||
return `---
|
||||
${yaml}
|
||||
---
|
||||
|
||||
# ${title}
|
||||
|
||||
*This note was created from Fantasy Map Generator*
|
||||
|
||||
## Description
|
||||
|
||||
Add your lore here...
|
||||
|
||||
## History
|
||||
|
||||
## Notable Features
|
||||
|
||||
## Related
|
||||
`;
|
||||
}
|
||||
|
||||
return {
|
||||
init,
|
||||
config,
|
||||
saveConfig,
|
||||
testConnection,
|
||||
getVaultFiles,
|
||||
getNote,
|
||||
updateNote,
|
||||
createNote,
|
||||
parseFrontmatter,
|
||||
findNotesByCoordinates,
|
||||
findNoteByFmgId,
|
||||
generateNoteTemplate
|
||||
};
|
||||
})();
|
||||
|
||||
// Initialize on load
|
||||
ObsidianBridge.init();
|
||||
|
|
@ -167,6 +167,32 @@ async function saveToStorage(mapData, showTip = false) {
|
|||
showTip && tip("Map is saved to the browser storage", false, "success");
|
||||
}
|
||||
|
||||
// save current map as the default map
|
||||
async function saveAsDefaultMap() {
|
||||
if (customization) return tip("Map cannot be saved in EDIT mode, please complete the edit and retry", false, "error");
|
||||
|
||||
try {
|
||||
const mapData = prepareMapData();
|
||||
const blob = new Blob([mapData], {type: "text/plain"});
|
||||
await ldb.set("defaultMap", blob);
|
||||
tip("Map is set as default and will open on load", true, "success", 5000);
|
||||
} catch (error) {
|
||||
ERROR && console.error(error);
|
||||
tip("Failed to set default map", true, "error", 3000);
|
||||
}
|
||||
}
|
||||
|
||||
// clear the default map setting
|
||||
async function clearDefaultMap() {
|
||||
try {
|
||||
await ldb.set("defaultMap", null);
|
||||
tip("Default map cleared", false, "success", 2000);
|
||||
} catch (error) {
|
||||
ERROR && console.error(error);
|
||||
tip("Failed to clear default map", false, "error", 2000);
|
||||
}
|
||||
}
|
||||
|
||||
// download map file
|
||||
function saveToMachine(mapData, filename) {
|
||||
const blob = new Blob([mapData], {type: "text/plain"});
|
||||
|
|
|
|||
73
modules/ui/obsidian-config.js
Normal file
73
modules/ui/obsidian-config.js
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
"use strict";
|
||||
|
||||
// Obsidian Configuration UI
|
||||
|
||||
function openObsidianConfig() {
|
||||
// Load current config
|
||||
const {apiUrl, apiKey, vaultName, enabled} = ObsidianBridge.config;
|
||||
|
||||
byId("obsidianApiUrl").value = apiUrl || "http://127.0.0.1:27123";
|
||||
byId("obsidianApiKey").value = apiKey || "";
|
||||
byId("obsidianVaultName").value = vaultName || "";
|
||||
|
||||
updateObsidianStatus(enabled);
|
||||
|
||||
$("#obsidianConfig").dialog({
|
||||
title: "Obsidian Vault Configuration",
|
||||
width: "600px",
|
||||
position: {my: "center", at: "center", of: "svg"}
|
||||
});
|
||||
}
|
||||
|
||||
function updateObsidianStatus(enabled) {
|
||||
const statusEl = byId("obsidianStatus");
|
||||
if (enabled) {
|
||||
statusEl.textContent = "✅ Connected";
|
||||
statusEl.style.color = "#2ecc71";
|
||||
} else {
|
||||
statusEl.textContent = "❌ Not connected";
|
||||
statusEl.style.color = "#e74c3c";
|
||||
}
|
||||
}
|
||||
|
||||
async function testObsidianConnection() {
|
||||
const apiUrl = byId("obsidianApiUrl").value.trim();
|
||||
const apiKey = byId("obsidianApiKey").value.trim();
|
||||
|
||||
if (!apiUrl || !apiKey) {
|
||||
tip("Please enter both API URL and API Key", false, "error", 3000);
|
||||
return;
|
||||
}
|
||||
|
||||
// Temporarily set config for testing
|
||||
Object.assign(ObsidianBridge.config, {apiUrl, apiKey});
|
||||
|
||||
byId("obsidianStatus").textContent = "Testing connection...";
|
||||
byId("obsidianStatus").style.color = "#f39c12";
|
||||
|
||||
try {
|
||||
await ObsidianBridge.testConnection();
|
||||
updateObsidianStatus(true);
|
||||
tip("Successfully connected to Obsidian!", true, "success", 3000);
|
||||
} catch (error) {
|
||||
updateObsidianStatus(false);
|
||||
tip("Connection failed: " + error.message, true, "error", 5000);
|
||||
}
|
||||
}
|
||||
|
||||
function saveObsidianConfig() {
|
||||
const apiUrl = byId("obsidianApiUrl").value.trim();
|
||||
const apiKey = byId("obsidianApiKey").value.trim();
|
||||
const vaultName = byId("obsidianVaultName").value.trim();
|
||||
|
||||
if (!apiUrl || !apiKey) {
|
||||
tip("Please enter both API URL and API Key", false, "error", 3000);
|
||||
return;
|
||||
}
|
||||
|
||||
Object.assign(ObsidianBridge.config, {apiUrl, apiKey, vaultName});
|
||||
ObsidianBridge.saveConfig();
|
||||
|
||||
$("#obsidianConfig").dialog("close");
|
||||
tip("Obsidian configuration saved", true, "success", 2000);
|
||||
}
|
||||
358
modules/ui/obsidian-notes-editor.js
Normal file
358
modules/ui/obsidian-notes-editor.js
Normal file
|
|
@ -0,0 +1,358 @@
|
|||
"use strict";
|
||||
|
||||
// Modern Markdown Notes Editor with Obsidian Integration
|
||||
|
||||
function editObsidianNote(elementId, elementType, coordinates) {
|
||||
const {x, y} = coordinates;
|
||||
|
||||
// Show loading dialog
|
||||
showLoadingDialog();
|
||||
|
||||
// Try to find note by FMG ID first, then by coordinates
|
||||
findOrCreateNote(elementId, elementType, coordinates)
|
||||
.then(noteData => {
|
||||
showMarkdownEditor(noteData, elementType);
|
||||
})
|
||||
.catch(error => {
|
||||
ERROR && console.error("Failed to load note:", error);
|
||||
tip("Failed to load Obsidian note: " + error.message, true, "error", 5000);
|
||||
closeDialogs("#obsidianNoteLoading");
|
||||
});
|
||||
}
|
||||
|
||||
async function findOrCreateNote(elementId, elementType, coordinates) {
|
||||
const {x, y} = coordinates;
|
||||
|
||||
// First try to find by FMG ID
|
||||
let note = await ObsidianBridge.findNoteByFmgId(elementId);
|
||||
|
||||
if (note) {
|
||||
INFO && console.log("Found note by FMG ID:", note.path);
|
||||
return note;
|
||||
}
|
||||
|
||||
// Find by coordinates
|
||||
const matches = await ObsidianBridge.findNotesByCoordinates(x, y, 8);
|
||||
|
||||
closeDialogs("#obsidianNoteLoading");
|
||||
|
||||
if (matches.length === 0) {
|
||||
// No matches - offer to create new note
|
||||
return await promptCreateNewNote(elementId, elementType, coordinates);
|
||||
}
|
||||
|
||||
if (matches.length === 1) {
|
||||
// Single match - load it
|
||||
const match = matches[0];
|
||||
const content = await ObsidianBridge.getNote(match.path);
|
||||
return {
|
||||
path: match.path,
|
||||
name: match.name,
|
||||
content,
|
||||
frontmatter: match.frontmatter
|
||||
};
|
||||
}
|
||||
|
||||
// Multiple matches - show selection dialog
|
||||
return await showNoteSelectionDialog(matches, elementId, elementType, coordinates);
|
||||
}
|
||||
|
||||
function showLoadingDialog() {
|
||||
alertMessage.innerHTML = `
|
||||
<div style="text-align: center; padding: 2em;">
|
||||
<div class="spinner" style="margin: 0 auto 1em;"></div>
|
||||
<p>Searching Obsidian vault for matching notes...</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
$("#alert").dialog({
|
||||
title: "Loading from Obsidian",
|
||||
width: "400px",
|
||||
closeOnEscape: false,
|
||||
buttons: {},
|
||||
dialogClass: "no-close",
|
||||
position: {my: "center", at: "center", of: "svg"}
|
||||
});
|
||||
}
|
||||
|
||||
async function showNoteSelectionDialog(matches, elementId, elementType, coordinates) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const matchList = matches
|
||||
.map(
|
||||
(match, index) => `
|
||||
<div class="note-match" data-index="${index}" style="
|
||||
padding: 12px;
|
||||
margin: 8px 0;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
" onmouseover="this.style.background='#f0f0f0'" onmouseout="this.style.background='white'">
|
||||
<div style="font-weight: bold; margin-bottom: 4px;">${match.name}</div>
|
||||
<div style="font-size: 0.9em; color: #666;">
|
||||
Distance: ${match.distance.toFixed(1)} units<br/>
|
||||
Coordinates: (${match.coordinates.x}, ${match.coordinates.y})<br/>
|
||||
Path: ${match.path}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
)
|
||||
.join("");
|
||||
|
||||
alertMessage.innerHTML = `
|
||||
<div style="max-height: 60vh; overflow-y: auto;">
|
||||
<p style="margin-bottom: 1em;">Found ${matches.length} notes near this location. Select one:</p>
|
||||
${matchList}
|
||||
</div>
|
||||
`;
|
||||
|
||||
$("#alert").dialog({
|
||||
title: "Select Obsidian Note",
|
||||
width: "600px",
|
||||
buttons: {
|
||||
"Create New": async function () {
|
||||
$(this).dialog("close");
|
||||
try {
|
||||
const newNote = await promptCreateNewNote(elementId, elementType, coordinates);
|
||||
resolve(newNote);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
},
|
||||
Cancel: function () {
|
||||
$(this).dialog("close");
|
||||
reject(new Error("Cancelled"));
|
||||
}
|
||||
},
|
||||
position: {my: "center", at: "center", of: "svg"}
|
||||
});
|
||||
|
||||
// Add click handlers to matches
|
||||
document.querySelectorAll(".note-match").forEach((el, index) => {
|
||||
el.addEventListener("click", async () => {
|
||||
$("#alert").dialog("close");
|
||||
try {
|
||||
const match = matches[index];
|
||||
const content = await ObsidianBridge.getNote(match.path);
|
||||
resolve({
|
||||
path: match.path,
|
||||
name: match.name,
|
||||
content,
|
||||
frontmatter: match.frontmatter
|
||||
});
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function promptCreateNewNote(elementId, elementType, coordinates) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const element = getElementData(elementId, elementType);
|
||||
const suggestedName = element.name || `${elementType}-${element.i}`;
|
||||
|
||||
alertMessage.innerHTML = `
|
||||
<p>No matching notes found. Create a new note in your Obsidian vault?</p>
|
||||
<div style="margin: 1em 0;">
|
||||
<label for="newNoteName" style="display: block; margin-bottom: 0.5em;">Note name:</label>
|
||||
<input id="newNoteName" type="text" value="${suggestedName}" style="width: 100%; padding: 8px; font-size: 1em;"/>
|
||||
</div>
|
||||
<div style="margin: 1em 0;">
|
||||
<label for="newNotePath" style="display: block; margin-bottom: 0.5em;">Folder (optional):</label>
|
||||
<input id="newNotePath" type="text" placeholder="e.g., Locations/Cities" style="width: 100%; padding: 8px; font-size: 1em;"/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
$("#alert").dialog({
|
||||
title: "Create New Note",
|
||||
width: "500px",
|
||||
buttons: {
|
||||
Create: async function () {
|
||||
const name = byId("newNoteName").value.trim();
|
||||
const folder = byId("newNotePath").value.trim();
|
||||
|
||||
if (!name) {
|
||||
tip("Please enter a note name", false, "error");
|
||||
return;
|
||||
}
|
||||
|
||||
const notePath = folder ? `${folder}/${name}.md` : `${name}.md`;
|
||||
|
||||
$(this).dialog("close");
|
||||
|
||||
try {
|
||||
const template = ObsidianBridge.generateNoteTemplate(element, elementType);
|
||||
await ObsidianBridge.createNote(notePath, template);
|
||||
|
||||
const {frontmatter} = ObsidianBridge.parseFrontmatter(template);
|
||||
|
||||
resolve({
|
||||
path: notePath,
|
||||
name,
|
||||
content: template,
|
||||
frontmatter,
|
||||
isNew: true
|
||||
});
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
},
|
||||
Cancel: function () {
|
||||
$(this).dialog("close");
|
||||
reject(new Error("Cancelled"));
|
||||
}
|
||||
},
|
||||
position: {my: "center", at: "center", of: "svg"}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getElementData(elementId, elementType) {
|
||||
// Extract element data based on type
|
||||
if (elementType === "burg") {
|
||||
const burgId = parseInt(elementId.replace("burg", ""));
|
||||
return pack.burgs[burgId];
|
||||
} else if (elementType === "marker") {
|
||||
const markerId = parseInt(elementId.replace("marker", ""));
|
||||
return pack.markers[markerId];
|
||||
} else {
|
||||
// Generic element
|
||||
const el = document.getElementById(elementId);
|
||||
return {
|
||||
id: elementId,
|
||||
name: elementId,
|
||||
x: parseFloat(el?.getAttribute("cx") || 0),
|
||||
y: parseFloat(el?.getAttribute("cy") || 0)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function showMarkdownEditor(noteData, elementType) {
|
||||
const {path, name, content, frontmatter, isNew} = noteData;
|
||||
|
||||
// Extract frontmatter and body
|
||||
const {content: bodyContent} = ObsidianBridge.parseFrontmatter(content);
|
||||
|
||||
// Set up the dialog
|
||||
byId("obsidianNotePath").textContent = path;
|
||||
byId("obsidianNoteName").value = name;
|
||||
byId("obsidianMarkdownEditor").value = content;
|
||||
byId("obsidianMarkdownPreview").innerHTML = renderMarkdown(bodyContent);
|
||||
|
||||
// Store current note data
|
||||
showMarkdownEditor.currentNote = noteData;
|
||||
showMarkdownEditor.originalContent = content;
|
||||
|
||||
$("#obsidianNotesEditor").dialog({
|
||||
title: `Obsidian Note: ${name}`,
|
||||
width: Math.min(svgWidth * 0.9, 1200),
|
||||
height: svgHeight * 0.85,
|
||||
position: {my: "center", at: "center", of: "svg"},
|
||||
close: () => {
|
||||
showMarkdownEditor.currentNote = null;
|
||||
showMarkdownEditor.originalContent = null;
|
||||
}
|
||||
});
|
||||
|
||||
// Update preview on edit
|
||||
updateMarkdownPreview();
|
||||
|
||||
if (isNew) {
|
||||
tip("New note created in Obsidian vault", true, "success", 3000);
|
||||
}
|
||||
}
|
||||
|
||||
function updateMarkdownPreview() {
|
||||
const content = byId("obsidianMarkdownEditor").value;
|
||||
const {content: bodyContent} = ObsidianBridge.parseFrontmatter(content);
|
||||
byId("obsidianMarkdownPreview").innerHTML = renderMarkdown(bodyContent);
|
||||
}
|
||||
|
||||
function renderMarkdown(markdown) {
|
||||
// Simple Markdown renderer (will be replaced with marked.js)
|
||||
let html = markdown;
|
||||
|
||||
// Headers
|
||||
html = html.replace(/^### (.*$)/gim, "<h3>$1</h3>");
|
||||
html = html.replace(/^## (.*$)/gim, "<h2>$1</h2>");
|
||||
html = html.replace(/^# (.*$)/gim, "<h1>$1</h1>");
|
||||
|
||||
// Bold
|
||||
html = html.replace(/\*\*(.*?)\*\*/g, "<strong>$1</strong>");
|
||||
html = html.replace(/\_\_(.*?)\_\_/g, "<strong>$1</strong>");
|
||||
|
||||
// Italic
|
||||
html = html.replace(/\*(.*?)\*/g, "<em>$1</em>");
|
||||
html = html.replace(/\_(.*?)\_/g, "<em>$1</em>");
|
||||
|
||||
// Links
|
||||
html = html.replace(/\[([^\]]+)\]\(([^\)]+)\)/g, '<a href="$2" target="_blank">$1</a>');
|
||||
|
||||
// Wikilinks [[Page]]
|
||||
html = html.replace(/\[\[([^\]]+)\]\]/g, '<span class="wikilink">$1</span>');
|
||||
|
||||
// Lists
|
||||
html = html.replace(/^\* (.*)$/gim, "<li>$1</li>");
|
||||
html = html.replace(/^\- (.*)$/gim, "<li>$1</li>");
|
||||
html = html.replace(/(<li>.*<\/li>)/s, "<ul>$1</ul>");
|
||||
|
||||
// Paragraphs
|
||||
html = html.replace(/\n\n/g, "</p><p>");
|
||||
html = "<p>" + html + "</p>";
|
||||
|
||||
// Clean up
|
||||
html = html.replace(/<p><\/p>/g, "");
|
||||
html = html.replace(/<p>(<h[1-6]>)/g, "$1");
|
||||
html = html.replace(/(<\/h[1-6]>)<\/p>/g, "$1");
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
async function saveObsidianNote() {
|
||||
if (!showMarkdownEditor.currentNote) {
|
||||
tip("No note loaded", false, "error");
|
||||
return;
|
||||
}
|
||||
|
||||
const content = byId("obsidianMarkdownEditor").value;
|
||||
const {path} = showMarkdownEditor.currentNote;
|
||||
|
||||
try {
|
||||
await ObsidianBridge.updateNote(path, content);
|
||||
showMarkdownEditor.originalContent = content;
|
||||
tip("Note saved to Obsidian vault", true, "success", 2000);
|
||||
} catch (error) {
|
||||
ERROR && console.error("Failed to save note:", error);
|
||||
tip("Failed to save note: " + error.message, true, "error", 5000);
|
||||
}
|
||||
}
|
||||
|
||||
function openInObsidian() {
|
||||
if (!showMarkdownEditor.currentNote) return;
|
||||
|
||||
const {path} = showMarkdownEditor.currentNote;
|
||||
const vaultName = ObsidianBridge.config.vaultName || "vault";
|
||||
const obsidianUrl = `obsidian://open?vault=${encodeURIComponent(vaultName)}&file=${encodeURIComponent(path)}`;
|
||||
|
||||
window.open(obsidianUrl, "_blank");
|
||||
tip("Opening in Obsidian app...", false, "success", 2000);
|
||||
}
|
||||
|
||||
function togglePreviewMode() {
|
||||
const editor = byId("obsidianMarkdownEditor");
|
||||
const preview = byId("obsidianMarkdownPreview");
|
||||
const isPreviewMode = editor.style.display === "none";
|
||||
|
||||
if (isPreviewMode) {
|
||||
editor.style.display = "block";
|
||||
preview.style.display = "none";
|
||||
byId("togglePreview").textContent = "👁 Preview";
|
||||
} else {
|
||||
updateMarkdownPreview();
|
||||
editor.style.display = "none";
|
||||
preview.style.display = "block";
|
||||
byId("togglePreview").textContent = "✏ Edit";
|
||||
}
|
||||
}
|
||||
|
|
@ -121,11 +121,16 @@ function editUnits() {
|
|||
|
||||
function addRuler() {
|
||||
if (!layerIsOn("toggleRulers")) toggleRulers();
|
||||
|
||||
const width = Math.min(graphWidth, svgWidth);
|
||||
const height = Math.min(graphHeight, svgHeight);
|
||||
const pt = byId("map").createSVGPoint();
|
||||
(pt.x = graphWidth / 2), (pt.y = graphHeight / 4);
|
||||
pt.x = width / 2;
|
||||
pt.y = height / 4;
|
||||
const p = pt.matrixTransform(viewbox.node().getScreenCTM().inverse());
|
||||
const dx = graphWidth / 4 / scale;
|
||||
const dy = (rulers.data.length * 40) % (graphHeight / 2);
|
||||
|
||||
const dx = width / 4 / scale;
|
||||
const dy = (rulers.data.length * 40) % (height / 2);
|
||||
const from = [(p.x - dx) | 0, (p.y + dy) | 0];
|
||||
const to = [(p.x + dx) | 0, (p.y + dy) | 0];
|
||||
rulers.create(Ruler, [from, to]).draw();
|
||||
|
|
|
|||
|
|
@ -13,7 +13,9 @@
|
|||
* Example: 1.102.2 -> Major version 1, Minor version 102, Patch version 2
|
||||
*/
|
||||
|
||||
const VERSION = "1.108.11";
|
||||
|
||||
const VERSION = "1.108.13";
|
||||
|
||||
if (parseMapVersion(VERSION) !== VERSION) alert("versioning.js: Invalid format or parsing function");
|
||||
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue