doom-osm-godmode 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +22 -0
- package/README.md +332 -0
- package/examples/colosseum.json +8 -0
- package/examples/demo-site.geojson +101 -0
- package/examples/demo-site.json +7 -0
- package/package.json +56 -0
- package/src/cli.js +68 -0
- package/src/feature-set.js +155 -0
- package/src/geo.js +98 -0
- package/src/geocode.js +26 -0
- package/src/layout.js +233 -0
- package/src/lib/cli.js +67 -0
- package/src/lib/fs.js +38 -0
- package/src/osm.js +110 -0
- package/src/udmf.js +207 -0
- package/src/wad.js +67 -0
- package/src/workflow.js +171 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
const path = require('node:path');
|
|
2
|
+
const { readJson } = require('./lib/fs.js');
|
|
3
|
+
const { closeRing, computeBounds, createProjection, polygonArea } = require('./geo.js');
|
|
4
|
+
|
|
5
|
+
function inferKind(properties = {}) {
|
|
6
|
+
if (properties.kind) {
|
|
7
|
+
return properties.kind;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const tags = properties.tags || {};
|
|
11
|
+
|
|
12
|
+
if (tags.building) {
|
|
13
|
+
return 'building';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (tags.natural === 'water' || tags.water) {
|
|
17
|
+
return 'water';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (tags.leisure || tags.landuse) {
|
|
21
|
+
return 'park';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return 'building';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function extractPrimaryRing(geometry) {
|
|
28
|
+
if (!geometry) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (geometry.type === 'Polygon' && Array.isArray(geometry.coordinates?.[0])) {
|
|
33
|
+
return closeRing(geometry.coordinates[0]);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (geometry.type === 'MultiPolygon' && Array.isArray(geometry.coordinates)) {
|
|
37
|
+
let bestRing = null;
|
|
38
|
+
let bestPointCount = 0;
|
|
39
|
+
|
|
40
|
+
for (const polygon of geometry.coordinates) {
|
|
41
|
+
const ring = closeRing(polygon?.[0]);
|
|
42
|
+
if (Array.isArray(ring) && ring.length > bestPointCount) {
|
|
43
|
+
bestRing = ring;
|
|
44
|
+
bestPointCount = ring.length;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return bestRing;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function collectAllCoordinates(featureCollection) {
|
|
55
|
+
const coordinates = [];
|
|
56
|
+
|
|
57
|
+
for (const feature of featureCollection.features || []) {
|
|
58
|
+
const ring = extractPrimaryRing(feature.geometry);
|
|
59
|
+
if (!ring) {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
for (const [lon, lat] of ring) {
|
|
64
|
+
coordinates.push({ lon: Number(lon), lat: Number(lat) });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return coordinates;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function resolveCenter(featureCollection) {
|
|
72
|
+
const coordinates = collectAllCoordinates(featureCollection);
|
|
73
|
+
if (coordinates.length === 0) {
|
|
74
|
+
throw new Error('No polygon features found.');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const lons = coordinates.map((point) => point.lon);
|
|
78
|
+
const lats = coordinates.map((point) => point.lat);
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
lon: (Math.min(...lons) + Math.max(...lons)) / 2,
|
|
82
|
+
lat: (Math.min(...lats) + Math.max(...lats)) / 2
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function formatFeatureName(feature, index) {
|
|
87
|
+
const candidate = feature.properties?.name;
|
|
88
|
+
if (candidate && String(candidate).trim().length > 0) {
|
|
89
|
+
return String(candidate).trim();
|
|
90
|
+
}
|
|
91
|
+
return `${inferKind(feature.properties)}-${index + 1}`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function prepareFeatureSet(featureCollection, options = {}) {
|
|
95
|
+
const maxRooms = Number.isFinite(options.maxRooms) ? options.maxRooms : 12;
|
|
96
|
+
const center = resolveCenter(featureCollection);
|
|
97
|
+
const projection = createProjection(center.lat, center.lon);
|
|
98
|
+
|
|
99
|
+
const features = [];
|
|
100
|
+
|
|
101
|
+
for (const [index, feature] of (featureCollection.features || []).entries()) {
|
|
102
|
+
const ring = extractPrimaryRing(feature.geometry);
|
|
103
|
+
if (!ring || ring.length < 4) {
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const projectedRing = ring.map(([lon, lat]) => projection.project(Number(lon), Number(lat)));
|
|
108
|
+
const area = Math.abs(polygonArea(projectedRing));
|
|
109
|
+
|
|
110
|
+
if (area < 40) {
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const bounds = computeBounds(projectedRing);
|
|
115
|
+
features.push({
|
|
116
|
+
id: feature.properties?.id || `feature-${index + 1}`,
|
|
117
|
+
name: formatFeatureName(feature, index),
|
|
118
|
+
kind: inferKind(feature.properties),
|
|
119
|
+
sourceProperties: feature.properties || {},
|
|
120
|
+
sourceRing: ring,
|
|
121
|
+
projectedRing,
|
|
122
|
+
areaMeters: area,
|
|
123
|
+
boundsMeters: bounds,
|
|
124
|
+
centroid: {
|
|
125
|
+
x: (bounds.minX + bounds.maxX) / 2,
|
|
126
|
+
y: (bounds.minY + bounds.maxY) / 2
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
features.sort((left, right) => right.areaMeters - left.areaMeters);
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
title: options.title || 'Imported Site',
|
|
135
|
+
center,
|
|
136
|
+
featureCount: features.length,
|
|
137
|
+
features: features.slice(0, maxRooms)
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function loadFeatureCollectionFromFile(filePath) {
|
|
142
|
+
const absolutePath = path.resolve(filePath);
|
|
143
|
+
const value = await readJson(absolutePath);
|
|
144
|
+
|
|
145
|
+
if (value.type !== 'FeatureCollection' || !Array.isArray(value.features)) {
|
|
146
|
+
throw new Error(`Unsupported GeoJSON in ${absolutePath}.`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return value;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
module.exports = {
|
|
153
|
+
loadFeatureCollectionFromFile,
|
|
154
|
+
prepareFeatureSet
|
|
155
|
+
};
|
package/src/geo.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
const EARTH_RADIUS_METERS = 6378137;
|
|
2
|
+
|
|
3
|
+
function toRadians(value) {
|
|
4
|
+
return (value * Math.PI) / 180;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function toDegrees(value) {
|
|
8
|
+
return (value * 180) / Math.PI;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function bboxFromNominatimResult(result) {
|
|
12
|
+
if (!Array.isArray(result.boundingbox) || result.boundingbox.length !== 4) {
|
|
13
|
+
throw new Error('Nominatim result is missing boundingbox.');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return {
|
|
17
|
+
south: Number(result.boundingbox[0]),
|
|
18
|
+
north: Number(result.boundingbox[1]),
|
|
19
|
+
west: Number(result.boundingbox[2]),
|
|
20
|
+
east: Number(result.boundingbox[3])
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function bboxAroundPoint(lat, lon, radiusMeters) {
|
|
25
|
+
const deltaLat = radiusMeters / EARTH_RADIUS_METERS;
|
|
26
|
+
const deltaLon = radiusMeters / (EARTH_RADIUS_METERS * Math.cos(toRadians(lat)));
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
south: lat - toDegrees(deltaLat),
|
|
30
|
+
north: lat + toDegrees(deltaLat),
|
|
31
|
+
west: lon - toDegrees(deltaLon),
|
|
32
|
+
east: lon + toDegrees(deltaLon)
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function createProjection(centerLat, centerLon, scale = 1) {
|
|
37
|
+
const lat0 = toRadians(centerLat);
|
|
38
|
+
const lon0 = toRadians(centerLon);
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
project(lon, lat) {
|
|
42
|
+
const x = (toRadians(lon) - lon0) * Math.cos(lat0) * EARTH_RADIUS_METERS * scale;
|
|
43
|
+
const y = (toRadians(lat) - lat0) * EARTH_RADIUS_METERS * scale;
|
|
44
|
+
return { x, y };
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function computeBounds(points) {
|
|
50
|
+
const xs = points.map((point) => point.x);
|
|
51
|
+
const ys = points.map((point) => point.y);
|
|
52
|
+
return {
|
|
53
|
+
minX: Math.min(...xs),
|
|
54
|
+
maxX: Math.max(...xs),
|
|
55
|
+
minY: Math.min(...ys),
|
|
56
|
+
maxY: Math.max(...ys),
|
|
57
|
+
width: Math.max(...xs) - Math.min(...xs),
|
|
58
|
+
height: Math.max(...ys) - Math.min(...ys)
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function polygonArea(points) {
|
|
63
|
+
let area = 0;
|
|
64
|
+
|
|
65
|
+
for (let index = 0; index < points.length - 1; index += 1) {
|
|
66
|
+
const current = points[index];
|
|
67
|
+
const next = points[index + 1];
|
|
68
|
+
area += current.x * next.y;
|
|
69
|
+
area -= next.x * current.y;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return area / 2;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function closeRing(ring) {
|
|
76
|
+
if (!Array.isArray(ring) || ring.length < 3) {
|
|
77
|
+
return ring;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const first = ring[0];
|
|
81
|
+
const last = ring[ring.length - 1];
|
|
82
|
+
|
|
83
|
+
if (first[0] === last[0] && first[1] === last[1]) {
|
|
84
|
+
return ring;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return [...ring, first];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
module.exports = {
|
|
91
|
+
bboxAroundPoint,
|
|
92
|
+
bboxFromNominatimResult,
|
|
93
|
+
closeRing,
|
|
94
|
+
computeBounds,
|
|
95
|
+
createProjection,
|
|
96
|
+
polygonArea
|
|
97
|
+
};
|
|
98
|
+
|
package/src/geocode.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
const NOMINATIM_URL = 'https://nominatim.openstreetmap.org/search';
|
|
2
|
+
const USER_AGENT = 'doom-osm-wad/0.1.0';
|
|
3
|
+
|
|
4
|
+
async function searchPlace(query, limit = 5) {
|
|
5
|
+
const url = new URL(NOMINATIM_URL);
|
|
6
|
+
url.searchParams.set('format', 'jsonv2');
|
|
7
|
+
url.searchParams.set('limit', String(limit));
|
|
8
|
+
url.searchParams.set('q', query);
|
|
9
|
+
|
|
10
|
+
const response = await fetch(url, {
|
|
11
|
+
headers: {
|
|
12
|
+
'user-agent': USER_AGENT
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
if (!response.ok) {
|
|
17
|
+
throw new Error(`Nominatim search failed with ${response.status}.`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return response.json();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
module.exports = {
|
|
24
|
+
searchPlace
|
|
25
|
+
};
|
|
26
|
+
|
package/src/layout.js
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
const CELL_SIZE = 128;
|
|
2
|
+
const ROOM_GAP_CELLS = 4;
|
|
3
|
+
const OUTER_PADDING_CELLS = 2;
|
|
4
|
+
|
|
5
|
+
function clamp(value, min, max) {
|
|
6
|
+
return Math.min(max, Math.max(min, value));
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function themeForKind(kind) {
|
|
10
|
+
if (kind === 'water') {
|
|
11
|
+
return {
|
|
12
|
+
floorHeight: -24,
|
|
13
|
+
ceilingHeight: 144,
|
|
14
|
+
floorTexture: 'NUKAGE1',
|
|
15
|
+
ceilingTexture: 'CEIL3_5',
|
|
16
|
+
wallTexture: 'STONE2',
|
|
17
|
+
lightLevel: 144
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (kind === 'park') {
|
|
22
|
+
return {
|
|
23
|
+
floorHeight: 0,
|
|
24
|
+
ceilingHeight: 192,
|
|
25
|
+
floorTexture: 'FLOOR0_3',
|
|
26
|
+
ceilingTexture: 'F_SKY1',
|
|
27
|
+
wallTexture: 'BROWN96',
|
|
28
|
+
lightLevel: 176
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (kind === 'road') {
|
|
33
|
+
return {
|
|
34
|
+
floorHeight: 0,
|
|
35
|
+
ceilingHeight: 160,
|
|
36
|
+
floorTexture: 'FLOOR4_8',
|
|
37
|
+
ceilingTexture: 'CEIL3_5',
|
|
38
|
+
wallTexture: 'STARTAN3',
|
|
39
|
+
lightLevel: 160
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
floorHeight: 0,
|
|
45
|
+
ceilingHeight: 176,
|
|
46
|
+
floorTexture: 'FLOOR0_1',
|
|
47
|
+
ceilingTexture: 'CEIL5_1',
|
|
48
|
+
wallTexture: 'BIGBRIK1',
|
|
49
|
+
lightLevel: 152
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function sizeFromFeature(feature) {
|
|
54
|
+
const widthCells = clamp(Math.round(feature.boundsMeters.width / 12) + 4, 6, 18);
|
|
55
|
+
const heightCells = clamp(Math.round(feature.boundsMeters.height / 12) + 4, 6, 18);
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
widthCells,
|
|
59
|
+
heightCells
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function markCell(cells, x, y, value) {
|
|
64
|
+
const key = `${x},${y}`;
|
|
65
|
+
if (cells.has(key)) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
cells.set(key, {
|
|
70
|
+
x,
|
|
71
|
+
y,
|
|
72
|
+
...value
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function paintCorridor(cells, from, to) {
|
|
77
|
+
const width = 2;
|
|
78
|
+
|
|
79
|
+
const paintSpan = (x, y, horizontal) => {
|
|
80
|
+
if (horizontal) {
|
|
81
|
+
for (let offset = 0; offset < width; offset += 1) {
|
|
82
|
+
markCell(cells, x, y + offset, { kind: 'road', label: 'corridor' });
|
|
83
|
+
}
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
for (let offset = 0; offset < width; offset += 1) {
|
|
88
|
+
markCell(cells, x + offset, y, { kind: 'road', label: 'corridor' });
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const stepX = from.x <= to.x ? 1 : -1;
|
|
93
|
+
for (let x = from.x; x !== to.x; x += stepX) {
|
|
94
|
+
paintSpan(x, from.y, true);
|
|
95
|
+
}
|
|
96
|
+
paintSpan(to.x, from.y, true);
|
|
97
|
+
|
|
98
|
+
const stepY = from.y <= to.y ? 1 : -1;
|
|
99
|
+
for (let y = from.y; y !== to.y; y += stepY) {
|
|
100
|
+
paintSpan(to.x, y, false);
|
|
101
|
+
}
|
|
102
|
+
paintSpan(to.x, to.y, false);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function fallbackFeatureSet() {
|
|
106
|
+
return [
|
|
107
|
+
{
|
|
108
|
+
id: 'fallback-1',
|
|
109
|
+
name: 'Start Plaza',
|
|
110
|
+
kind: 'building',
|
|
111
|
+
boundsMeters: { width: 48, height: 48 }
|
|
112
|
+
}
|
|
113
|
+
];
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function buildRoomPlacements(features) {
|
|
117
|
+
const source = features.length > 0 ? features : fallbackFeatureSet();
|
|
118
|
+
const rooms = source.map((feature, index) => {
|
|
119
|
+
const size = sizeFromFeature(feature);
|
|
120
|
+
return {
|
|
121
|
+
id: `room-${index + 1}`,
|
|
122
|
+
featureId: feature.id,
|
|
123
|
+
featureName: feature.name,
|
|
124
|
+
kind: feature.kind,
|
|
125
|
+
widthCells: size.widthCells,
|
|
126
|
+
heightCells: size.heightCells
|
|
127
|
+
};
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
const columns = Math.max(1, Math.ceil(Math.sqrt(rooms.length)));
|
|
131
|
+
const rows = Math.ceil(rooms.length / columns);
|
|
132
|
+
const columnWidths = Array.from({ length: columns }, () => 0);
|
|
133
|
+
const rowHeights = Array.from({ length: rows }, () => 0);
|
|
134
|
+
|
|
135
|
+
for (const [index, room] of rooms.entries()) {
|
|
136
|
+
const column = index % columns;
|
|
137
|
+
const row = Math.floor(index / columns);
|
|
138
|
+
columnWidths[column] = Math.max(columnWidths[column], room.widthCells);
|
|
139
|
+
rowHeights[row] = Math.max(rowHeights[row], room.heightCells);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const xOffsets = [];
|
|
143
|
+
const yOffsets = [];
|
|
144
|
+
let runningX = OUTER_PADDING_CELLS;
|
|
145
|
+
let runningY = OUTER_PADDING_CELLS;
|
|
146
|
+
|
|
147
|
+
for (const width of columnWidths) {
|
|
148
|
+
xOffsets.push(runningX);
|
|
149
|
+
runningX += width + ROOM_GAP_CELLS;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
for (const height of rowHeights) {
|
|
153
|
+
yOffsets.push(runningY);
|
|
154
|
+
runningY += height + ROOM_GAP_CELLS;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return rooms.map((room, index) => {
|
|
158
|
+
const column = index % columns;
|
|
159
|
+
const row = Math.floor(index / columns);
|
|
160
|
+
const roomX = xOffsets[column] + Math.floor((columnWidths[column] - room.widthCells) / 2);
|
|
161
|
+
const roomY = yOffsets[row] + Math.floor((rowHeights[row] - room.heightCells) / 2);
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
...room,
|
|
165
|
+
row,
|
|
166
|
+
column,
|
|
167
|
+
x: roomX,
|
|
168
|
+
y: roomY,
|
|
169
|
+
center: {
|
|
170
|
+
x: roomX + Math.floor(room.widthCells / 2),
|
|
171
|
+
y: roomY + Math.floor(room.heightCells / 2)
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function buildLayout(featureSet) {
|
|
178
|
+
const placements = buildRoomPlacements(featureSet.features);
|
|
179
|
+
const cells = new Map();
|
|
180
|
+
|
|
181
|
+
for (const room of placements) {
|
|
182
|
+
for (let x = room.x; x < room.x + room.widthCells; x += 1) {
|
|
183
|
+
for (let y = room.y; y < room.y + room.heightCells; y += 1) {
|
|
184
|
+
markCell(cells, x, y, {
|
|
185
|
+
kind: room.kind,
|
|
186
|
+
roomId: room.id,
|
|
187
|
+
label: room.featureName
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
for (let index = 0; index < placements.length - 1; index += 1) {
|
|
194
|
+
paintCorridor(cells, placements[index].center, placements[index + 1].center);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const cellList = Array.from(cells.values()).sort((left, right) => {
|
|
198
|
+
if (left.y !== right.y) {
|
|
199
|
+
return left.y - right.y;
|
|
200
|
+
}
|
|
201
|
+
return left.x - right.x;
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
const xs = cellList.map((cell) => cell.x);
|
|
205
|
+
const ys = cellList.map((cell) => cell.y);
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
title: featureSet.title,
|
|
209
|
+
cellSize: CELL_SIZE,
|
|
210
|
+
gridBounds: {
|
|
211
|
+
minX: Math.min(...xs),
|
|
212
|
+
maxX: Math.max(...xs),
|
|
213
|
+
minY: Math.min(...ys),
|
|
214
|
+
maxY: Math.max(...ys)
|
|
215
|
+
},
|
|
216
|
+
rooms: placements,
|
|
217
|
+
cells: cellList.map((cell) => ({
|
|
218
|
+
...cell,
|
|
219
|
+
theme: themeForKind(cell.kind)
|
|
220
|
+
})),
|
|
221
|
+
playerStart: placements[0]
|
|
222
|
+
? {
|
|
223
|
+
x: placements[0].center.x,
|
|
224
|
+
y: placements[0].center.y
|
|
225
|
+
}
|
|
226
|
+
: { x: OUTER_PADDING_CELLS + 3, y: OUTER_PADDING_CELLS + 3 }
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
module.exports = {
|
|
231
|
+
buildLayout
|
|
232
|
+
};
|
|
233
|
+
|
package/src/lib/cli.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
function parseArgs(argv) {
|
|
2
|
+
const parsed = { _: [] };
|
|
3
|
+
|
|
4
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
5
|
+
const token = argv[index];
|
|
6
|
+
|
|
7
|
+
if (!token.startsWith('--')) {
|
|
8
|
+
parsed._.push(token);
|
|
9
|
+
continue;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const key = token.slice(2);
|
|
13
|
+
const next = argv[index + 1];
|
|
14
|
+
|
|
15
|
+
if (!next || next.startsWith('--')) {
|
|
16
|
+
parsed[key] = true;
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
parsed[key] = next;
|
|
21
|
+
index += 1;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return parsed;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function readStringFlag(args, name, fallback) {
|
|
28
|
+
const value = args[name];
|
|
29
|
+
if (typeof value === 'string' && value.length > 0) {
|
|
30
|
+
return value;
|
|
31
|
+
}
|
|
32
|
+
return fallback;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function readNumberFlag(args, name, fallback) {
|
|
36
|
+
const value = args[name];
|
|
37
|
+
if (value === undefined || value === true) {
|
|
38
|
+
return fallback;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const parsed = Number(value);
|
|
42
|
+
if (!Number.isFinite(parsed)) {
|
|
43
|
+
throw new Error(`Flag --${name} must be numeric.`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return parsed;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function printHelp() {
|
|
50
|
+
console.log(`
|
|
51
|
+
doom-osm-godmode — turn real places into GZDoom WADs
|
|
52
|
+
|
|
53
|
+
Commands:
|
|
54
|
+
search <query>
|
|
55
|
+
build-place <query> [--radius 450] [--map MAP01] [--max-rooms 12] [--title name] [--out output/site]
|
|
56
|
+
build-config <config.json>
|
|
57
|
+
demo [--out output/demo-site]
|
|
58
|
+
`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
module.exports = {
|
|
62
|
+
parseArgs,
|
|
63
|
+
printHelp,
|
|
64
|
+
readStringFlag,
|
|
65
|
+
readNumberFlag
|
|
66
|
+
};
|
|
67
|
+
|
package/src/lib/fs.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
const fs = require('node:fs/promises');
|
|
2
|
+
const path = require('node:path');
|
|
3
|
+
|
|
4
|
+
async function ensureDir(dirPath) {
|
|
5
|
+
await fs.mkdir(dirPath, { recursive: true });
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
async function readJson(filePath) {
|
|
9
|
+
const text = await fs.readFile(filePath, 'utf8');
|
|
10
|
+
return JSON.parse(text);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async function writeJson(filePath, value) {
|
|
14
|
+
await ensureDir(path.dirname(filePath));
|
|
15
|
+
await fs.writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function writeText(filePath, text) {
|
|
19
|
+
await ensureDir(path.dirname(filePath));
|
|
20
|
+
await fs.writeFile(filePath, text, 'utf8');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function slugify(value) {
|
|
24
|
+
return String(value)
|
|
25
|
+
.trim()
|
|
26
|
+
.toLowerCase()
|
|
27
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
28
|
+
.replace(/^-+|-+$/g, '') || 'site';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
module.exports = {
|
|
32
|
+
ensureDir,
|
|
33
|
+
readJson,
|
|
34
|
+
writeJson,
|
|
35
|
+
writeText,
|
|
36
|
+
slugify
|
|
37
|
+
};
|
|
38
|
+
|