@connected-web/terrain-editor 0.1.2 → 0.1.4
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/dist/index.cjs +64 -0
- package/dist/index.d.cts +31 -1
- package/dist/index.d.ts +31 -1
- package/dist/index.js +62 -0
- package/documentation/WYN-FILE-FORMAT.md +198 -0
- package/documentation/schemas/legend.schema.json +66 -0
- package/documentation/schemas/locations.schema.json +64 -0
- package/documentation/schemas/metadata.schema.json +12 -0
- package/documentation/schemas/theme.schema.json +83 -0
- package/package.json +11 -4
package/dist/index.cjs
CHANGED
|
@@ -34,11 +34,13 @@ __export(index_exports, {
|
|
|
34
34
|
buildRimMesh: () => buildRimMesh,
|
|
35
35
|
buildWynArchive: () => buildWynArchive,
|
|
36
36
|
createHeightSampler: () => createHeightSampler,
|
|
37
|
+
createIconUrlMap: () => createIconUrlMap,
|
|
37
38
|
createLayerBrowserStore: () => createLayerBrowserStore,
|
|
38
39
|
createMaskEditor: () => createMaskEditor,
|
|
39
40
|
createProjectStore: () => createProjectStore,
|
|
40
41
|
createTerrainViewerHost: () => createTerrainViewerHost,
|
|
41
42
|
createViewerOverlay: () => createViewerOverlay,
|
|
43
|
+
enhanceLocationsWithIconUrls: () => enhanceLocationsWithIconUrls,
|
|
42
44
|
getDefaultTerrainTheme: () => getDefaultTerrainTheme,
|
|
43
45
|
initTerrainViewer: () => initTerrainViewer,
|
|
44
46
|
loadWynArchive: () => loadWynArchive,
|
|
@@ -1655,6 +1657,9 @@ async function initTerrainViewer(container, dataset, options = {}) {
|
|
|
1655
1657
|
},
|
|
1656
1658
|
setCameraOffset: (offset, focusId) => {
|
|
1657
1659
|
cameraOffset.target = THREE2.MathUtils.clamp(offset, -0.45, 0.45);
|
|
1660
|
+
if (cameraTween) {
|
|
1661
|
+
return;
|
|
1662
|
+
}
|
|
1658
1663
|
const targetId = focusId ?? currentFocusId;
|
|
1659
1664
|
if (targetId) {
|
|
1660
1665
|
const loc = currentLocations.find((item) => item.id === targetId);
|
|
@@ -3244,17 +3249,76 @@ function createMaskEditor(options) {
|
|
|
3244
3249
|
markClean
|
|
3245
3250
|
};
|
|
3246
3251
|
}
|
|
3252
|
+
|
|
3253
|
+
// src/iconHelpers.ts
|
|
3254
|
+
function inferMimeType(path, fallback = "image/png") {
|
|
3255
|
+
const extension = path.split(".").pop()?.toLowerCase();
|
|
3256
|
+
if (!extension) return fallback;
|
|
3257
|
+
if (extension === "png") return "image/png";
|
|
3258
|
+
if (extension === "jpg" || extension === "jpeg") return "image/jpeg";
|
|
3259
|
+
if (extension === "webp") return "image/webp";
|
|
3260
|
+
if (extension === "gif") return "image/gif";
|
|
3261
|
+
if (extension === "svg") return "image/svg+xml";
|
|
3262
|
+
return fallback;
|
|
3263
|
+
}
|
|
3264
|
+
function toObjectUrl(entry) {
|
|
3265
|
+
const type = entry.type ?? inferMimeType(entry.path);
|
|
3266
|
+
const blob = new Blob([entry.data], { type });
|
|
3267
|
+
return URL.createObjectURL(blob);
|
|
3268
|
+
}
|
|
3269
|
+
function createIconUrlMap(files, options = {}) {
|
|
3270
|
+
const urls = /* @__PURE__ */ new Map();
|
|
3271
|
+
const prefix = options.prefix ?? "icons/";
|
|
3272
|
+
if (!files?.length) {
|
|
3273
|
+
return {
|
|
3274
|
+
urls,
|
|
3275
|
+
cleanup: () => void 0
|
|
3276
|
+
};
|
|
3277
|
+
}
|
|
3278
|
+
for (const entry of files) {
|
|
3279
|
+
if (!entry.path) continue;
|
|
3280
|
+
if (prefix && !entry.path.startsWith(prefix)) continue;
|
|
3281
|
+
const url = toObjectUrl(entry);
|
|
3282
|
+
urls.set(entry.path, url);
|
|
3283
|
+
}
|
|
3284
|
+
const cleanup = () => {
|
|
3285
|
+
for (const url of urls.values()) {
|
|
3286
|
+
URL.revokeObjectURL(url);
|
|
3287
|
+
}
|
|
3288
|
+
urls.clear();
|
|
3289
|
+
};
|
|
3290
|
+
return { urls, cleanup };
|
|
3291
|
+
}
|
|
3292
|
+
function enhanceLocationsWithIconUrls(locations, files, options) {
|
|
3293
|
+
const { urls, cleanup } = createIconUrlMap(files, options);
|
|
3294
|
+
const enhanced = locations.map((location) => {
|
|
3295
|
+
if (!location.icon) {
|
|
3296
|
+
return { ...location };
|
|
3297
|
+
}
|
|
3298
|
+
const iconUrl = urls.get(location.icon);
|
|
3299
|
+
if (!iconUrl) {
|
|
3300
|
+
return { ...location };
|
|
3301
|
+
}
|
|
3302
|
+
return {
|
|
3303
|
+
...location,
|
|
3304
|
+
iconUrl
|
|
3305
|
+
};
|
|
3306
|
+
});
|
|
3307
|
+
return { locations: enhanced, cleanup };
|
|
3308
|
+
}
|
|
3247
3309
|
// Annotate the CommonJS export names for ESM import in node:
|
|
3248
3310
|
0 && (module.exports = {
|
|
3249
3311
|
applyHeightField,
|
|
3250
3312
|
buildRimMesh,
|
|
3251
3313
|
buildWynArchive,
|
|
3252
3314
|
createHeightSampler,
|
|
3315
|
+
createIconUrlMap,
|
|
3253
3316
|
createLayerBrowserStore,
|
|
3254
3317
|
createMaskEditor,
|
|
3255
3318
|
createProjectStore,
|
|
3256
3319
|
createTerrainViewerHost,
|
|
3257
3320
|
createViewerOverlay,
|
|
3321
|
+
enhanceLocationsWithIconUrls,
|
|
3258
3322
|
getDefaultTerrainTheme,
|
|
3259
3323
|
initTerrainViewer,
|
|
3260
3324
|
loadWynArchive,
|
package/dist/index.d.cts
CHANGED
|
@@ -406,4 +406,34 @@ type MaskEditorOptions = {
|
|
|
406
406
|
};
|
|
407
407
|
declare function createMaskEditor(options: MaskEditorOptions): MaskEditorHandle;
|
|
408
408
|
|
|
409
|
-
|
|
409
|
+
type CreateIconUrlMapOptions = {
|
|
410
|
+
/**
|
|
411
|
+
* Directory prefix to match when building the icon map.
|
|
412
|
+
* Defaults to `icons/`, matching standard .wyn archives.
|
|
413
|
+
*/
|
|
414
|
+
prefix?: string;
|
|
415
|
+
};
|
|
416
|
+
type IconUrlEntry = {
|
|
417
|
+
path: string;
|
|
418
|
+
url: string;
|
|
419
|
+
};
|
|
420
|
+
type IconUrlMapResult = {
|
|
421
|
+
/**
|
|
422
|
+
* Map of file paths (relative to the archive root) to object URLs.
|
|
423
|
+
*/
|
|
424
|
+
urls: Map<string, string>;
|
|
425
|
+
/**
|
|
426
|
+
* Revoke all allocated object URLs.
|
|
427
|
+
*/
|
|
428
|
+
cleanup: () => void;
|
|
429
|
+
};
|
|
430
|
+
type EnhancedLocationsResult<T extends TerrainLocation = TerrainLocation> = {
|
|
431
|
+
locations: Array<T & {
|
|
432
|
+
iconUrl?: string;
|
|
433
|
+
}>;
|
|
434
|
+
cleanup: () => void;
|
|
435
|
+
};
|
|
436
|
+
declare function createIconUrlMap(files?: TerrainProjectFileEntry[], options?: CreateIconUrlMapOptions): IconUrlMapResult;
|
|
437
|
+
declare function enhanceLocationsWithIconUrls<T extends TerrainLocation = TerrainLocation>(locations: T[], files?: TerrainProjectFileEntry[], options?: CreateIconUrlMapOptions): EnhancedLocationsResult<T>;
|
|
438
|
+
|
|
439
|
+
export { type BuildWynArchiveOptions, type Cleanup, type CreateIconUrlMapOptions, type DeepPartial, type EnhancedLocationsResult, type HeightSampler, type IconUrlEntry, type IconUrlMapResult, type LayerBrowserEntry, type LayerBrowserState, type LayerBrowserStore, type LayerToggleState, type LegendLayer, type LoadWynArchiveOptions, type LoadedWynFile, type LocationPickPayload, type LocationViewState, type MarkerSpriteStateStyle, type MarkerSpriteTheme, type MarkerStemGeometryShape, type MarkerStemStateStyle, type MarkerStemTheme, type MaskEditorHandle, type MaskEditorState, type MaskImage, type MaskStroke, type MaskStrokeMode, type MaskStrokePoint, type TerrainDataset, type TerrainHandle, type TerrainLegend, type TerrainLocation, type TerrainProjectFileEntry, type TerrainProjectMetadata, type TerrainProjectSnapshot, type TerrainProjectStore, type TerrainTheme, type TerrainThemeOverrides, type TerrainViewMode, type TerrainViewerHostHandle, type TerrainViewerHostOptions, type ViewerLifecycleState, type ViewerOverlayCustomButton, type ViewerOverlayHandle, type ViewerOverlayLoadingState, type ViewerOverlayOptions, type WynArchiveProgressEvent, applyHeightField, buildRimMesh, buildWynArchive, createHeightSampler, createIconUrlMap, createLayerBrowserStore, createMaskEditor, createProjectStore, createTerrainViewerHost, createViewerOverlay, enhanceLocationsWithIconUrls, getDefaultTerrainTheme, initTerrainViewer, loadWynArchive, loadWynArchiveFromArrayBuffer, loadWynArchiveFromFile, resolveTerrainTheme, sampleHeightValue };
|
package/dist/index.d.ts
CHANGED
|
@@ -406,4 +406,34 @@ type MaskEditorOptions = {
|
|
|
406
406
|
};
|
|
407
407
|
declare function createMaskEditor(options: MaskEditorOptions): MaskEditorHandle;
|
|
408
408
|
|
|
409
|
-
|
|
409
|
+
type CreateIconUrlMapOptions = {
|
|
410
|
+
/**
|
|
411
|
+
* Directory prefix to match when building the icon map.
|
|
412
|
+
* Defaults to `icons/`, matching standard .wyn archives.
|
|
413
|
+
*/
|
|
414
|
+
prefix?: string;
|
|
415
|
+
};
|
|
416
|
+
type IconUrlEntry = {
|
|
417
|
+
path: string;
|
|
418
|
+
url: string;
|
|
419
|
+
};
|
|
420
|
+
type IconUrlMapResult = {
|
|
421
|
+
/**
|
|
422
|
+
* Map of file paths (relative to the archive root) to object URLs.
|
|
423
|
+
*/
|
|
424
|
+
urls: Map<string, string>;
|
|
425
|
+
/**
|
|
426
|
+
* Revoke all allocated object URLs.
|
|
427
|
+
*/
|
|
428
|
+
cleanup: () => void;
|
|
429
|
+
};
|
|
430
|
+
type EnhancedLocationsResult<T extends TerrainLocation = TerrainLocation> = {
|
|
431
|
+
locations: Array<T & {
|
|
432
|
+
iconUrl?: string;
|
|
433
|
+
}>;
|
|
434
|
+
cleanup: () => void;
|
|
435
|
+
};
|
|
436
|
+
declare function createIconUrlMap(files?: TerrainProjectFileEntry[], options?: CreateIconUrlMapOptions): IconUrlMapResult;
|
|
437
|
+
declare function enhanceLocationsWithIconUrls<T extends TerrainLocation = TerrainLocation>(locations: T[], files?: TerrainProjectFileEntry[], options?: CreateIconUrlMapOptions): EnhancedLocationsResult<T>;
|
|
438
|
+
|
|
439
|
+
export { type BuildWynArchiveOptions, type Cleanup, type CreateIconUrlMapOptions, type DeepPartial, type EnhancedLocationsResult, type HeightSampler, type IconUrlEntry, type IconUrlMapResult, type LayerBrowserEntry, type LayerBrowserState, type LayerBrowserStore, type LayerToggleState, type LegendLayer, type LoadWynArchiveOptions, type LoadedWynFile, type LocationPickPayload, type LocationViewState, type MarkerSpriteStateStyle, type MarkerSpriteTheme, type MarkerStemGeometryShape, type MarkerStemStateStyle, type MarkerStemTheme, type MaskEditorHandle, type MaskEditorState, type MaskImage, type MaskStroke, type MaskStrokeMode, type MaskStrokePoint, type TerrainDataset, type TerrainHandle, type TerrainLegend, type TerrainLocation, type TerrainProjectFileEntry, type TerrainProjectMetadata, type TerrainProjectSnapshot, type TerrainProjectStore, type TerrainTheme, type TerrainThemeOverrides, type TerrainViewMode, type TerrainViewerHostHandle, type TerrainViewerHostOptions, type ViewerLifecycleState, type ViewerOverlayCustomButton, type ViewerOverlayHandle, type ViewerOverlayLoadingState, type ViewerOverlayOptions, type WynArchiveProgressEvent, applyHeightField, buildRimMesh, buildWynArchive, createHeightSampler, createIconUrlMap, createLayerBrowserStore, createMaskEditor, createProjectStore, createTerrainViewerHost, createViewerOverlay, enhanceLocationsWithIconUrls, getDefaultTerrainTheme, initTerrainViewer, loadWynArchive, loadWynArchiveFromArrayBuffer, loadWynArchiveFromFile, resolveTerrainTheme, sampleHeightValue };
|
package/dist/index.js
CHANGED
|
@@ -1604,6 +1604,9 @@ async function initTerrainViewer(container, dataset, options = {}) {
|
|
|
1604
1604
|
},
|
|
1605
1605
|
setCameraOffset: (offset, focusId) => {
|
|
1606
1606
|
cameraOffset.target = THREE2.MathUtils.clamp(offset, -0.45, 0.45);
|
|
1607
|
+
if (cameraTween) {
|
|
1608
|
+
return;
|
|
1609
|
+
}
|
|
1607
1610
|
const targetId = focusId ?? currentFocusId;
|
|
1608
1611
|
if (targetId) {
|
|
1609
1612
|
const loc = currentLocations.find((item) => item.id === targetId);
|
|
@@ -3193,16 +3196,75 @@ function createMaskEditor(options) {
|
|
|
3193
3196
|
markClean
|
|
3194
3197
|
};
|
|
3195
3198
|
}
|
|
3199
|
+
|
|
3200
|
+
// src/iconHelpers.ts
|
|
3201
|
+
function inferMimeType(path, fallback = "image/png") {
|
|
3202
|
+
const extension = path.split(".").pop()?.toLowerCase();
|
|
3203
|
+
if (!extension) return fallback;
|
|
3204
|
+
if (extension === "png") return "image/png";
|
|
3205
|
+
if (extension === "jpg" || extension === "jpeg") return "image/jpeg";
|
|
3206
|
+
if (extension === "webp") return "image/webp";
|
|
3207
|
+
if (extension === "gif") return "image/gif";
|
|
3208
|
+
if (extension === "svg") return "image/svg+xml";
|
|
3209
|
+
return fallback;
|
|
3210
|
+
}
|
|
3211
|
+
function toObjectUrl(entry) {
|
|
3212
|
+
const type = entry.type ?? inferMimeType(entry.path);
|
|
3213
|
+
const blob = new Blob([entry.data], { type });
|
|
3214
|
+
return URL.createObjectURL(blob);
|
|
3215
|
+
}
|
|
3216
|
+
function createIconUrlMap(files, options = {}) {
|
|
3217
|
+
const urls = /* @__PURE__ */ new Map();
|
|
3218
|
+
const prefix = options.prefix ?? "icons/";
|
|
3219
|
+
if (!files?.length) {
|
|
3220
|
+
return {
|
|
3221
|
+
urls,
|
|
3222
|
+
cleanup: () => void 0
|
|
3223
|
+
};
|
|
3224
|
+
}
|
|
3225
|
+
for (const entry of files) {
|
|
3226
|
+
if (!entry.path) continue;
|
|
3227
|
+
if (prefix && !entry.path.startsWith(prefix)) continue;
|
|
3228
|
+
const url = toObjectUrl(entry);
|
|
3229
|
+
urls.set(entry.path, url);
|
|
3230
|
+
}
|
|
3231
|
+
const cleanup = () => {
|
|
3232
|
+
for (const url of urls.values()) {
|
|
3233
|
+
URL.revokeObjectURL(url);
|
|
3234
|
+
}
|
|
3235
|
+
urls.clear();
|
|
3236
|
+
};
|
|
3237
|
+
return { urls, cleanup };
|
|
3238
|
+
}
|
|
3239
|
+
function enhanceLocationsWithIconUrls(locations, files, options) {
|
|
3240
|
+
const { urls, cleanup } = createIconUrlMap(files, options);
|
|
3241
|
+
const enhanced = locations.map((location) => {
|
|
3242
|
+
if (!location.icon) {
|
|
3243
|
+
return { ...location };
|
|
3244
|
+
}
|
|
3245
|
+
const iconUrl = urls.get(location.icon);
|
|
3246
|
+
if (!iconUrl) {
|
|
3247
|
+
return { ...location };
|
|
3248
|
+
}
|
|
3249
|
+
return {
|
|
3250
|
+
...location,
|
|
3251
|
+
iconUrl
|
|
3252
|
+
};
|
|
3253
|
+
});
|
|
3254
|
+
return { locations: enhanced, cleanup };
|
|
3255
|
+
}
|
|
3196
3256
|
export {
|
|
3197
3257
|
applyHeightField,
|
|
3198
3258
|
buildRimMesh,
|
|
3199
3259
|
buildWynArchive,
|
|
3200
3260
|
createHeightSampler,
|
|
3261
|
+
createIconUrlMap,
|
|
3201
3262
|
createLayerBrowserStore,
|
|
3202
3263
|
createMaskEditor,
|
|
3203
3264
|
createProjectStore,
|
|
3204
3265
|
createTerrainViewerHost,
|
|
3205
3266
|
createViewerOverlay,
|
|
3267
|
+
enhanceLocationsWithIconUrls,
|
|
3206
3268
|
getDefaultTerrainTheme,
|
|
3207
3269
|
initTerrainViewer,
|
|
3208
3270
|
loadWynArchive,
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# WYN File Format
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
`.wyn` files are ZIP archives containing terrain data, layer masks, and optional metadata used by
|
|
6
|
+
`@connected-web/terrain-editor`. The loader expects JSON descriptors plus referenced PNG assets.
|
|
7
|
+
All asset paths are relative to the archive root.
|
|
8
|
+
|
|
9
|
+
## Archive Layout
|
|
10
|
+
|
|
11
|
+
Required:
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
legend.json
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Common (referenced by `legend.json`):
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
layers/
|
|
21
|
+
icons/
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Optional:
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
locations.json
|
|
28
|
+
theme.json
|
|
29
|
+
metadata.json
|
|
30
|
+
thumbnails/
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
The archive can include any additional assets as long as paths match the JSON references.
|
|
34
|
+
|
|
35
|
+
## legend.json (required)
|
|
36
|
+
|
|
37
|
+
Describes terrain dimensions, height/topology maps, and per-layer masks.
|
|
38
|
+
|
|
39
|
+
Schema: `schemas/legend.schema.json`
|
|
40
|
+
|
|
41
|
+
```json
|
|
42
|
+
{
|
|
43
|
+
"size": [1024, 1536],
|
|
44
|
+
"sea_level": 0.35,
|
|
45
|
+
"heightmap": "layers/heightmap.png",
|
|
46
|
+
"topology": "layers/topology.png",
|
|
47
|
+
"biomes": {
|
|
48
|
+
"forest": { "mask": "layers/forest_mask.png", "rgb": [48, 92, 54], "label": "Forest" }
|
|
49
|
+
},
|
|
50
|
+
"overlays": {
|
|
51
|
+
"water": { "mask": "layers/water_mask.png", "rgb": [34, 92, 124], "label": "Water" }
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Schema:
|
|
57
|
+
|
|
58
|
+
- `size`: `[width, height]` in pixels for the map and mask assets.
|
|
59
|
+
- `sea_level`: Optional float in 0-1 heightmap range; used to offset terrain vertically.
|
|
60
|
+
- `heightmap`: Path to a PNG. The viewer samples the **red channel** (0-255) as height data.
|
|
61
|
+
- `topology`: Optional path to a PNG used for shaded legend/composite rendering. Falls back to
|
|
62
|
+
`heightmap` when omitted.
|
|
63
|
+
- `biomes`: Record of biome layers. Keys are layer ids used by the editor/viewer.
|
|
64
|
+
- `overlays`: Record of overlay layers. Keys are layer ids used by the editor/viewer.
|
|
65
|
+
|
|
66
|
+
Each layer entry:
|
|
67
|
+
|
|
68
|
+
- `mask`: Path to a PNG mask. The viewer converts the max RGB channel into alpha.
|
|
69
|
+
- `rgb`: `[r, g, b]` integers (0-255) used to colorize the legend composite.
|
|
70
|
+
- `label`: Optional display name in the editor UI.
|
|
71
|
+
|
|
72
|
+
## locations.json (optional)
|
|
73
|
+
|
|
74
|
+
Array of location markers. Coordinates are expressed in legend pixel space.
|
|
75
|
+
|
|
76
|
+
Schema: `schemas/locations.schema.json`
|
|
77
|
+
|
|
78
|
+
```json
|
|
79
|
+
[
|
|
80
|
+
{
|
|
81
|
+
"id": "loc-123",
|
|
82
|
+
"name": "Castle",
|
|
83
|
+
"icon": "icons/icon_castle.png",
|
|
84
|
+
"description": "Seat of power.",
|
|
85
|
+
"pixel": { "x": 514, "y": 728 },
|
|
86
|
+
"showBorder": true,
|
|
87
|
+
"view": {
|
|
88
|
+
"distance": 1.82,
|
|
89
|
+
"polar": 0.96,
|
|
90
|
+
"azimuth": 1.87,
|
|
91
|
+
"targetPixel": { "x": 514, "y": 728 }
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
]
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Fields:
|
|
98
|
+
|
|
99
|
+
- `id`: Stable unique id string.
|
|
100
|
+
- `name`: Optional label for markers.
|
|
101
|
+
- `icon`: Optional icon source. If the value looks like a file path or image filename, the viewer
|
|
102
|
+
loads it as a texture; otherwise the first character is rendered as a glyph.
|
|
103
|
+
- `description`: Optional freeform text (ignored by the viewer, preserved by the editor).
|
|
104
|
+
- `pixel`: `{ x, y }` in legend pixel space. The viewer converts to UV/world coordinates.
|
|
105
|
+
- `showBorder`: Optional boolean for marker label border visibility.
|
|
106
|
+
- `view`: Optional camera view state:
|
|
107
|
+
- `distance`: Camera distance from target in world units.
|
|
108
|
+
- `polar`: Polar angle in radians.
|
|
109
|
+
- `azimuth`: Azimuthal angle in radians.
|
|
110
|
+
- `targetPixel`: Optional pixel coordinate to orbit around.
|
|
111
|
+
|
|
112
|
+
## theme.json (optional)
|
|
113
|
+
|
|
114
|
+
Overrides for the default terrain marker theme.
|
|
115
|
+
|
|
116
|
+
Schema: `schemas/theme.schema.json`
|
|
117
|
+
|
|
118
|
+
```json
|
|
119
|
+
{
|
|
120
|
+
"locationMarkers": {
|
|
121
|
+
"sprite": {
|
|
122
|
+
"fontFamily": "\"DM Sans\", sans-serif",
|
|
123
|
+
"fontWeight": "600",
|
|
124
|
+
"maxFontSize": 52,
|
|
125
|
+
"minFontSize": 22,
|
|
126
|
+
"paddingX": 20,
|
|
127
|
+
"paddingY": 10,
|
|
128
|
+
"borderRadius": 4,
|
|
129
|
+
"states": {
|
|
130
|
+
"default": {
|
|
131
|
+
"textColor": "#ffffff",
|
|
132
|
+
"backgroundColor": "rgba(8, 10, 18, 0.78)",
|
|
133
|
+
"borderColor": "rgba(255, 255, 255, 0.35)",
|
|
134
|
+
"borderThickness": 2,
|
|
135
|
+
"opacity": 0.85
|
|
136
|
+
},
|
|
137
|
+
"hover": { "backgroundColor": "#3c49af" },
|
|
138
|
+
"focus": { "backgroundColor": "#cb811a" }
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
"stem": {
|
|
142
|
+
"shape": "triangle",
|
|
143
|
+
"radius": 0.02,
|
|
144
|
+
"states": {
|
|
145
|
+
"default": { "color": "#d9c39c", "opacity": 0.2 },
|
|
146
|
+
"focus": { "color": "#c00c0c", "opacity": 0.75 }
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Notes:
|
|
154
|
+
|
|
155
|
+
- `theme.json` is a partial override; any missing values fall back to the default theme.
|
|
156
|
+
- `stem.shape` supports: `cylinder`, `triangle`, `square`, `pentagon`, `hexagon`.
|
|
157
|
+
|
|
158
|
+
## metadata.json (optional)
|
|
159
|
+
|
|
160
|
+
Basic project metadata stored by the editor.
|
|
161
|
+
|
|
162
|
+
Schema: `schemas/metadata.schema.json`
|
|
163
|
+
|
|
164
|
+
```json
|
|
165
|
+
{
|
|
166
|
+
"label": "wynnal-terrain.wyn",
|
|
167
|
+
"author": "J. Markavian",
|
|
168
|
+
"source": "archive"
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Fields:
|
|
173
|
+
|
|
174
|
+
- `label`: Display name for the project.
|
|
175
|
+
- `author`: Optional author string.
|
|
176
|
+
- `source`: `archive` or `scratch`.
|
|
177
|
+
|
|
178
|
+
## Assets and Conventions
|
|
179
|
+
|
|
180
|
+
- Use PNGs for all image assets. The loader infers mime types by file extension.
|
|
181
|
+
- All referenced assets must exist inside the archive.
|
|
182
|
+
- The editor stores arbitrary files referenced by the JSON in the ZIP (see `buildWynArchive`).
|
|
183
|
+
- `legend.size` should match the pixel dimensions of mask assets.
|
|
184
|
+
|
|
185
|
+
## Example Archive
|
|
186
|
+
|
|
187
|
+
```
|
|
188
|
+
legend.json
|
|
189
|
+
locations.json
|
|
190
|
+
theme.json
|
|
191
|
+
metadata.json
|
|
192
|
+
layers/heightmap.png
|
|
193
|
+
layers/topology.png
|
|
194
|
+
layers/forest_mask.png
|
|
195
|
+
layers/water_mask.png
|
|
196
|
+
icons/icon_castle.png
|
|
197
|
+
thumbnails/thumbnail.png
|
|
198
|
+
```
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://connected-web.github.io/terrain-editor/schemas/legend.schema.json",
|
|
4
|
+
"title": "WYN Legend",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": false,
|
|
7
|
+
"required": ["size", "heightmap", "biomes", "overlays"],
|
|
8
|
+
"properties": {
|
|
9
|
+
"size": {
|
|
10
|
+
"type": "array",
|
|
11
|
+
"minItems": 2,
|
|
12
|
+
"maxItems": 2,
|
|
13
|
+
"items": {
|
|
14
|
+
"type": "integer",
|
|
15
|
+
"minimum": 1
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"sea_level": {
|
|
19
|
+
"type": "number",
|
|
20
|
+
"minimum": 0,
|
|
21
|
+
"maximum": 1
|
|
22
|
+
},
|
|
23
|
+
"heightmap": {
|
|
24
|
+
"type": "string",
|
|
25
|
+
"minLength": 1
|
|
26
|
+
},
|
|
27
|
+
"topology": {
|
|
28
|
+
"type": "string",
|
|
29
|
+
"minLength": 1
|
|
30
|
+
},
|
|
31
|
+
"biomes": {
|
|
32
|
+
"type": "object",
|
|
33
|
+
"additionalProperties": { "$ref": "#/definitions/legendLayer" }
|
|
34
|
+
},
|
|
35
|
+
"overlays": {
|
|
36
|
+
"type": "object",
|
|
37
|
+
"additionalProperties": { "$ref": "#/definitions/legendLayer" }
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"definitions": {
|
|
41
|
+
"legendLayer": {
|
|
42
|
+
"type": "object",
|
|
43
|
+
"additionalProperties": false,
|
|
44
|
+
"required": ["mask", "rgb"],
|
|
45
|
+
"properties": {
|
|
46
|
+
"mask": {
|
|
47
|
+
"type": "string",
|
|
48
|
+
"minLength": 1
|
|
49
|
+
},
|
|
50
|
+
"rgb": {
|
|
51
|
+
"type": "array",
|
|
52
|
+
"minItems": 3,
|
|
53
|
+
"maxItems": 3,
|
|
54
|
+
"items": {
|
|
55
|
+
"type": "integer",
|
|
56
|
+
"minimum": 0,
|
|
57
|
+
"maximum": 255
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
"label": {
|
|
61
|
+
"type": "string"
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://connected-web.github.io/terrain-editor/schemas/locations.schema.json",
|
|
4
|
+
"title": "WYN Locations",
|
|
5
|
+
"type": "array",
|
|
6
|
+
"items": { "$ref": "#/definitions/location" },
|
|
7
|
+
"definitions": {
|
|
8
|
+
"location": {
|
|
9
|
+
"type": "object",
|
|
10
|
+
"additionalProperties": false,
|
|
11
|
+
"required": ["id", "pixel"],
|
|
12
|
+
"properties": {
|
|
13
|
+
"id": { "type": "string", "minLength": 1 },
|
|
14
|
+
"name": { "type": "string" },
|
|
15
|
+
"icon": { "type": "string" },
|
|
16
|
+
"description": { "type": "string" },
|
|
17
|
+
"showBorder": { "type": "boolean" },
|
|
18
|
+
"pixel": { "$ref": "#/definitions/pixel" },
|
|
19
|
+
"uv": { "$ref": "#/definitions/uv" },
|
|
20
|
+
"world": { "$ref": "#/definitions/world" },
|
|
21
|
+
"view": { "$ref": "#/definitions/view" }
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"pixel": {
|
|
25
|
+
"type": "object",
|
|
26
|
+
"additionalProperties": false,
|
|
27
|
+
"required": ["x", "y"],
|
|
28
|
+
"properties": {
|
|
29
|
+
"x": { "type": "number" },
|
|
30
|
+
"y": { "type": "number" }
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"uv": {
|
|
34
|
+
"type": "object",
|
|
35
|
+
"additionalProperties": false,
|
|
36
|
+
"required": ["u", "v"],
|
|
37
|
+
"properties": {
|
|
38
|
+
"u": { "type": "number" },
|
|
39
|
+
"v": { "type": "number" }
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
"world": {
|
|
43
|
+
"type": "object",
|
|
44
|
+
"additionalProperties": false,
|
|
45
|
+
"required": ["x", "y", "z"],
|
|
46
|
+
"properties": {
|
|
47
|
+
"x": { "type": "number" },
|
|
48
|
+
"y": { "type": "number" },
|
|
49
|
+
"z": { "type": "number" }
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
"view": {
|
|
53
|
+
"type": "object",
|
|
54
|
+
"additionalProperties": false,
|
|
55
|
+
"required": ["distance", "polar", "azimuth"],
|
|
56
|
+
"properties": {
|
|
57
|
+
"distance": { "type": "number" },
|
|
58
|
+
"polar": { "type": "number" },
|
|
59
|
+
"azimuth": { "type": "number" },
|
|
60
|
+
"targetPixel": { "$ref": "#/definitions/pixel" }
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://connected-web.github.io/terrain-editor/schemas/metadata.schema.json",
|
|
4
|
+
"title": "WYN Metadata",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": false,
|
|
7
|
+
"properties": {
|
|
8
|
+
"label": { "type": "string" },
|
|
9
|
+
"author": { "type": "string" },
|
|
10
|
+
"source": { "type": "string", "enum": ["archive", "scratch"] }
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://connected-web.github.io/terrain-editor/schemas/theme.schema.json",
|
|
4
|
+
"title": "WYN Theme Overrides",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": false,
|
|
7
|
+
"properties": {
|
|
8
|
+
"locationMarkers": {
|
|
9
|
+
"type": "object",
|
|
10
|
+
"additionalProperties": false,
|
|
11
|
+
"properties": {
|
|
12
|
+
"sprite": { "$ref": "#/definitions/sprite" },
|
|
13
|
+
"stem": { "$ref": "#/definitions/stem" }
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"definitions": {
|
|
18
|
+
"sprite": {
|
|
19
|
+
"type": "object",
|
|
20
|
+
"additionalProperties": false,
|
|
21
|
+
"properties": {
|
|
22
|
+
"fontFamily": { "type": "string" },
|
|
23
|
+
"fontWeight": { "type": "string" },
|
|
24
|
+
"maxFontSize": { "type": "number" },
|
|
25
|
+
"minFontSize": { "type": "number" },
|
|
26
|
+
"paddingX": { "type": "number" },
|
|
27
|
+
"paddingY": { "type": "number" },
|
|
28
|
+
"borderRadius": { "type": "number" },
|
|
29
|
+
"states": { "$ref": "#/definitions/spriteStates" }
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"spriteStates": {
|
|
33
|
+
"type": "object",
|
|
34
|
+
"additionalProperties": false,
|
|
35
|
+
"properties": {
|
|
36
|
+
"default": { "$ref": "#/definitions/spriteState" },
|
|
37
|
+
"hover": { "$ref": "#/definitions/spriteState" },
|
|
38
|
+
"focus": { "$ref": "#/definitions/spriteState" }
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
"spriteState": {
|
|
42
|
+
"type": "object",
|
|
43
|
+
"additionalProperties": false,
|
|
44
|
+
"properties": {
|
|
45
|
+
"textColor": { "type": "string" },
|
|
46
|
+
"backgroundColor": { "type": "string" },
|
|
47
|
+
"borderColor": { "type": "string" },
|
|
48
|
+
"borderThickness": { "type": "number" },
|
|
49
|
+
"opacity": { "type": "number" }
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
"stem": {
|
|
53
|
+
"type": "object",
|
|
54
|
+
"additionalProperties": false,
|
|
55
|
+
"properties": {
|
|
56
|
+
"shape": {
|
|
57
|
+
"type": "string",
|
|
58
|
+
"enum": ["cylinder", "triangle", "square", "pentagon", "hexagon"]
|
|
59
|
+
},
|
|
60
|
+
"radius": { "type": "number" },
|
|
61
|
+
"scale": { "type": "number" },
|
|
62
|
+
"states": { "$ref": "#/definitions/stemStates" }
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
"stemStates": {
|
|
66
|
+
"type": "object",
|
|
67
|
+
"additionalProperties": false,
|
|
68
|
+
"properties": {
|
|
69
|
+
"default": { "$ref": "#/definitions/stemState" },
|
|
70
|
+
"hover": { "$ref": "#/definitions/stemState" },
|
|
71
|
+
"focus": { "$ref": "#/definitions/stemState" }
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
"stemState": {
|
|
75
|
+
"type": "object",
|
|
76
|
+
"additionalProperties": false,
|
|
77
|
+
"properties": {
|
|
78
|
+
"color": { "type": "string" },
|
|
79
|
+
"opacity": { "type": "number" }
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@connected-web/terrain-editor",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Reusable viewer/editor utilities for Wyn terrain files.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.cjs",
|
|
@@ -11,14 +11,17 @@
|
|
|
11
11
|
"types": "./dist/index.d.ts",
|
|
12
12
|
"import": "./dist/index.js",
|
|
13
13
|
"require": "./dist/index.cjs"
|
|
14
|
-
}
|
|
14
|
+
},
|
|
15
|
+
"./documentation/*": "./documentation/*",
|
|
16
|
+
"./schemas/*": "./documentation/schemas/*"
|
|
15
17
|
},
|
|
16
18
|
"files": [
|
|
17
19
|
"dist",
|
|
20
|
+
"documentation",
|
|
18
21
|
"README.md"
|
|
19
22
|
],
|
|
20
23
|
"scripts": {
|
|
21
|
-
"build": "tsup src/index.ts --dts --format cjs,esm --out-dir dist --clean --tsconfig tsconfig.json"
|
|
24
|
+
"build": "node ./scripts/copy-docs.mjs && tsup src/index.ts --dts --format cjs,esm --out-dir dist --clean --tsconfig tsconfig.json"
|
|
22
25
|
},
|
|
23
26
|
"dependencies": {
|
|
24
27
|
"jszip": "^3.10.1",
|
|
@@ -31,5 +34,9 @@
|
|
|
31
34
|
"three": "^0.181.1"
|
|
32
35
|
},
|
|
33
36
|
"license": "ISC",
|
|
34
|
-
"author": "Connected Web"
|
|
37
|
+
"author": "Connected Web",
|
|
38
|
+
"repository": {
|
|
39
|
+
"type": "git",
|
|
40
|
+
"url": "https://github.com/connected-web/terrain-editor.git"
|
|
41
|
+
}
|
|
35
42
|
}
|