@connected-web/terrain-editor 0.1.3 → 0.1.5
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 +78 -1
- package/dist/index.d.cts +33 -1
- package/dist/index.d.ts +33 -1
- package/dist/index.js +76 -1
- 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 +6 -3
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,
|
|
@@ -939,10 +941,16 @@ async function initTerrainViewer(container, dataset, options = {}) {
|
|
|
939
941
|
let viewportWidth = width;
|
|
940
942
|
let viewportHeight = height;
|
|
941
943
|
let viewOffsetPixels = 0;
|
|
944
|
+
const maxPixelRatio = Math.max(1, options.maxPixelRatio ?? 1.5);
|
|
945
|
+
let currentPixelRatio = 1;
|
|
946
|
+
function resolvePixelRatio() {
|
|
947
|
+
return Math.max(1, Math.min(window.devicePixelRatio || 1, maxPixelRatio));
|
|
948
|
+
}
|
|
942
949
|
const renderer = new THREE2.WebGLRenderer({ antialias: true, alpha: true });
|
|
943
950
|
renderer.toneMapping = THREE2.ACESFilmicToneMapping;
|
|
944
951
|
renderer.toneMappingExposure = 1.08;
|
|
945
|
-
|
|
952
|
+
currentPixelRatio = resolvePixelRatio();
|
|
953
|
+
renderer.setPixelRatio(currentPixelRatio);
|
|
946
954
|
const hostStyle = window.getComputedStyle(container);
|
|
947
955
|
if (hostStyle.position === "static") {
|
|
948
956
|
container.style.position = "relative";
|
|
@@ -1469,6 +1477,11 @@ async function initTerrainViewer(container, dataset, options = {}) {
|
|
|
1469
1477
|
viewOffsetPixels = shiftTarget;
|
|
1470
1478
|
camera.aspect = clientWidth / clientHeight;
|
|
1471
1479
|
camera.updateProjectionMatrix();
|
|
1480
|
+
const nextPixelRatio = resolvePixelRatio();
|
|
1481
|
+
if (nextPixelRatio !== currentPixelRatio) {
|
|
1482
|
+
currentPixelRatio = nextPixelRatio;
|
|
1483
|
+
renderer.setPixelRatio(currentPixelRatio);
|
|
1484
|
+
}
|
|
1472
1485
|
renderer.setSize(clientWidth, clientHeight, false);
|
|
1473
1486
|
applyViewOffset();
|
|
1474
1487
|
});
|
|
@@ -1495,6 +1508,8 @@ async function initTerrainViewer(container, dataset, options = {}) {
|
|
|
1495
1508
|
const markerId = pickMarkerId();
|
|
1496
1509
|
if (markerId !== hoveredLocationId) {
|
|
1497
1510
|
hoveredLocationId = markerId;
|
|
1511
|
+
const hoveredLocation = markerId ? currentLocations.find((location) => location.id === markerId) : void 0;
|
|
1512
|
+
renderer.domElement.title = hoveredLocation?.name || hoveredLocation?.id || "";
|
|
1498
1513
|
options.onLocationHover?.(markerId);
|
|
1499
1514
|
updateMarkerVisuals();
|
|
1500
1515
|
}
|
|
@@ -1655,6 +1670,9 @@ async function initTerrainViewer(container, dataset, options = {}) {
|
|
|
1655
1670
|
},
|
|
1656
1671
|
setCameraOffset: (offset, focusId) => {
|
|
1657
1672
|
cameraOffset.target = THREE2.MathUtils.clamp(offset, -0.45, 0.45);
|
|
1673
|
+
if (cameraTween) {
|
|
1674
|
+
return;
|
|
1675
|
+
}
|
|
1658
1676
|
const targetId = focusId ?? currentFocusId;
|
|
1659
1677
|
if (targetId) {
|
|
1660
1678
|
const loc = currentLocations.find((item) => item.id === targetId);
|
|
@@ -3244,17 +3262,76 @@ function createMaskEditor(options) {
|
|
|
3244
3262
|
markClean
|
|
3245
3263
|
};
|
|
3246
3264
|
}
|
|
3265
|
+
|
|
3266
|
+
// src/iconHelpers.ts
|
|
3267
|
+
function inferMimeType(path, fallback = "image/png") {
|
|
3268
|
+
const extension = path.split(".").pop()?.toLowerCase();
|
|
3269
|
+
if (!extension) return fallback;
|
|
3270
|
+
if (extension === "png") return "image/png";
|
|
3271
|
+
if (extension === "jpg" || extension === "jpeg") return "image/jpeg";
|
|
3272
|
+
if (extension === "webp") return "image/webp";
|
|
3273
|
+
if (extension === "gif") return "image/gif";
|
|
3274
|
+
if (extension === "svg") return "image/svg+xml";
|
|
3275
|
+
return fallback;
|
|
3276
|
+
}
|
|
3277
|
+
function toObjectUrl(entry) {
|
|
3278
|
+
const type = entry.type ?? inferMimeType(entry.path);
|
|
3279
|
+
const blob = new Blob([entry.data], { type });
|
|
3280
|
+
return URL.createObjectURL(blob);
|
|
3281
|
+
}
|
|
3282
|
+
function createIconUrlMap(files, options = {}) {
|
|
3283
|
+
const urls = /* @__PURE__ */ new Map();
|
|
3284
|
+
const prefix = options.prefix ?? "icons/";
|
|
3285
|
+
if (!files?.length) {
|
|
3286
|
+
return {
|
|
3287
|
+
urls,
|
|
3288
|
+
cleanup: () => void 0
|
|
3289
|
+
};
|
|
3290
|
+
}
|
|
3291
|
+
for (const entry of files) {
|
|
3292
|
+
if (!entry.path) continue;
|
|
3293
|
+
if (prefix && !entry.path.startsWith(prefix)) continue;
|
|
3294
|
+
const url = toObjectUrl(entry);
|
|
3295
|
+
urls.set(entry.path, url);
|
|
3296
|
+
}
|
|
3297
|
+
const cleanup = () => {
|
|
3298
|
+
for (const url of urls.values()) {
|
|
3299
|
+
URL.revokeObjectURL(url);
|
|
3300
|
+
}
|
|
3301
|
+
urls.clear();
|
|
3302
|
+
};
|
|
3303
|
+
return { urls, cleanup };
|
|
3304
|
+
}
|
|
3305
|
+
function enhanceLocationsWithIconUrls(locations, files, options) {
|
|
3306
|
+
const { urls, cleanup } = createIconUrlMap(files, options);
|
|
3307
|
+
const enhanced = locations.map((location) => {
|
|
3308
|
+
if (!location.icon) {
|
|
3309
|
+
return { ...location };
|
|
3310
|
+
}
|
|
3311
|
+
const iconUrl = urls.get(location.icon);
|
|
3312
|
+
if (!iconUrl) {
|
|
3313
|
+
return { ...location };
|
|
3314
|
+
}
|
|
3315
|
+
return {
|
|
3316
|
+
...location,
|
|
3317
|
+
iconUrl
|
|
3318
|
+
};
|
|
3319
|
+
});
|
|
3320
|
+
return { locations: enhanced, cleanup };
|
|
3321
|
+
}
|
|
3247
3322
|
// Annotate the CommonJS export names for ESM import in node:
|
|
3248
3323
|
0 && (module.exports = {
|
|
3249
3324
|
applyHeightField,
|
|
3250
3325
|
buildRimMesh,
|
|
3251
3326
|
buildWynArchive,
|
|
3252
3327
|
createHeightSampler,
|
|
3328
|
+
createIconUrlMap,
|
|
3253
3329
|
createLayerBrowserStore,
|
|
3254
3330
|
createMaskEditor,
|
|
3255
3331
|
createProjectStore,
|
|
3256
3332
|
createTerrainViewerHost,
|
|
3257
3333
|
createViewerOverlay,
|
|
3334
|
+
enhanceLocationsWithIconUrls,
|
|
3258
3335
|
getDefaultTerrainTheme,
|
|
3259
3336
|
initTerrainViewer,
|
|
3260
3337
|
loadWynArchive,
|
package/dist/index.d.cts
CHANGED
|
@@ -82,6 +82,7 @@ type TerrainLocation = {
|
|
|
82
82
|
id: string;
|
|
83
83
|
name?: string;
|
|
84
84
|
icon?: string;
|
|
85
|
+
description?: string;
|
|
85
86
|
showBorder?: boolean;
|
|
86
87
|
pixel: {
|
|
87
88
|
x: number;
|
|
@@ -119,6 +120,7 @@ type TerrainInitOptions = {
|
|
|
119
120
|
onLifecycleChange?: (state: ViewerLifecycleState) => void;
|
|
120
121
|
heightScale?: number;
|
|
121
122
|
waterLevelPercent?: number;
|
|
123
|
+
maxPixelRatio?: number;
|
|
122
124
|
layers?: LayerToggleState;
|
|
123
125
|
interactive?: boolean;
|
|
124
126
|
onLocationPick?: (payload: LocationPickPayload) => void;
|
|
@@ -406,4 +408,34 @@ type MaskEditorOptions = {
|
|
|
406
408
|
};
|
|
407
409
|
declare function createMaskEditor(options: MaskEditorOptions): MaskEditorHandle;
|
|
408
410
|
|
|
409
|
-
|
|
411
|
+
type CreateIconUrlMapOptions = {
|
|
412
|
+
/**
|
|
413
|
+
* Directory prefix to match when building the icon map.
|
|
414
|
+
* Defaults to `icons/`, matching standard .wyn archives.
|
|
415
|
+
*/
|
|
416
|
+
prefix?: string;
|
|
417
|
+
};
|
|
418
|
+
type IconUrlEntry = {
|
|
419
|
+
path: string;
|
|
420
|
+
url: string;
|
|
421
|
+
};
|
|
422
|
+
type IconUrlMapResult = {
|
|
423
|
+
/**
|
|
424
|
+
* Map of file paths (relative to the archive root) to object URLs.
|
|
425
|
+
*/
|
|
426
|
+
urls: Map<string, string>;
|
|
427
|
+
/**
|
|
428
|
+
* Revoke all allocated object URLs.
|
|
429
|
+
*/
|
|
430
|
+
cleanup: () => void;
|
|
431
|
+
};
|
|
432
|
+
type EnhancedLocationsResult<T extends TerrainLocation = TerrainLocation> = {
|
|
433
|
+
locations: Array<T & {
|
|
434
|
+
iconUrl?: string;
|
|
435
|
+
}>;
|
|
436
|
+
cleanup: () => void;
|
|
437
|
+
};
|
|
438
|
+
declare function createIconUrlMap(files?: TerrainProjectFileEntry[], options?: CreateIconUrlMapOptions): IconUrlMapResult;
|
|
439
|
+
declare function enhanceLocationsWithIconUrls<T extends TerrainLocation = TerrainLocation>(locations: T[], files?: TerrainProjectFileEntry[], options?: CreateIconUrlMapOptions): EnhancedLocationsResult<T>;
|
|
440
|
+
|
|
441
|
+
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
|
@@ -82,6 +82,7 @@ type TerrainLocation = {
|
|
|
82
82
|
id: string;
|
|
83
83
|
name?: string;
|
|
84
84
|
icon?: string;
|
|
85
|
+
description?: string;
|
|
85
86
|
showBorder?: boolean;
|
|
86
87
|
pixel: {
|
|
87
88
|
x: number;
|
|
@@ -119,6 +120,7 @@ type TerrainInitOptions = {
|
|
|
119
120
|
onLifecycleChange?: (state: ViewerLifecycleState) => void;
|
|
120
121
|
heightScale?: number;
|
|
121
122
|
waterLevelPercent?: number;
|
|
123
|
+
maxPixelRatio?: number;
|
|
122
124
|
layers?: LayerToggleState;
|
|
123
125
|
interactive?: boolean;
|
|
124
126
|
onLocationPick?: (payload: LocationPickPayload) => void;
|
|
@@ -406,4 +408,34 @@ type MaskEditorOptions = {
|
|
|
406
408
|
};
|
|
407
409
|
declare function createMaskEditor(options: MaskEditorOptions): MaskEditorHandle;
|
|
408
410
|
|
|
409
|
-
|
|
411
|
+
type CreateIconUrlMapOptions = {
|
|
412
|
+
/**
|
|
413
|
+
* Directory prefix to match when building the icon map.
|
|
414
|
+
* Defaults to `icons/`, matching standard .wyn archives.
|
|
415
|
+
*/
|
|
416
|
+
prefix?: string;
|
|
417
|
+
};
|
|
418
|
+
type IconUrlEntry = {
|
|
419
|
+
path: string;
|
|
420
|
+
url: string;
|
|
421
|
+
};
|
|
422
|
+
type IconUrlMapResult = {
|
|
423
|
+
/**
|
|
424
|
+
* Map of file paths (relative to the archive root) to object URLs.
|
|
425
|
+
*/
|
|
426
|
+
urls: Map<string, string>;
|
|
427
|
+
/**
|
|
428
|
+
* Revoke all allocated object URLs.
|
|
429
|
+
*/
|
|
430
|
+
cleanup: () => void;
|
|
431
|
+
};
|
|
432
|
+
type EnhancedLocationsResult<T extends TerrainLocation = TerrainLocation> = {
|
|
433
|
+
locations: Array<T & {
|
|
434
|
+
iconUrl?: string;
|
|
435
|
+
}>;
|
|
436
|
+
cleanup: () => void;
|
|
437
|
+
};
|
|
438
|
+
declare function createIconUrlMap(files?: TerrainProjectFileEntry[], options?: CreateIconUrlMapOptions): IconUrlMapResult;
|
|
439
|
+
declare function enhanceLocationsWithIconUrls<T extends TerrainLocation = TerrainLocation>(locations: T[], files?: TerrainProjectFileEntry[], options?: CreateIconUrlMapOptions): EnhancedLocationsResult<T>;
|
|
440
|
+
|
|
441
|
+
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
|
@@ -888,10 +888,16 @@ async function initTerrainViewer(container, dataset, options = {}) {
|
|
|
888
888
|
let viewportWidth = width;
|
|
889
889
|
let viewportHeight = height;
|
|
890
890
|
let viewOffsetPixels = 0;
|
|
891
|
+
const maxPixelRatio = Math.max(1, options.maxPixelRatio ?? 1.5);
|
|
892
|
+
let currentPixelRatio = 1;
|
|
893
|
+
function resolvePixelRatio() {
|
|
894
|
+
return Math.max(1, Math.min(window.devicePixelRatio || 1, maxPixelRatio));
|
|
895
|
+
}
|
|
891
896
|
const renderer = new THREE2.WebGLRenderer({ antialias: true, alpha: true });
|
|
892
897
|
renderer.toneMapping = THREE2.ACESFilmicToneMapping;
|
|
893
898
|
renderer.toneMappingExposure = 1.08;
|
|
894
|
-
|
|
899
|
+
currentPixelRatio = resolvePixelRatio();
|
|
900
|
+
renderer.setPixelRatio(currentPixelRatio);
|
|
895
901
|
const hostStyle = window.getComputedStyle(container);
|
|
896
902
|
if (hostStyle.position === "static") {
|
|
897
903
|
container.style.position = "relative";
|
|
@@ -1418,6 +1424,11 @@ async function initTerrainViewer(container, dataset, options = {}) {
|
|
|
1418
1424
|
viewOffsetPixels = shiftTarget;
|
|
1419
1425
|
camera.aspect = clientWidth / clientHeight;
|
|
1420
1426
|
camera.updateProjectionMatrix();
|
|
1427
|
+
const nextPixelRatio = resolvePixelRatio();
|
|
1428
|
+
if (nextPixelRatio !== currentPixelRatio) {
|
|
1429
|
+
currentPixelRatio = nextPixelRatio;
|
|
1430
|
+
renderer.setPixelRatio(currentPixelRatio);
|
|
1431
|
+
}
|
|
1421
1432
|
renderer.setSize(clientWidth, clientHeight, false);
|
|
1422
1433
|
applyViewOffset();
|
|
1423
1434
|
});
|
|
@@ -1444,6 +1455,8 @@ async function initTerrainViewer(container, dataset, options = {}) {
|
|
|
1444
1455
|
const markerId = pickMarkerId();
|
|
1445
1456
|
if (markerId !== hoveredLocationId) {
|
|
1446
1457
|
hoveredLocationId = markerId;
|
|
1458
|
+
const hoveredLocation = markerId ? currentLocations.find((location) => location.id === markerId) : void 0;
|
|
1459
|
+
renderer.domElement.title = hoveredLocation?.name || hoveredLocation?.id || "";
|
|
1447
1460
|
options.onLocationHover?.(markerId);
|
|
1448
1461
|
updateMarkerVisuals();
|
|
1449
1462
|
}
|
|
@@ -1604,6 +1617,9 @@ async function initTerrainViewer(container, dataset, options = {}) {
|
|
|
1604
1617
|
},
|
|
1605
1618
|
setCameraOffset: (offset, focusId) => {
|
|
1606
1619
|
cameraOffset.target = THREE2.MathUtils.clamp(offset, -0.45, 0.45);
|
|
1620
|
+
if (cameraTween) {
|
|
1621
|
+
return;
|
|
1622
|
+
}
|
|
1607
1623
|
const targetId = focusId ?? currentFocusId;
|
|
1608
1624
|
if (targetId) {
|
|
1609
1625
|
const loc = currentLocations.find((item) => item.id === targetId);
|
|
@@ -3193,16 +3209,75 @@ function createMaskEditor(options) {
|
|
|
3193
3209
|
markClean
|
|
3194
3210
|
};
|
|
3195
3211
|
}
|
|
3212
|
+
|
|
3213
|
+
// src/iconHelpers.ts
|
|
3214
|
+
function inferMimeType(path, fallback = "image/png") {
|
|
3215
|
+
const extension = path.split(".").pop()?.toLowerCase();
|
|
3216
|
+
if (!extension) return fallback;
|
|
3217
|
+
if (extension === "png") return "image/png";
|
|
3218
|
+
if (extension === "jpg" || extension === "jpeg") return "image/jpeg";
|
|
3219
|
+
if (extension === "webp") return "image/webp";
|
|
3220
|
+
if (extension === "gif") return "image/gif";
|
|
3221
|
+
if (extension === "svg") return "image/svg+xml";
|
|
3222
|
+
return fallback;
|
|
3223
|
+
}
|
|
3224
|
+
function toObjectUrl(entry) {
|
|
3225
|
+
const type = entry.type ?? inferMimeType(entry.path);
|
|
3226
|
+
const blob = new Blob([entry.data], { type });
|
|
3227
|
+
return URL.createObjectURL(blob);
|
|
3228
|
+
}
|
|
3229
|
+
function createIconUrlMap(files, options = {}) {
|
|
3230
|
+
const urls = /* @__PURE__ */ new Map();
|
|
3231
|
+
const prefix = options.prefix ?? "icons/";
|
|
3232
|
+
if (!files?.length) {
|
|
3233
|
+
return {
|
|
3234
|
+
urls,
|
|
3235
|
+
cleanup: () => void 0
|
|
3236
|
+
};
|
|
3237
|
+
}
|
|
3238
|
+
for (const entry of files) {
|
|
3239
|
+
if (!entry.path) continue;
|
|
3240
|
+
if (prefix && !entry.path.startsWith(prefix)) continue;
|
|
3241
|
+
const url = toObjectUrl(entry);
|
|
3242
|
+
urls.set(entry.path, url);
|
|
3243
|
+
}
|
|
3244
|
+
const cleanup = () => {
|
|
3245
|
+
for (const url of urls.values()) {
|
|
3246
|
+
URL.revokeObjectURL(url);
|
|
3247
|
+
}
|
|
3248
|
+
urls.clear();
|
|
3249
|
+
};
|
|
3250
|
+
return { urls, cleanup };
|
|
3251
|
+
}
|
|
3252
|
+
function enhanceLocationsWithIconUrls(locations, files, options) {
|
|
3253
|
+
const { urls, cleanup } = createIconUrlMap(files, options);
|
|
3254
|
+
const enhanced = locations.map((location) => {
|
|
3255
|
+
if (!location.icon) {
|
|
3256
|
+
return { ...location };
|
|
3257
|
+
}
|
|
3258
|
+
const iconUrl = urls.get(location.icon);
|
|
3259
|
+
if (!iconUrl) {
|
|
3260
|
+
return { ...location };
|
|
3261
|
+
}
|
|
3262
|
+
return {
|
|
3263
|
+
...location,
|
|
3264
|
+
iconUrl
|
|
3265
|
+
};
|
|
3266
|
+
});
|
|
3267
|
+
return { locations: enhanced, cleanup };
|
|
3268
|
+
}
|
|
3196
3269
|
export {
|
|
3197
3270
|
applyHeightField,
|
|
3198
3271
|
buildRimMesh,
|
|
3199
3272
|
buildWynArchive,
|
|
3200
3273
|
createHeightSampler,
|
|
3274
|
+
createIconUrlMap,
|
|
3201
3275
|
createLayerBrowserStore,
|
|
3202
3276
|
createMaskEditor,
|
|
3203
3277
|
createProjectStore,
|
|
3204
3278
|
createTerrainViewerHost,
|
|
3205
3279
|
createViewerOverlay,
|
|
3280
|
+
enhanceLocationsWithIconUrls,
|
|
3206
3281
|
getDefaultTerrainTheme,
|
|
3207
3282
|
initTerrainViewer,
|
|
3208
3283
|
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.5",
|
|
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",
|