@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 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
- renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
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
- export { type BuildWynArchiveOptions, type Cleanup, type DeepPartial, type HeightSampler, 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, createLayerBrowserStore, createMaskEditor, createProjectStore, createTerrainViewerHost, createViewerOverlay, getDefaultTerrainTheme, initTerrainViewer, loadWynArchive, loadWynArchiveFromArrayBuffer, loadWynArchiveFromFile, resolveTerrainTheme, sampleHeightValue };
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
- export { type BuildWynArchiveOptions, type Cleanup, type DeepPartial, type HeightSampler, 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, createLayerBrowserStore, createMaskEditor, createProjectStore, createTerrainViewerHost, createViewerOverlay, getDefaultTerrainTheme, initTerrainViewer, loadWynArchive, loadWynArchiveFromArrayBuffer, loadWynArchiveFromFile, resolveTerrainTheme, sampleHeightValue };
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
- renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
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",
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",