mirror of
https://github.com/Azgaar/Fantasy-Map-Generator.git
synced 2025-12-17 17:51: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>
|
<td>
|
||||||
<select id="onloadBehavior" data-stored="onloadBehavior">
|
<select id="onloadBehavior" data-stored="onloadBehavior">
|
||||||
<option value="random" selected>Generate random map</option>
|
<option value="random" selected>Generate random map</option>
|
||||||
|
<option value="default">Open default map</option>
|
||||||
<option value="lastSaved">Open last saved map</option>
|
<option value="lastSaved">Open last saved map</option>
|
||||||
</select>
|
</select>
|
||||||
</td>
|
</td>
|
||||||
|
|
@ -2061,6 +2062,9 @@
|
||||||
Namesbase
|
Namesbase
|
||||||
</button>
|
</button>
|
||||||
<button id="editNotesButton" data-tip="Click to open Notes Editor" data-shortcut="Shift + O">Notes</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">
|
<button id="editProvincesButton" data-tip="Click to open Provinces Editor" data-shortcut="Shift + P">
|
||||||
Provinces
|
Provinces
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -4962,6 +4966,107 @@
|
||||||
</div>
|
</div>
|
||||||
</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 id="aiGenerator" class="dialog stable" style="display: none">
|
||||||
<div style="display: flex; flex-direction: column; gap: 0.3em; width: 100%">
|
<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>
|
<textarea id="aiGeneratorResult" placeholder="Generated text will appear here" cols="30" rows="10"></textarea>
|
||||||
|
|
@ -6087,6 +6192,15 @@
|
||||||
browser
|
browser
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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>
|
<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
|
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.
|
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/style-presets.js?v=1.100.00"></script>
|
||||||
<script src="modules/ui/general.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="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/style.js?v=1.108.4"></script>
|
||||||
<script defer src="modules/ui/editors.js?v=1.108.5"></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/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/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/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/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/ai-generator.js?v=1.108.8"></script>
|
||||||
<script defer src="modules/ui/diplomacy-editor.js?v=1.99.00"></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="modules/coa-renderer.js?v=1.99.00"></script>
|
||||||
<script defer src="libs/rgbquant.min.js"></script>
|
<script defer src="libs/rgbquant.min.js"></script>
|
||||||
<script defer src="libs/jquery.ui.touch-punch.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/load.js?v=1.108.0"></script>
|
||||||
<script defer src="modules/io/cloud.js?v=1.106.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/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-features.js?v=1.108.2"></script>
|
||||||
<script defer src="modules/renderers/draw-borders.js?v=1.104.0"></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;
|
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
|
// check if there is a map saved to indexedDB
|
||||||
if (byId("onloadBehavior").value === "lastSaved") {
|
if (byId("onloadBehavior").value === "lastSaved") {
|
||||||
try {
|
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");
|
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
|
// download map file
|
||||||
function saveToMachine(mapData, filename) {
|
function saveToMachine(mapData, filename) {
|
||||||
const blob = new Blob([mapData], {type: "text/plain"});
|
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() {
|
function addRuler() {
|
||||||
if (!layerIsOn("toggleRulers")) toggleRulers();
|
if (!layerIsOn("toggleRulers")) toggleRulers();
|
||||||
|
|
||||||
|
const width = Math.min(graphWidth, svgWidth);
|
||||||
|
const height = Math.min(graphHeight, svgHeight);
|
||||||
const pt = byId("map").createSVGPoint();
|
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 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 from = [(p.x - dx) | 0, (p.y + dy) | 0];
|
||||||
const to = [(p.x + dx) | 0, (p.y + dy) | 0];
|
const to = [(p.x + dx) | 0, (p.y + dy) | 0];
|
||||||
rulers.create(Ruler, [from, to]).draw();
|
rulers.create(Ruler, [from, to]).draw();
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,9 @@
|
||||||
* Example: 1.102.2 -> Major version 1, Minor version 102, Patch version 2
|
* 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");
|
if (parseMapVersion(VERSION) !== VERSION) alert("versioning.js: Invalid format or parsing function");
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue