@rpgjs/map-builder 0.0.0-stage → 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +19 -0
- package/README.md +341 -2
- package/dist/authority/authority.d.ts +118 -0
- package/dist/authority/authority.d.ts.map +1 -0
- package/dist/authority/authority.js +289 -0
- package/dist/authority/authority.js.map +1 -0
- package/dist/authority/index.d.ts +3 -0
- package/dist/authority/index.d.ts.map +1 -0
- package/dist/authority/index.js +3 -0
- package/dist/authority/store.d.ts +36 -0
- package/dist/authority/store.d.ts.map +1 -0
- package/dist/authority/store.js +35 -0
- package/dist/authority/store.js.map +1 -0
- package/dist/client/index.d.ts +72 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +200 -0
- package/dist/client/index.js.map +1 -0
- package/dist/embed/client.d.ts +85 -0
- package/dist/embed/client.d.ts.map +1 -0
- package/dist/embed/client.js +255 -0
- package/dist/embed/client.js.map +1 -0
- package/dist/embed/host.d.ts +112 -0
- package/dist/embed/host.d.ts.map +1 -0
- package/dist/embed/host.js +278 -0
- package/dist/embed/host.js.map +1 -0
- package/dist/embed/index.d.ts +4 -0
- package/dist/embed/index.d.ts.map +1 -0
- package/dist/embed/index.js +4 -0
- package/dist/embed/protocol.d.ts +137 -0
- package/dist/embed/protocol.d.ts.map +1 -0
- package/dist/embed/protocol.js +161 -0
- package/dist/embed/protocol.js.map +1 -0
- package/dist/features/index.d.ts +66 -0
- package/dist/features/index.d.ts.map +1 -0
- package/dist/features/index.js +157 -0
- package/dist/features/index.js.map +1 -0
- package/dist/features/skin.d.ts +9 -0
- package/dist/features/skin.d.ts.map +1 -0
- package/dist/features/skin.js +43 -0
- package/dist/features/skin.js.map +1 -0
- package/dist/features/terrain.d.ts +66 -0
- package/dist/features/terrain.d.ts.map +1 -0
- package/dist/features/terrain.js +291 -0
- package/dist/features/terrain.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +51 -0
- package/dist/interaction/builtin-intents.d.ts +9 -0
- package/dist/interaction/builtin-intents.d.ts.map +1 -0
- package/dist/interaction/builtin-intents.js +61 -0
- package/dist/interaction/builtin-intents.js.map +1 -0
- package/dist/interaction/catalog.d.ts +24 -0
- package/dist/interaction/catalog.d.ts.map +1 -0
- package/dist/interaction/catalog.js +20 -0
- package/dist/interaction/catalog.js.map +1 -0
- package/dist/interaction/controller.d.ts +137 -0
- package/dist/interaction/controller.d.ts.map +1 -0
- package/dist/interaction/controller.js +215 -0
- package/dist/interaction/controller.js.map +1 -0
- package/dist/interaction/index.d.ts +14 -0
- package/dist/interaction/index.d.ts.map +1 -0
- package/dist/interaction/index.js +13 -0
- package/dist/interaction/input.d.ts +26 -0
- package/dist/interaction/input.d.ts.map +1 -0
- package/dist/interaction/input.js +33 -0
- package/dist/interaction/input.js.map +1 -0
- package/dist/interaction/intents.d.ts +80 -0
- package/dist/interaction/intents.d.ts.map +1 -0
- package/dist/interaction/keyboard.d.ts +26 -0
- package/dist/interaction/keyboard.d.ts.map +1 -0
- package/dist/interaction/keyboard.js +32 -0
- package/dist/interaction/keyboard.js.map +1 -0
- package/dist/interaction/picking.d.ts +33 -0
- package/dist/interaction/picking.d.ts.map +1 -0
- package/dist/interaction/picking.js +35 -0
- package/dist/interaction/picking.js.map +1 -0
- package/dist/interaction/pipeline.d.ts +45 -0
- package/dist/interaction/pipeline.d.ts.map +1 -0
- package/dist/interaction/pipeline.js +26 -0
- package/dist/interaction/pipeline.js.map +1 -0
- package/dist/interaction/placement.d.ts +76 -0
- package/dist/interaction/placement.d.ts.map +1 -0
- package/dist/interaction/placement.js +238 -0
- package/dist/interaction/placement.js.map +1 -0
- package/dist/interaction/selection-tool.d.ts +29 -0
- package/dist/interaction/selection-tool.d.ts.map +1 -0
- package/dist/interaction/selection-tool.js +190 -0
- package/dist/interaction/selection-tool.js.map +1 -0
- package/dist/interaction/steps.d.ts +62 -0
- package/dist/interaction/steps.d.ts.map +1 -0
- package/dist/interaction/steps.js +126 -0
- package/dist/interaction/steps.js.map +1 -0
- package/dist/interaction/store.d.ts +10 -0
- package/dist/interaction/store.d.ts.map +1 -0
- package/dist/interaction/store.js +28 -0
- package/dist/interaction/store.js.map +1 -0
- package/dist/interaction/tools.d.ts +26 -0
- package/dist/interaction/tools.d.ts.map +1 -0
- package/dist/interaction/tools.js +41 -0
- package/dist/interaction/tools.js.map +1 -0
- package/dist/model/assets.d.ts +36 -0
- package/dist/model/assets.d.ts.map +1 -0
- package/dist/model/assets.js +18 -0
- package/dist/model/assets.js.map +1 -0
- package/dist/model/committer.d.ts +66 -0
- package/dist/model/committer.d.ts.map +1 -0
- package/dist/model/committer.js +38 -0
- package/dist/model/committer.js.map +1 -0
- package/dist/model/core-patches.d.ts +2 -0
- package/dist/model/core-patches.d.ts.map +1 -0
- package/dist/model/core-patches.js +193 -0
- package/dist/model/core-patches.js.map +1 -0
- package/dist/model/define-map.d.ts +52 -0
- package/dist/model/define-map.d.ts.map +1 -0
- package/dist/model/define-map.js +66 -0
- package/dist/model/define-map.js.map +1 -0
- package/dist/model/definition.d.ts +57 -0
- package/dist/model/definition.d.ts.map +1 -0
- package/dist/model/definition.js +6 -0
- package/dist/model/definition.js.map +1 -0
- package/dist/model/errors.d.ts +21 -0
- package/dist/model/errors.d.ts.map +1 -0
- package/dist/model/errors.js +14 -0
- package/dist/model/errors.js.map +1 -0
- package/dist/model/history.d.ts +43 -0
- package/dist/model/history.d.ts.map +1 -0
- package/dist/model/history.js +141 -0
- package/dist/model/history.js.map +1 -0
- package/dist/model/index.d.ts +14 -0
- package/dist/model/index.d.ts.map +1 -0
- package/dist/model/index.js +14 -0
- package/dist/model/json.d.ts +15 -0
- package/dist/model/json.d.ts.map +1 -0
- package/dist/model/json.js +35 -0
- package/dist/model/json.js.map +1 -0
- package/dist/model/model.d.ts +68 -0
- package/dist/model/model.d.ts.map +1 -0
- package/dist/model/model.js +170 -0
- package/dist/model/model.js.map +1 -0
- package/dist/model/parse.d.ts +24 -0
- package/dist/model/parse.d.ts.map +1 -0
- package/dist/model/parse.js +165 -0
- package/dist/model/parse.js.map +1 -0
- package/dist/model/patches.d.ts +100 -0
- package/dist/model/patches.d.ts.map +1 -0
- package/dist/model/patches.js +21 -0
- package/dist/model/patches.js.map +1 -0
- package/dist/model/result.d.ts +22 -0
- package/dist/model/result.d.ts.map +1 -0
- package/dist/model/result.js +10 -0
- package/dist/model/result.js.map +1 -0
- package/dist/model/source.d.ts +52 -0
- package/dist/model/source.d.ts.map +1 -0
- package/dist/model/source.js +75 -0
- package/dist/model/source.js.map +1 -0
- package/dist/protocol/index.d.ts +92 -0
- package/dist/protocol/index.d.ts.map +1 -0
- package/dist/protocol/index.js +106 -0
- package/dist/protocol/index.js.map +1 -0
- package/dist/remote/committer.d.ts +43 -0
- package/dist/remote/committer.d.ts.map +1 -0
- package/dist/remote/committer.js +206 -0
- package/dist/remote/committer.js.map +1 -0
- package/dist/remote/index.d.ts +4 -0
- package/dist/remote/index.d.ts.map +1 -0
- package/dist/remote/index.js +4 -0
- package/dist/remote/join.d.ts +30 -0
- package/dist/remote/join.d.ts.map +1 -0
- package/dist/remote/join.js +36 -0
- package/dist/remote/join.js.map +1 -0
- package/dist/remote/memory-transport.d.ts +20 -0
- package/dist/remote/memory-transport.d.ts.map +1 -0
- package/dist/remote/memory-transport.js +48 -0
- package/dist/remote/memory-transport.js.map +1 -0
- package/dist/renderer/effects.d.ts +52 -0
- package/dist/renderer/effects.d.ts.map +1 -0
- package/dist/renderer/effects.js +111 -0
- package/dist/renderer/effects.js.map +1 -0
- package/dist/renderer/feature.d.ts +79 -0
- package/dist/renderer/feature.d.ts.map +1 -0
- package/dist/renderer/feature.js +50 -0
- package/dist/renderer/feature.js.map +1 -0
- package/dist/renderer/geometry.d.ts +59 -0
- package/dist/renderer/geometry.d.ts.map +1 -0
- package/dist/renderer/geometry.js +114 -0
- package/dist/renderer/geometry.js.map +1 -0
- package/dist/renderer/index.d.ts +7 -0
- package/dist/renderer/index.d.ts.map +1 -0
- package/dist/renderer/index.js +7 -0
- package/dist/renderer/scene.d.ts +77 -0
- package/dist/renderer/scene.d.ts.map +1 -0
- package/dist/renderer/scene.js +125 -0
- package/dist/renderer/scene.js.map +1 -0
- package/dist/renderer/textures.d.ts +20 -0
- package/dist/renderer/textures.d.ts.map +1 -0
- package/dist/renderer/textures.js +61 -0
- package/dist/renderer/textures.js.map +1 -0
- package/dist/renderer/view.d.ts +139 -0
- package/dist/renderer/view.d.ts.map +1 -0
- package/dist/renderer/view.js +349 -0
- package/dist/renderer/view.js.map +1 -0
- package/dist/rpgjs/shared.d.ts +41 -0
- package/dist/rpgjs/shared.d.ts.map +1 -0
- package/dist/rpgjs/shared.js +77 -0
- package/dist/rpgjs/shared.js.map +1 -0
- package/dist/server/index.d.ts +64 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +205 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/storage.d.ts +16 -0
- package/dist/server/storage.d.ts.map +1 -0
- package/dist/server/storage.js +52 -0
- package/dist/server/storage.js.map +1 -0
- package/dist/skins/default.d.ts +27 -0
- package/dist/skins/default.d.ts.map +1 -0
- package/dist/skins/default.js +144 -0
- package/dist/skins/default.js.map +1 -0
- package/dist/skins/frames.d.ts +19 -0
- package/dist/skins/frames.d.ts.map +1 -0
- package/dist/skins/frames.js +49 -0
- package/dist/skins/frames.js.map +1 -0
- package/dist/skins/game.d.ts +21 -0
- package/dist/skins/game.d.ts.map +1 -0
- package/dist/skins/game.js +21 -0
- package/dist/skins/game.js.map +1 -0
- package/dist/skins/index.d.ts +5 -0
- package/dist/skins/index.d.ts.map +1 -0
- package/dist/skins/index.js +5 -0
- package/dist/skins/types.d.ts +46 -0
- package/dist/skins/types.d.ts.map +1 -0
- package/dist/skins/types.js +12 -0
- package/dist/skins/types.js.map +1 -0
- package/dist/terrain/data.d.ts +10 -0
- package/dist/terrain/data.d.ts.map +1 -0
- package/dist/terrain/data.js +83 -0
- package/dist/terrain/data.js.map +1 -0
- package/dist/terrain/define.d.ts +72 -0
- package/dist/terrain/define.d.ts.map +1 -0
- package/dist/terrain/define.js +91 -0
- package/dist/terrain/define.js.map +1 -0
- package/dist/terrain/editing.d.ts +54 -0
- package/dist/terrain/editing.d.ts.map +1 -0
- package/dist/terrain/editing.js +169 -0
- package/dist/terrain/editing.js.map +1 -0
- package/dist/terrain/index.d.ts +10 -0
- package/dist/terrain/index.d.ts.map +1 -0
- package/dist/terrain/index.js +10 -0
- package/dist/terrain/kernel.d.ts +57 -0
- package/dist/terrain/kernel.d.ts.map +1 -0
- package/dist/terrain/kernel.js +198 -0
- package/dist/terrain/kernel.js.map +1 -0
- package/dist/terrain/mask.d.ts +37 -0
- package/dist/terrain/mask.d.ts.map +1 -0
- package/dist/terrain/mask.js +244 -0
- package/dist/terrain/mask.js.map +1 -0
- package/dist/terrain/patches.d.ts +32 -0
- package/dist/terrain/patches.d.ts.map +1 -0
- package/dist/terrain/patches.js +135 -0
- package/dist/terrain/patches.js.map +1 -0
- package/dist/terrain/rle.d.ts +4 -0
- package/dist/terrain/rle.d.ts.map +1 -0
- package/dist/terrain/rle.js +68 -0
- package/dist/terrain/rle.js.map +1 -0
- package/dist/terrain/scheduler.d.ts +46 -0
- package/dist/terrain/scheduler.d.ts.map +1 -0
- package/dist/terrain/scheduler.js +148 -0
- package/dist/terrain/scheduler.js.map +1 -0
- package/dist/terrain/types.d.ts +110 -0
- package/dist/terrain/types.d.ts.map +1 -0
- package/dist/terrain/types.js +11 -0
- package/dist/terrain/types.js.map +1 -0
- package/dist/terrain-render/backend.d.ts +66 -0
- package/dist/terrain-render/backend.d.ts.map +1 -0
- package/dist/terrain-render/backend.js +96 -0
- package/dist/terrain-render/backend.js.map +1 -0
- package/dist/terrain-render/host.d.ts +21 -0
- package/dist/terrain-render/host.d.ts.map +1 -0
- package/dist/terrain-render/host.js +62 -0
- package/dist/terrain-render/host.js.map +1 -0
- package/dist/terrain-render/index.d.ts +3 -0
- package/dist/terrain-render/index.d.ts.map +1 -0
- package/dist/terrain-render/index.js +3 -0
- package/dist/terrain-render/worker.d.ts +2 -0
- package/dist/terrain-render/worker.d.ts.map +1 -0
- package/dist/terrain-render/worker.js +34 -0
- package/dist/terrain-render/worker.js.map +1 -0
- package/package.json +124 -3
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mask.js","names":[],"sources":["../../src/terrain/mask.ts"],"sourcesContent":["import { NO_MATERIAL, type MaskRect, type TerrainData, type TerrainMask, type TerrainShape } from \"./types\";\nimport { decodeRuns, encodeRuns } from \"./rle\";\n\n/** Pure operations on the chunked mask. Nothing here mutates its input. */\nconst key = (col: number, row: number): string => `${col},${row}`;\n\nexport const chunkCount = (mask: TerrainMask): { cols: number; rows: number } => ({\n cols: Math.ceil(mask.width / mask.chunk),\n rows: Math.ceil(mask.height / mask.chunk),\n});\n\n/** The values of a chunk. A chunk that is not stored is flat. */\nexport function readChunk(mask: TerrainMask, col: number, row: number): Uint16Array {\n const stored = mask.chunks[key(col, row)];\n if (stored === undefined) return new Uint16Array(mask.chunk * mask.chunk).fill(mask.fill);\n return decodeRuns(stored, mask.chunk * mask.chunk);\n}\n\n/** Material index at a mask pixel. */\nexport function readPixel(mask: TerrainMask, x: number, y: number): number {\n if (x < 0 || y < 0 || x >= mask.width || y >= mask.height) return NO_MATERIAL;\n return readChunk(mask, Math.floor(x / mask.chunk), Math.floor(y / mask.chunk))[(y % mask.chunk) * mask.chunk + (x % mask.chunk)]!;\n}\n\n/** Whole mask as one array, row by row. For the kernel and for tests: it is as big as the map. */\nexport function readAll(mask: TerrainMask): Uint16Array {\n const out = new Uint16Array(mask.width * mask.height);\n const { cols, rows } = chunkCount(mask);\n for (let row = 0; row < rows; row += 1) {\n for (let col = 0; col < cols; col += 1) {\n const chunk = readChunk(mask, col, row);\n const width = Math.min(mask.chunk, mask.width - col * mask.chunk);\n const height = Math.min(mask.chunk, mask.height - row * mask.chunk);\n for (let y = 0; y < height; y += 1) {\n out.set(chunk.subarray(y * mask.chunk, y * mask.chunk + width), (row * mask.chunk + y) * mask.width + col * mask.chunk);\n }\n }\n }\n return out;\n}\n\n/** Values of a rectangle, row by row. The rectangle is clipped by the caller. */\nexport function readRect(mask: TerrainMask, rect: MaskRect): Uint16Array {\n const out = new Uint16Array(rect.width * rect.height);\n const c0 = Math.floor(rect.x / mask.chunk);\n const c1 = Math.floor((rect.x + rect.width - 1) / mask.chunk);\n const r0 = Math.floor(rect.y / mask.chunk);\n const r1 = Math.floor((rect.y + rect.height - 1) / mask.chunk);\n for (let row = r0; row <= r1; row += 1) {\n for (let col = c0; col <= c1; col += 1) {\n const chunk = readChunk(mask, col, row);\n const x0 = Math.max(rect.x, col * mask.chunk);\n const x1 = Math.min(rect.x + rect.width, (col + 1) * mask.chunk);\n const y0 = Math.max(rect.y, row * mask.chunk);\n const y1 = Math.min(rect.y + rect.height, (row + 1) * mask.chunk);\n for (let y = y0; y < y1; y += 1) {\n const from = (y - row * mask.chunk) * mask.chunk + (x0 - col * mask.chunk);\n out.set(chunk.subarray(from, from + (x1 - x0)), (y - rect.y) * rect.width + (x0 - rect.x));\n }\n }\n }\n return out;\n}\n\nexport function clipRect(mask: TerrainMask, rect: MaskRect): MaskRect | undefined {\n const x0 = Math.max(0, Math.floor(rect.x));\n const y0 = Math.max(0, Math.floor(rect.y));\n const x1 = Math.min(mask.width, Math.ceil(rect.x + rect.width));\n const y1 = Math.min(mask.height, Math.ceil(rect.y + rect.height));\n return x1 > x0 && y1 > y0 ? { x: x0, y: y0, width: x1 - x0, height: y1 - y0 } : undefined;\n}\n\n/** Bounds of a shape in mask pixels, clipped to the mask. `undefined` when it is entirely outside. */\nexport function shapeBounds(mask: TerrainMask, shape: TerrainShape): MaskRect | undefined {\n const s = mask.scale;\n switch (shape.type) {\n case \"fill\":\n return { x: 0, y: 0, width: mask.width, height: mask.height };\n case \"circle\":\n return clipRect(mask, { x: (shape.x - shape.radius) / s, y: (shape.y - shape.radius) / s, width: (shape.radius * 2) / s + 1, height: (shape.radius * 2) / s + 1 });\n case \"rect\":\n return clipRect(mask, { x: shape.x / s, y: shape.y / s, width: shape.width / s, height: shape.height / s });\n case \"stroke\": {\n if (shape.points.length === 0) return undefined;\n const xs = shape.points.map((p) => p[0]);\n const ys = shape.points.map((p) => p[1]);\n const x0 = Math.min(...xs) - shape.radius;\n const y0 = Math.min(...ys) - shape.radius;\n return clipRect(mask, { x: x0 / s, y: y0 / s, width: (Math.max(...xs) - Math.min(...xs) + shape.radius * 2) / s + 1, height: (Math.max(...ys) - Math.min(...ys) + shape.radius * 2) / s + 1 });\n }\n }\n}\n\nconst distanceToSegment2 = (px: number, py: number, ax: number, ay: number, bx: number, by: number): number => {\n const dx = bx - ax;\n const dy = by - ay;\n const length2 = dx * dx + dy * dy;\n const t = length2 === 0 ? 0 : Math.max(0, Math.min(1, ((px - ax) * dx + (py - ay) * dy) / length2));\n const cx = ax + t * dx;\n const cy = ay + t * dy;\n return (px - cx) ** 2 + (py - cy) ** 2;\n};\n\n/** Whether the centre of a mask pixel is inside the shape. */\nexport function covers(mask: TerrainMask, shape: TerrainShape, x: number, y: number): boolean {\n const wx = (x + 0.5) * mask.scale;\n const wy = (y + 0.5) * mask.scale;\n switch (shape.type) {\n case \"fill\":\n return true;\n case \"circle\":\n return (wx - shape.x) ** 2 + (wy - shape.y) ** 2 <= shape.radius ** 2;\n case \"rect\":\n return wx >= shape.x && wx < shape.x + shape.width && wy >= shape.y && wy < shape.y + shape.height;\n case \"stroke\": {\n const r2 = shape.radius ** 2;\n if (shape.points.length === 1) return (wx - shape.points[0]![0]) ** 2 + (wy - shape.points[0]![1]) ** 2 <= r2;\n for (let i = 1; i < shape.points.length; i += 1) {\n const a = shape.points[i - 1]!;\n const b = shape.points[i]!;\n if (distanceToSegment2(wx, wy, a[0], a[1], b[0], b[1]) <= r2) return true;\n }\n return false;\n }\n }\n}\n\nexport interface PaintResult {\n readonly mask: TerrainMask;\n /** Smallest rectangle (in mask pixels) that contains every pixel that changed. Undefined when none did. */\n readonly changed?: MaskRect;\n /** The values of `changed` before the change, for the inverse. */\n readonly before?: Uint16Array;\n}\n\n/** Sets the material under a shape. Chunks that do not change are shared with the input. */\nexport function paintShape(mask: TerrainMask, shape: TerrainShape, material: number): PaintResult {\n const bounds = shapeBounds(mask, shape);\n if (!bounds) return { mask };\n const c0 = Math.floor(bounds.x / mask.chunk);\n const c1 = Math.floor((bounds.x + bounds.width - 1) / mask.chunk);\n const r0 = Math.floor(bounds.y / mask.chunk);\n const r1 = Math.floor((bounds.y + bounds.height - 1) / mask.chunk);\n const next: Record<string, string> = { ...mask.chunks };\n let minX = Infinity, minY = Infinity, maxX = -1, maxY = -1;\n for (let row = r0; row <= r1; row += 1) {\n for (let col = c0; col <= c1; col += 1) {\n const values = readChunk(mask, col, row);\n let dirty = false;\n const x0 = Math.max(bounds.x, col * mask.chunk);\n const x1 = Math.min(bounds.x + bounds.width, (col + 1) * mask.chunk);\n const y0 = Math.max(bounds.y, row * mask.chunk);\n const y1 = Math.min(bounds.y + bounds.height, (row + 1) * mask.chunk);\n for (let y = y0; y < y1; y += 1) {\n for (let x = x0; x < x1; x += 1) {\n if (!covers(mask, shape, x, y)) continue;\n const index = (y - row * mask.chunk) * mask.chunk + (x - col * mask.chunk);\n if (values[index] === material) continue;\n values[index] = material;\n dirty = true;\n if (x < minX) minX = x;\n if (x > maxX) maxX = x;\n if (y < minY) minY = y;\n if (y > maxY) maxY = y;\n }\n }\n if (!dirty) continue;\n const encoded = encodeRuns(values);\n // A chunk that went back to being flat is not stored.\n if (values.every((value) => value === mask.fill)) delete next[key(col, row)];\n else next[key(col, row)] = encoded;\n }\n }\n if (maxX < 0) return { mask };\n const changed: MaskRect = { x: minX, y: minY, width: maxX - minX + 1, height: maxY - minY + 1 };\n // The values before, over the changed rectangle, taken from the chunks as they were.\n const before = readRect(mask, changed);\n return { mask: { ...mask, chunks: next }, changed, before };\n}\n\n/** Writes values over a rectangle (an inverse, or an imported region). Chunks that do not change are shared. */\nexport function writeRect(mask: TerrainMask, rect: MaskRect, values: ArrayLike<number>): PaintResult {\n const clipped = clipRect(mask, rect);\n if (!clipped || clipped.x !== rect.x || clipped.y !== rect.y || clipped.width !== rect.width || clipped.height !== rect.height) return { mask };\n const c0 = Math.floor(rect.x / mask.chunk);\n const c1 = Math.floor((rect.x + rect.width - 1) / mask.chunk);\n const r0 = Math.floor(rect.y / mask.chunk);\n const r1 = Math.floor((rect.y + rect.height - 1) / mask.chunk);\n const next: Record<string, string> = { ...mask.chunks };\n let minX = Infinity, minY = Infinity, maxX = -1, maxY = -1;\n for (let row = r0; row <= r1; row += 1) {\n for (let col = c0; col <= c1; col += 1) {\n const chunk = readChunk(mask, col, row);\n let dirty = false;\n const x0 = Math.max(rect.x, col * mask.chunk);\n const x1 = Math.min(rect.x + rect.width, (col + 1) * mask.chunk);\n const y0 = Math.max(rect.y, row * mask.chunk);\n const y1 = Math.min(rect.y + rect.height, (row + 1) * mask.chunk);\n for (let y = y0; y < y1; y += 1) {\n for (let x = x0; x < x1; x += 1) {\n const value = values[(y - rect.y) * rect.width + (x - rect.x)]!;\n const index = (y - row * mask.chunk) * mask.chunk + (x - col * mask.chunk);\n if (chunk[index] === value) continue;\n chunk[index] = value;\n dirty = true;\n if (x < minX) minX = x;\n if (x > maxX) maxX = x;\n if (y < minY) minY = y;\n if (y > maxY) maxY = y;\n }\n }\n if (!dirty) continue;\n if (chunk.every((value) => value === mask.fill)) delete next[key(col, row)];\n else next[key(col, row)] = encodeRuns(chunk);\n }\n }\n if (maxX < 0) return { mask };\n const changed: MaskRect = { x: minX, y: minY, width: maxX - minX + 1, height: maxY - minY + 1 };\n return { mask: { ...mask, chunks: next }, changed, before: readRect(mask, changed) };\n}\n\n/** A new, flat mask. */\nexport function createMask(width: number, height: number, options: { scale?: number; chunk?: number; fill?: number } = {}): TerrainMask {\n const scale = options.scale ?? 1;\n return {\n scale,\n width: Math.ceil(width / scale),\n height: Math.ceil(height / scale),\n chunk: options.chunk ?? 256,\n fill: options.fill ?? NO_MATERIAL,\n chunks: {},\n };\n}\n\nexport type { TerrainData };\n"],"mappings":"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"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { JsonObject } from '../model/json';
|
|
2
|
+
import { PatchHandlers } from '../model/patches';
|
|
3
|
+
import { readRect } from './mask';
|
|
4
|
+
import { MaskRect, TerrainData, TerrainMaterial, TerrainShape } from './types';
|
|
5
|
+
declare module "../model/patches" {
|
|
6
|
+
interface PatchRegistry {
|
|
7
|
+
/** Paints a shape of the mask with a material, or erases it with `null`. */
|
|
8
|
+
"terrain.paint": {
|
|
9
|
+
readonly shape: TerrainShape;
|
|
10
|
+
readonly material: string | null;
|
|
11
|
+
};
|
|
12
|
+
/** Replaces a rectangle of the mask (mask pixels) with run-length encoded values. It is the inverse of a paint. */
|
|
13
|
+
"terrain.region": MaskRect & {
|
|
14
|
+
readonly runs: string;
|
|
15
|
+
};
|
|
16
|
+
/** Replaces the palette. Existing materials keep their id and their position: the mask refers to positions. */
|
|
17
|
+
"terrain.materials": {
|
|
18
|
+
readonly materials: readonly TerrainMaterial[];
|
|
19
|
+
};
|
|
20
|
+
/** Replaces the morphology features (holes and walls). */
|
|
21
|
+
"terrain.morphology": {
|
|
22
|
+
readonly features: readonly JsonObject[];
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/** Most pixel tests one paint may ask for (pixels in the bounds x segments of a stroke): a client must not be able to stall the authority. */
|
|
27
|
+
export declare const MAX_PAINT_COST = 50000000;
|
|
28
|
+
export declare const paintCost: (mask: TerrainData["mask"], shape: TerrainShape) => number;
|
|
29
|
+
/** Handlers of the terrain patches. Pass them to `createMapModel({ patchHandlers: [terrainPatchHandlers] })`. */
|
|
30
|
+
export declare const terrainPatchHandlers: PatchHandlers;
|
|
31
|
+
export { readRect };
|
|
32
|
+
//# sourceMappingURL=patches.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"patches.d.ts","sourceRoot":"","sources":["../../src/terrain/patches.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAa,MAAM,eAAe,CAAC;AAG3D,OAAO,EAA0D,KAAK,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAE9G,OAAO,EAAwB,QAAQ,EAA0B,MAAM,QAAQ,CAAC;AAEhF,OAAO,EAAwB,KAAK,QAAQ,EAAE,KAAK,WAAW,EAAE,KAAK,eAAe,EAAE,KAAK,YAAY,EAAe,MAAM,SAAS,CAAC;AAEtI,OAAO,QAAQ,kBAAkB,CAAC;IAChC,UAAU,aAAa;QACrB,4EAA4E;QAC5E,eAAe,EAAE;YAAE,QAAQ,CAAC,KAAK,EAAE,YAAY,CAAC;YAAC,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;SAAE,CAAC;QACpF,mHAAmH;QACnH,gBAAgB,EAAE,QAAQ,GAAG;YAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;SAAE,CAAC;QACvD,+GAA+G;QAC/G,mBAAmB,EAAE;YAAE,QAAQ,CAAC,SAAS,EAAE,SAAS,eAAe,EAAE,CAAA;SAAE,CAAC;QACxE,0DAA0D;QAC1D,oBAAoB,EAAE;YAAE,QAAQ,CAAC,QAAQ,EAAE,SAAS,UAAU,EAAE,CAAA;SAAE,CAAC;KACpE;CACF;AAgCD,8IAA8I;AAC9I,eAAO,MAAM,cAAc,WAAa,CAAC;AAEzC,eAAO,MAAM,SAAS,GAAI,MAAM,WAAW,CAAC,MAAM,CAAC,EAAE,OAAO,YAAY,KAAG,MAK1E,CAAC;AAIF,iHAAiH;AACjH,eAAO,MAAM,oBAAoB,EAAE,aA4EjC,CAAC;AAEH,OAAO,EAAE,QAAQ,EAAE,CAAC"}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { cloneJson, isFiniteNumberValue, isPlainObject } from "../model/json.js";
|
|
2
|
+
import { definePatchHandlers, patchFail, patchOk } from "../model/patches.js";
|
|
3
|
+
import { NO_MATERIAL, TERRAIN_EXTENSION_ID } from "./types.js";
|
|
4
|
+
import { terrainIssues } from "./data.js";
|
|
5
|
+
import { decodeRuns, encodeRuns } from "./rle.js";
|
|
6
|
+
import { clipRect, paintShape, shapeBounds, writeRect } from "./mask.js";
|
|
7
|
+
//#region src/terrain/patches.ts
|
|
8
|
+
function terrainOf(state) {
|
|
9
|
+
const value = state.extensions?.[TERRAIN_EXTENSION_ID];
|
|
10
|
+
if (value === void 0) return { failure: "this map has no terrain" };
|
|
11
|
+
const issues = terrainIssues(value);
|
|
12
|
+
return issues.length > 0 ? { failure: `the terrain of this map is invalid (${issues[0].path}: ${issues[0].message})` } : value;
|
|
13
|
+
}
|
|
14
|
+
var withTerrain = (state, terrain) => ({
|
|
15
|
+
...state,
|
|
16
|
+
extensions: {
|
|
17
|
+
...state.extensions ?? {},
|
|
18
|
+
[TERRAIN_EXTENSION_ID]: terrain
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
var validShape = (shape) => {
|
|
22
|
+
if (!isPlainObject(shape)) return false;
|
|
23
|
+
const finite = (...values) => values.every(isFiniteNumberValue);
|
|
24
|
+
switch (shape.type) {
|
|
25
|
+
case "fill": return true;
|
|
26
|
+
case "circle": return finite(shape.x, shape.y, shape.radius) && shape.radius >= 0;
|
|
27
|
+
case "rect": return finite(shape.x, shape.y, shape.width, shape.height);
|
|
28
|
+
case "stroke": return finite(shape.radius) && shape.radius >= 0 && Array.isArray(shape.points) && shape.points.length > 0 && shape.points.length <= 1e4 && shape.points.every((p) => Array.isArray(p) && p.length === 2 && finite(p[0], p[1]));
|
|
29
|
+
default: return false;
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
/** Most pixel tests one paint may ask for (pixels in the bounds x segments of a stroke): a client must not be able to stall the authority. */
|
|
33
|
+
var MAX_PAINT_COST = 5e7;
|
|
34
|
+
var paintCost = (mask, shape) => {
|
|
35
|
+
if (shape.type === "fill") return 0;
|
|
36
|
+
const bounds = shapeBounds(mask, shape);
|
|
37
|
+
if (!bounds) return 0;
|
|
38
|
+
return bounds.width * bounds.height * (shape.type === "stroke" ? Math.max(1, shape.points.length - 1) : 1);
|
|
39
|
+
};
|
|
40
|
+
var regionPatch = (rect, values) => ({
|
|
41
|
+
type: "terrain.region",
|
|
42
|
+
...rect,
|
|
43
|
+
runs: encodeRuns(values)
|
|
44
|
+
});
|
|
45
|
+
/** Handlers of the terrain patches. Pass them to `createMapModel({ patchHandlers: [terrainPatchHandlers] })`. */
|
|
46
|
+
var terrainPatchHandlers = definePatchHandlers({
|
|
47
|
+
"terrain.paint": { apply(state, patch) {
|
|
48
|
+
const terrain = terrainOf(state);
|
|
49
|
+
if ("failure" in terrain) return patchFail("invalid-patch", terrain.failure);
|
|
50
|
+
if (!validShape(patch.shape)) return patchFail("invalid-patch", "shape is not valid");
|
|
51
|
+
if (paintCost(terrain.mask, patch.shape) > 5e7) return patchFail("invalid-patch", "shape is too large to paint at once");
|
|
52
|
+
let value;
|
|
53
|
+
if (patch.material === null) value = NO_MATERIAL;
|
|
54
|
+
else {
|
|
55
|
+
value = terrain.materials.findIndex((material) => material.id === patch.material);
|
|
56
|
+
if (value < 0) return patchFail("not-found", `material "${String(patch.material)}" is not in the palette`);
|
|
57
|
+
}
|
|
58
|
+
const result = paintShape(terrain.mask, patch.shape, value);
|
|
59
|
+
if (!result.changed) return patchOk(state, []);
|
|
60
|
+
return patchOk(withTerrain(state, {
|
|
61
|
+
...terrain,
|
|
62
|
+
mask: result.mask
|
|
63
|
+
}), [regionPatch(result.changed, result.before)]);
|
|
64
|
+
} },
|
|
65
|
+
"terrain.region": { apply(state, patch) {
|
|
66
|
+
const terrain = terrainOf(state);
|
|
67
|
+
if ("failure" in terrain) return patchFail("invalid-patch", terrain.failure);
|
|
68
|
+
const rect = {
|
|
69
|
+
x: patch.x,
|
|
70
|
+
y: patch.y,
|
|
71
|
+
width: patch.width,
|
|
72
|
+
height: patch.height
|
|
73
|
+
};
|
|
74
|
+
if (![
|
|
75
|
+
rect.x,
|
|
76
|
+
rect.y,
|
|
77
|
+
rect.width,
|
|
78
|
+
rect.height
|
|
79
|
+
].every((n) => Number.isInteger(n)) || rect.width <= 0 || rect.height <= 0) return patchFail("invalid-patch", "region must be a rectangle of whole pixels");
|
|
80
|
+
const clipped = clipRect(terrain.mask, rect);
|
|
81
|
+
if (!clipped || clipped.x !== rect.x || clipped.y !== rect.y || clipped.width !== rect.width || clipped.height !== rect.height) return patchFail("invalid-patch", "region is outside the mask");
|
|
82
|
+
let values;
|
|
83
|
+
try {
|
|
84
|
+
values = decodeRuns(patch.runs, rect.width * rect.height);
|
|
85
|
+
} catch (error) {
|
|
86
|
+
return patchFail("invalid-patch", error.message);
|
|
87
|
+
}
|
|
88
|
+
const limit = terrain.materials.length;
|
|
89
|
+
if (values.some((value) => value !== 65535 && value >= limit)) return patchFail("invalid-patch", "region uses a material that is not in the palette");
|
|
90
|
+
const result = writeRect(terrain.mask, rect, values);
|
|
91
|
+
if (!result.changed) return patchOk(state, []);
|
|
92
|
+
return patchOk(withTerrain(state, {
|
|
93
|
+
...terrain,
|
|
94
|
+
mask: result.mask
|
|
95
|
+
}), [regionPatch(result.changed, result.before)]);
|
|
96
|
+
} },
|
|
97
|
+
"terrain.materials": { apply(state, patch) {
|
|
98
|
+
const terrain = terrainOf(state);
|
|
99
|
+
if ("failure" in terrain) return patchFail("invalid-patch", terrain.failure);
|
|
100
|
+
if (!Array.isArray(patch.materials)) return patchFail("invalid-patch", "materials must be an array");
|
|
101
|
+
const candidate = {
|
|
102
|
+
...terrain,
|
|
103
|
+
materials: cloneJson(patch.materials)
|
|
104
|
+
};
|
|
105
|
+
const issues = terrainIssues(candidate);
|
|
106
|
+
if (issues.length > 0) return patchFail("invalid-patch", `${issues[0].path}: ${issues[0].message}`);
|
|
107
|
+
if (!terrain.materials.every((material, index) => candidate.materials[index] === void 0 || candidate.materials[index].id === material.id)) return patchFail("invalid-patch", "existing materials must keep their id and position (the mask refers to positions)");
|
|
108
|
+
if (candidate.materials.length < terrain.materials.length) {
|
|
109
|
+
const limit = candidate.materials.length;
|
|
110
|
+
if (terrain.mask.fill !== 65535 && terrain.mask.fill >= limit || Object.entries(terrain.mask.chunks).some(([, encoded]) => decodeRuns(encoded, terrain.mask.chunk * terrain.mask.chunk).some((value) => value !== 65535 && value >= limit))) return patchFail("invalid-patch", "a material that is removed is still painted on the mask");
|
|
111
|
+
}
|
|
112
|
+
return patchOk(withTerrain(state, candidate), [{
|
|
113
|
+
type: "terrain.materials",
|
|
114
|
+
materials: terrain.materials
|
|
115
|
+
}]);
|
|
116
|
+
} },
|
|
117
|
+
"terrain.morphology": { apply(state, patch) {
|
|
118
|
+
const terrain = terrainOf(state);
|
|
119
|
+
if ("failure" in terrain) return patchFail("invalid-patch", terrain.failure);
|
|
120
|
+
const candidate = {
|
|
121
|
+
...terrain,
|
|
122
|
+
morphology: cloneJson(patch.features)
|
|
123
|
+
};
|
|
124
|
+
const issues = terrainIssues(candidate);
|
|
125
|
+
if (issues.length > 0) return patchFail("invalid-patch", `${issues[0].path}: ${issues[0].message}`);
|
|
126
|
+
return patchOk(withTerrain(state, candidate), [{
|
|
127
|
+
type: "terrain.morphology",
|
|
128
|
+
features: terrain.morphology ?? []
|
|
129
|
+
}]);
|
|
130
|
+
} }
|
|
131
|
+
});
|
|
132
|
+
//#endregion
|
|
133
|
+
export { MAX_PAINT_COST, paintCost, terrainPatchHandlers };
|
|
134
|
+
|
|
135
|
+
//# sourceMappingURL=patches.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"patches.js","names":[],"sources":["../../src/terrain/patches.ts"],"sourcesContent":["import type { JsonObject, JsonValue } from \"../model/json\";\nimport { cloneJson, isFiniteNumberValue, isPlainObject } from \"../model/json\";\nimport type { MapState } from \"../model/definition\";\nimport { definePatchHandlers, patchFail, patchOk, type MapPatch, type PatchHandlers } from \"../model/patches\";\nimport { terrainIssues } from \"./data\";\nimport { clipRect, paintShape, readRect, shapeBounds, writeRect } from \"./mask\";\nimport { decodeRuns, encodeRuns } from \"./rle\";\nimport { TERRAIN_EXTENSION_ID, type MaskRect, type TerrainData, type TerrainMaterial, type TerrainShape, NO_MATERIAL } from \"./types\";\n\ndeclare module \"../model/patches\" {\n interface PatchRegistry {\n /** Paints a shape of the mask with a material, or erases it with `null`. */\n \"terrain.paint\": { readonly shape: TerrainShape; readonly material: string | null };\n /** Replaces a rectangle of the mask (mask pixels) with run-length encoded values. It is the inverse of a paint. */\n \"terrain.region\": MaskRect & { readonly runs: string };\n /** Replaces the palette. Existing materials keep their id and their position: the mask refers to positions. */\n \"terrain.materials\": { readonly materials: readonly TerrainMaterial[] };\n /** Replaces the morphology features (holes and walls). */\n \"terrain.morphology\": { readonly features: readonly JsonObject[] };\n }\n}\n\nfunction terrainOf(state: MapState): TerrainData | { readonly failure: string } {\n const value = state.extensions?.[TERRAIN_EXTENSION_ID];\n if (value === undefined) return { failure: \"this map has no terrain\" };\n const issues = terrainIssues(value);\n return issues.length > 0 ? { failure: `the terrain of this map is invalid (${issues[0]!.path}: ${issues[0]!.message})` } : (value as unknown as TerrainData);\n}\n\nconst withTerrain = (state: MapState, terrain: TerrainData): MapState => ({\n ...state,\n extensions: { ...(state.extensions ?? {}), [TERRAIN_EXTENSION_ID]: terrain as unknown as JsonValue },\n});\n\nconst validShape = (shape: unknown): shape is TerrainShape => {\n if (!isPlainObject(shape)) return false;\n const finite = (...values: unknown[]) => values.every(isFiniteNumberValue);\n switch (shape.type) {\n case \"fill\":\n return true;\n case \"circle\":\n return finite(shape.x, shape.y, shape.radius) && (shape.radius as number) >= 0;\n case \"rect\":\n return finite(shape.x, shape.y, shape.width, shape.height);\n case \"stroke\":\n return finite(shape.radius) && (shape.radius as number) >= 0 && Array.isArray(shape.points) && shape.points.length > 0 && shape.points.length <= 10_000\n && shape.points.every((p: unknown) => Array.isArray(p) && p.length === 2 && finite(p[0], p[1]));\n default:\n return false;\n }\n};\n\n/** Most pixel tests one paint may ask for (pixels in the bounds x segments of a stroke): a client must not be able to stall the authority. */\nexport const MAX_PAINT_COST = 50_000_000;\n\nexport const paintCost = (mask: TerrainData[\"mask\"], shape: TerrainShape): number => {\n if (shape.type === \"fill\") return 0;\n const bounds = shapeBounds(mask, shape);\n if (!bounds) return 0;\n return bounds.width * bounds.height * (shape.type === \"stroke\" ? Math.max(1, shape.points.length - 1) : 1);\n};\n\nconst regionPatch = (rect: MaskRect, values: Uint16Array): MapPatch => ({ type: \"terrain.region\", ...rect, runs: encodeRuns(values) });\n\n/** Handlers of the terrain patches. Pass them to `createMapModel({ patchHandlers: [terrainPatchHandlers] })`. */\nexport const terrainPatchHandlers: PatchHandlers = definePatchHandlers({\n \"terrain.paint\": {\n apply(state, patch) {\n const terrain = terrainOf(state);\n if (\"failure\" in terrain) return patchFail(\"invalid-patch\", terrain.failure);\n if (!validShape(patch.shape)) return patchFail(\"invalid-patch\", \"shape is not valid\");\n if (paintCost(terrain.mask, patch.shape) > MAX_PAINT_COST) return patchFail(\"invalid-patch\", \"shape is too large to paint at once\");\n let value: number;\n if (patch.material === null) value = NO_MATERIAL;\n else {\n value = terrain.materials.findIndex((material) => material.id === patch.material);\n if (value < 0) return patchFail(\"not-found\", `material \"${String(patch.material)}\" is not in the palette`);\n }\n const result = paintShape(terrain.mask, patch.shape, value);\n if (!result.changed) return patchOk(state, []);\n return patchOk(withTerrain(state, { ...terrain, mask: result.mask }), [regionPatch(result.changed, result.before!)]);\n },\n },\n\n \"terrain.region\": {\n apply(state, patch) {\n const terrain = terrainOf(state);\n if (\"failure\" in terrain) return patchFail(\"invalid-patch\", terrain.failure);\n const rect: MaskRect = { x: patch.x, y: patch.y, width: patch.width, height: patch.height };\n if (![rect.x, rect.y, rect.width, rect.height].every((n) => Number.isInteger(n)) || rect.width <= 0 || rect.height <= 0) {\n return patchFail(\"invalid-patch\", \"region must be a rectangle of whole pixels\");\n }\n const clipped = clipRect(terrain.mask, rect);\n if (!clipped || clipped.x !== rect.x || clipped.y !== rect.y || clipped.width !== rect.width || clipped.height !== rect.height) {\n return patchFail(\"invalid-patch\", \"region is outside the mask\");\n }\n let values: Uint16Array;\n try {\n values = decodeRuns(patch.runs, rect.width * rect.height);\n } catch (error) {\n return patchFail(\"invalid-patch\", (error as Error).message);\n }\n const limit = terrain.materials.length;\n if (values.some((value) => value !== NO_MATERIAL && value >= limit)) return patchFail(\"invalid-patch\", \"region uses a material that is not in the palette\");\n const result = writeRect(terrain.mask, rect, values);\n if (!result.changed) return patchOk(state, []);\n return patchOk(withTerrain(state, { ...terrain, mask: result.mask }), [regionPatch(result.changed, result.before!)]);\n },\n },\n\n \"terrain.materials\": {\n apply(state, patch) {\n const terrain = terrainOf(state);\n if (\"failure\" in terrain) return patchFail(\"invalid-patch\", terrain.failure);\n if (!Array.isArray(patch.materials)) return patchFail(\"invalid-patch\", \"materials must be an array\");\n const candidate = { ...terrain, materials: cloneJson(patch.materials as unknown as JsonValue) as unknown as TerrainMaterial[] };\n const issues = terrainIssues(candidate as unknown as JsonValue);\n if (issues.length > 0) return patchFail(\"invalid-patch\", `${issues[0]!.path}: ${issues[0]!.message}`);\n // Positions are what the mask stores: what stays must stay where it is, and only unused materials at the end may go.\n const kept = terrain.materials.every((material, index) => candidate.materials[index] === undefined || candidate.materials[index]!.id === material.id);\n if (!kept) return patchFail(\"invalid-patch\", \"existing materials must keep their id and position (the mask refers to positions)\");\n if (candidate.materials.length < terrain.materials.length) {\n const limit = candidate.materials.length;\n const inUse = terrain.mask.fill !== NO_MATERIAL && terrain.mask.fill >= limit\n || Object.entries(terrain.mask.chunks).some(([, encoded]) => decodeRuns(encoded, terrain.mask.chunk * terrain.mask.chunk).some((value) => value !== NO_MATERIAL && value >= limit));\n if (inUse) return patchFail(\"invalid-patch\", \"a material that is removed is still painted on the mask\");\n }\n return patchOk(withTerrain(state, candidate), [{ type: \"terrain.materials\", materials: terrain.materials }]);\n },\n },\n\n \"terrain.morphology\": {\n apply(state, patch) {\n const terrain = terrainOf(state);\n if (\"failure\" in terrain) return patchFail(\"invalid-patch\", terrain.failure);\n const candidate = { ...terrain, morphology: cloneJson(patch.features as unknown as JsonValue) as unknown as JsonObject[] };\n const issues = terrainIssues(candidate as unknown as JsonValue);\n if (issues.length > 0) return patchFail(\"invalid-patch\", `${issues[0]!.path}: ${issues[0]!.message}`);\n return patchOk(withTerrain(state, candidate), [{ type: \"terrain.morphology\", features: terrain.morphology ?? [] }]);\n },\n },\n});\n\nexport { readRect };\n"],"mappings":"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"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare function encodeRuns(values: ArrayLike<number>): string;
|
|
2
|
+
/** Decodes into `length` values. Throws when the text is not a valid encoding of exactly that many. */
|
|
3
|
+
export declare function decodeRuns(text: string, length: number): Uint16Array;
|
|
4
|
+
//# sourceMappingURL=rle.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rle.d.ts","sourceRoot":"","sources":["../../src/terrain/rle.ts"],"names":[],"mappings":"AAcA,wBAAgB,UAAU,CAAC,MAAM,EAAE,SAAS,CAAC,MAAM,CAAC,GAAG,MAAM,CAc5D;AAED,uGAAuG;AACvG,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,WAAW,CAiCpE"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
//#region src/terrain/rle.ts
|
|
2
|
+
/**
|
|
3
|
+
* Run-length encoding of mask values (unsigned 16 bits): a sequence of `(value, run length)` pairs,
|
|
4
|
+
* written as variable-length integers and then as base64. A flat chunk is a few bytes; a chunk
|
|
5
|
+
* full of blobs stays small as long as rows are mostly uniform.
|
|
6
|
+
*/
|
|
7
|
+
var writeVarint = (bytes, value) => {
|
|
8
|
+
let rest = value;
|
|
9
|
+
while (rest >= 128) {
|
|
10
|
+
bytes.push(rest & 127 | 128);
|
|
11
|
+
rest = Math.floor(rest / 128);
|
|
12
|
+
}
|
|
13
|
+
bytes.push(rest);
|
|
14
|
+
};
|
|
15
|
+
function encodeRuns(values) {
|
|
16
|
+
const bytes = [];
|
|
17
|
+
let index = 0;
|
|
18
|
+
while (index < values.length) {
|
|
19
|
+
const value = values[index];
|
|
20
|
+
let end = index + 1;
|
|
21
|
+
while (end < values.length && values[end] === value) end += 1;
|
|
22
|
+
writeVarint(bytes, value);
|
|
23
|
+
writeVarint(bytes, end - index);
|
|
24
|
+
index = end;
|
|
25
|
+
}
|
|
26
|
+
let binary = "";
|
|
27
|
+
for (let i = 0; i < bytes.length; i += 32768) binary += String.fromCharCode(...bytes.slice(i, i + 32768));
|
|
28
|
+
return btoa(binary);
|
|
29
|
+
}
|
|
30
|
+
/** Decodes into `length` values. Throws when the text is not a valid encoding of exactly that many. */
|
|
31
|
+
function decodeRuns(text, length) {
|
|
32
|
+
let binary;
|
|
33
|
+
try {
|
|
34
|
+
binary = atob(text);
|
|
35
|
+
} catch {
|
|
36
|
+
throw new Error("terrain mask: not valid base64");
|
|
37
|
+
}
|
|
38
|
+
const out = new Uint16Array(length);
|
|
39
|
+
let position = 0;
|
|
40
|
+
let offset = 0;
|
|
41
|
+
const readVarint = () => {
|
|
42
|
+
let result = 0;
|
|
43
|
+
let factor = 1;
|
|
44
|
+
for (;;) {
|
|
45
|
+
if (position >= binary.length) throw new Error("terrain mask: truncated run");
|
|
46
|
+
const byte = binary.charCodeAt(position);
|
|
47
|
+
position += 1;
|
|
48
|
+
result += (byte & 127) * factor;
|
|
49
|
+
if ((byte & 128) === 0) return result;
|
|
50
|
+
factor *= 128;
|
|
51
|
+
if (factor > 2 ** 35) throw new Error("terrain mask: run too long");
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
while (position < binary.length) {
|
|
55
|
+
const value = readVarint();
|
|
56
|
+
const run = readVarint();
|
|
57
|
+
if (value > 65535) throw new Error("terrain mask: value out of range");
|
|
58
|
+
if (run === 0 || offset + run > length) throw new Error("terrain mask: runs do not fit the chunk");
|
|
59
|
+
out.fill(value, offset, offset + run);
|
|
60
|
+
offset += run;
|
|
61
|
+
}
|
|
62
|
+
if (offset !== length) throw new Error("terrain mask: runs do not fill the chunk");
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
65
|
+
//#endregion
|
|
66
|
+
export { decodeRuns, encodeRuns };
|
|
67
|
+
|
|
68
|
+
//# sourceMappingURL=rle.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rle.js","names":[],"sources":["../../src/terrain/rle.ts"],"sourcesContent":["/**\n * Run-length encoding of mask values (unsigned 16 bits): a sequence of `(value, run length)` pairs,\n * written as variable-length integers and then as base64. A flat chunk is a few bytes; a chunk\n * full of blobs stays small as long as rows are mostly uniform.\n */\nconst writeVarint = (bytes: number[], value: number): void => {\n let rest = value;\n while (rest >= 0x80) {\n bytes.push((rest & 0x7f) | 0x80);\n rest = Math.floor(rest / 128);\n }\n bytes.push(rest);\n};\n\nexport function encodeRuns(values: ArrayLike<number>): string {\n const bytes: number[] = [];\n let index = 0;\n while (index < values.length) {\n const value = values[index]!;\n let end = index + 1;\n while (end < values.length && values[end] === value) end += 1;\n writeVarint(bytes, value);\n writeVarint(bytes, end - index);\n index = end;\n }\n let binary = \"\";\n for (let i = 0; i < bytes.length; i += 0x8000) binary += String.fromCharCode(...bytes.slice(i, i + 0x8000));\n return btoa(binary);\n}\n\n/** Decodes into `length` values. Throws when the text is not a valid encoding of exactly that many. */\nexport function decodeRuns(text: string, length: number): Uint16Array {\n let binary: string;\n try {\n binary = atob(text);\n } catch {\n throw new Error(\"terrain mask: not valid base64\");\n }\n const out = new Uint16Array(length);\n let position = 0;\n let offset = 0;\n const readVarint = (): number => {\n let result = 0;\n let factor = 1;\n for (;;) {\n if (position >= binary.length) throw new Error(\"terrain mask: truncated run\");\n const byte = binary.charCodeAt(position);\n position += 1;\n result += (byte & 0x7f) * factor;\n if ((byte & 0x80) === 0) return result;\n factor *= 128;\n if (factor > 2 ** 35) throw new Error(\"terrain mask: run too long\");\n }\n };\n while (position < binary.length) {\n const value = readVarint();\n const run = readVarint();\n if (value > 0xffff) throw new Error(\"terrain mask: value out of range\");\n if (run === 0 || offset + run > length) throw new Error(\"terrain mask: runs do not fit the chunk\");\n out.fill(value, offset, offset + run);\n offset += run;\n }\n if (offset !== length) throw new Error(\"terrain mask: runs do not fill the chunk\");\n return out;\n}\n"],"mappings":";;;;;;AAKA,IAAM,eAAe,OAAiB,UAAwB;CAC5D,IAAI,OAAO;CACX,OAAO,QAAQ,KAAM;EACnB,MAAM,KAAM,OAAO,MAAQ,GAAI;EAC/B,OAAO,KAAK,MAAM,OAAO,GAAG;CAC9B;CACA,MAAM,KAAK,IAAI;AACjB;AAEA,SAAgB,WAAW,QAAmC;CAC5D,MAAM,QAAkB,CAAC;CACzB,IAAI,QAAQ;CACZ,OAAO,QAAQ,OAAO,QAAQ;EAC5B,MAAM,QAAQ,OAAO;EACrB,IAAI,MAAM,QAAQ;EAClB,OAAO,MAAM,OAAO,UAAU,OAAO,SAAS,OAAO,OAAO;EAC5D,YAAY,OAAO,KAAK;EACxB,YAAY,OAAO,MAAM,KAAK;EAC9B,QAAQ;CACV;CACA,IAAI,SAAS;CACb,KAAK,IAAI,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK,OAAQ,UAAU,OAAO,aAAa,GAAG,MAAM,MAAM,GAAG,IAAI,KAAM,CAAC;CAC1G,OAAO,KAAK,MAAM;AACpB;;AAGA,SAAgB,WAAW,MAAc,QAA6B;CACpE,IAAI;CACJ,IAAI;EACF,SAAS,KAAK,IAAI;CACpB,QAAQ;EACN,MAAM,IAAI,MAAM,gCAAgC;CAClD;CACA,MAAM,MAAM,IAAI,YAAY,MAAM;CAClC,IAAI,WAAW;CACf,IAAI,SAAS;CACb,MAAM,mBAA2B;EAC/B,IAAI,SAAS;EACb,IAAI,SAAS;EACb,SAAS;GACP,IAAI,YAAY,OAAO,QAAQ,MAAM,IAAI,MAAM,6BAA6B;GAC5E,MAAM,OAAO,OAAO,WAAW,QAAQ;GACvC,YAAY;GACZ,WAAW,OAAO,OAAQ;GAC1B,KAAK,OAAO,SAAU,GAAG,OAAO;GAChC,UAAU;GACV,IAAI,SAAS,KAAK,IAAI,MAAM,IAAI,MAAM,4BAA4B;EACpE;CACF;CACA,OAAO,WAAW,OAAO,QAAQ;EAC/B,MAAM,QAAQ,WAAW;EACzB,MAAM,MAAM,WAAW;EACvB,IAAI,QAAQ,OAAQ,MAAM,IAAI,MAAM,kCAAkC;EACtE,IAAI,QAAQ,KAAK,SAAS,MAAM,QAAQ,MAAM,IAAI,MAAM,yCAAyC;EACjG,IAAI,KAAK,OAAO,QAAQ,SAAS,GAAG;EACpC,UAAU;CACZ;CACA,IAAI,WAAW,QAAQ,MAAM,IAAI,MAAM,0CAA0C;CACjF,OAAO;AACT"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/** A rectangle in world pixels. */
|
|
2
|
+
export interface WorldRect {
|
|
3
|
+
readonly x: number;
|
|
4
|
+
readonly y: number;
|
|
5
|
+
readonly width: number;
|
|
6
|
+
readonly height: number;
|
|
7
|
+
}
|
|
8
|
+
export interface TerrainFrame extends WorldRect {
|
|
9
|
+
/** RGBA, `width * height * 4`. */
|
|
10
|
+
readonly pixels: Uint8ClampedArray;
|
|
11
|
+
}
|
|
12
|
+
export interface SchedulerOptions {
|
|
13
|
+
/** World size, in pixels. */
|
|
14
|
+
readonly size: readonly [width: number, height: number];
|
|
15
|
+
/** Side of the squares the map is rendered in. Defaults to 512. */
|
|
16
|
+
readonly chunk?: number;
|
|
17
|
+
/** How many renders may be in flight at once. Defaults to 1. */
|
|
18
|
+
readonly concurrency?: number;
|
|
19
|
+
/** Renders a rectangle. May resolve in any order relative to other renders. */
|
|
20
|
+
readonly render: (rect: WorldRect) => Promise<TerrainFrame>;
|
|
21
|
+
/** Puts a frame where it belongs. Called in the order the renders were started, never concurrently. */
|
|
22
|
+
readonly apply: (frame: TerrainFrame) => void;
|
|
23
|
+
readonly onError?: (error: unknown, rect: WorldRect) => void;
|
|
24
|
+
}
|
|
25
|
+
export interface TerrainScheduler {
|
|
26
|
+
/** Something changed in this rectangle (or everywhere): it will be rendered again. */
|
|
27
|
+
invalidate(rect: WorldRect | "all"): void;
|
|
28
|
+
/** What is looked at, so that it is rendered first. `null` means no preference. */
|
|
29
|
+
setViewport(viewport: WorldRect | null): void;
|
|
30
|
+
/** Renders in flight or waiting. */
|
|
31
|
+
readonly pending: number;
|
|
32
|
+
/** Resolves when nothing is waiting or in flight. */
|
|
33
|
+
whenIdle(): Promise<void>;
|
|
34
|
+
destroy(): void;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Decides what to render, in what order, and applies it safely.
|
|
38
|
+
*
|
|
39
|
+
* - The map is cut into chunks; a change marks the chunks it touches with the smallest rectangle that covers it.
|
|
40
|
+
* - Dirty chunks are rendered in the viewport first, then by distance to its centre.
|
|
41
|
+
* - Several renders run at once (a pool of workers), and finish in any order, but a frame is applied only
|
|
42
|
+
* after every frame started before it: a slow, older frame can never put stale pixels over a newer one.
|
|
43
|
+
* - A chunk invalidated again while it renders is rendered again afterwards.
|
|
44
|
+
*/
|
|
45
|
+
export declare function createTerrainScheduler(options: SchedulerOptions): TerrainScheduler;
|
|
46
|
+
//# sourceMappingURL=scheduler.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scheduler.d.ts","sourceRoot":"","sources":["../../src/terrain/scheduler.ts"],"names":[],"mappings":"AAAA,mCAAmC;AACnC,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,YAAa,SAAQ,SAAS;IAC7C,kCAAkC;IAClC,QAAQ,CAAC,MAAM,EAAE,iBAAiB,CAAC;CACpC;AAED,MAAM,WAAW,gBAAgB;IAC/B,6BAA6B;IAC7B,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;IACxD,mEAAmE;IACnE,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,gEAAgE;IAChE,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,+EAA+E;IAC/E,QAAQ,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,SAAS,KAAK,OAAO,CAAC,YAAY,CAAC,CAAC;IAC5D,uGAAuG;IACvG,QAAQ,CAAC,KAAK,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,CAAC;IAC9C,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,KAAK,IAAI,CAAC;CAC9D;AAED,MAAM,WAAW,gBAAgB;IAC/B,sFAAsF;IACtF,UAAU,CAAC,IAAI,EAAE,SAAS,GAAG,KAAK,GAAG,IAAI,CAAC;IAC1C,mFAAmF;IACnF,WAAW,CAAC,QAAQ,EAAE,SAAS,GAAG,IAAI,GAAG,IAAI,CAAC;IAC9C,oCAAoC;IACpC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,qDAAqD;IACrD,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1B,OAAO,IAAI,IAAI,CAAC;CACjB;AAKD;;;;;;;;GAQG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,gBAAgB,GAAG,gBAAgB,CAoIlF"}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
//#region src/terrain/scheduler.ts
|
|
2
|
+
var intersects = (a, b) => a.x < b.x + b.width && b.x < a.x + a.width && a.y < b.y + b.height && b.y < a.y + a.height;
|
|
3
|
+
/**
|
|
4
|
+
* Decides what to render, in what order, and applies it safely.
|
|
5
|
+
*
|
|
6
|
+
* - The map is cut into chunks; a change marks the chunks it touches with the smallest rectangle that covers it.
|
|
7
|
+
* - Dirty chunks are rendered in the viewport first, then by distance to its centre.
|
|
8
|
+
* - Several renders run at once (a pool of workers), and finish in any order, but a frame is applied only
|
|
9
|
+
* after every frame started before it: a slow, older frame can never put stale pixels over a newer one.
|
|
10
|
+
* - A chunk invalidated again while it renders is rendered again afterwards.
|
|
11
|
+
*/
|
|
12
|
+
function createTerrainScheduler(options) {
|
|
13
|
+
const chunk = options.chunk ?? 512;
|
|
14
|
+
const concurrency = Math.max(1, options.concurrency ?? 1);
|
|
15
|
+
const cols = Math.ceil(options.size[0] / chunk);
|
|
16
|
+
const rows = Math.ceil(options.size[1] / chunk);
|
|
17
|
+
const dirty = /* @__PURE__ */ new Map();
|
|
18
|
+
let viewport = null;
|
|
19
|
+
let started = 0;
|
|
20
|
+
let applied = 0;
|
|
21
|
+
let inFlight = 0;
|
|
22
|
+
let destroyed = false;
|
|
23
|
+
const finished = /* @__PURE__ */ new Map();
|
|
24
|
+
const idleWaiters = [];
|
|
25
|
+
const chunkRect = (index) => {
|
|
26
|
+
const col = index % cols;
|
|
27
|
+
const row = Math.floor(index / cols);
|
|
28
|
+
return {
|
|
29
|
+
x: col * chunk,
|
|
30
|
+
y: row * chunk,
|
|
31
|
+
width: Math.min(chunk, options.size[0] - col * chunk),
|
|
32
|
+
height: Math.min(chunk, options.size[1] - row * chunk)
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
const union = (a, b) => {
|
|
36
|
+
const x0 = Math.min(a.x, b.x);
|
|
37
|
+
const y0 = Math.min(a.y, b.y);
|
|
38
|
+
return {
|
|
39
|
+
x: x0,
|
|
40
|
+
y: y0,
|
|
41
|
+
width: Math.max(a.x + a.width, b.x + b.width) - x0,
|
|
42
|
+
height: Math.max(a.y + a.height, b.y + b.height) - y0
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
const settleIdle = () => {
|
|
46
|
+
if (dirty.size === 0 && inFlight === 0 && finished.size === 0) for (const resolve of idleWaiters.splice(0)) resolve();
|
|
47
|
+
};
|
|
48
|
+
const nextChunk = () => {
|
|
49
|
+
let best;
|
|
50
|
+
let bestScore = Infinity;
|
|
51
|
+
for (const [index, rect] of dirty) {
|
|
52
|
+
const area = chunkRect(index);
|
|
53
|
+
const cx = viewport ? viewport.x + viewport.width / 2 : options.size[0] / 2;
|
|
54
|
+
const cy = viewport ? viewport.y + viewport.height / 2 : options.size[1] / 2;
|
|
55
|
+
const distance = Math.hypot(area.x + area.width / 2 - cx, area.y + area.height / 2 - cy);
|
|
56
|
+
const score = (viewport && intersects(rect, viewport) ? 0 : 1e9) + distance;
|
|
57
|
+
if (score < bestScore) {
|
|
58
|
+
bestScore = score;
|
|
59
|
+
best = index;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return best;
|
|
63
|
+
};
|
|
64
|
+
const flush = () => {
|
|
65
|
+
while (finished.has(applied)) {
|
|
66
|
+
const done = finished.get(applied);
|
|
67
|
+
finished.delete(applied);
|
|
68
|
+
applied += 1;
|
|
69
|
+
if (destroyed) continue;
|
|
70
|
+
if (done.frame) try {
|
|
71
|
+
options.apply(done.frame);
|
|
72
|
+
} catch (error) {
|
|
73
|
+
options.onError?.(error, done.rect);
|
|
74
|
+
}
|
|
75
|
+
else options.onError?.(done.error, done.rect);
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
const pump = () => {
|
|
79
|
+
while (!destroyed && inFlight < concurrency) {
|
|
80
|
+
const index = nextChunk();
|
|
81
|
+
if (index === void 0) break;
|
|
82
|
+
const rect = dirty.get(index);
|
|
83
|
+
dirty.delete(index);
|
|
84
|
+
const seq = started;
|
|
85
|
+
started += 1;
|
|
86
|
+
inFlight += 1;
|
|
87
|
+
options.render(rect).then((frame) => finished.set(seq, {
|
|
88
|
+
frame,
|
|
89
|
+
rect
|
|
90
|
+
}), (error) => finished.set(seq, {
|
|
91
|
+
error,
|
|
92
|
+
rect
|
|
93
|
+
})).then(() => {
|
|
94
|
+
inFlight -= 1;
|
|
95
|
+
flush();
|
|
96
|
+
pump();
|
|
97
|
+
settleIdle();
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
settleIdle();
|
|
101
|
+
};
|
|
102
|
+
return {
|
|
103
|
+
invalidate(target) {
|
|
104
|
+
if (destroyed) return;
|
|
105
|
+
if (target === "all") for (let index = 0; index < cols * rows; index += 1) dirty.set(index, chunkRect(index));
|
|
106
|
+
else {
|
|
107
|
+
const x0 = Math.max(0, target.x);
|
|
108
|
+
const y0 = Math.max(0, target.y);
|
|
109
|
+
const x1 = Math.min(options.size[0], target.x + target.width);
|
|
110
|
+
const y1 = Math.min(options.size[1], target.y + target.height);
|
|
111
|
+
if (x1 <= x0 || y1 <= y0) return;
|
|
112
|
+
for (let row = Math.floor(y0 / chunk); row <= Math.floor((y1 - 1) / chunk); row += 1) for (let col = Math.floor(x0 / chunk); col <= Math.floor((x1 - 1) / chunk); col += 1) {
|
|
113
|
+
const area = chunkRect(row * cols + col);
|
|
114
|
+
const part = {
|
|
115
|
+
x: Math.max(x0, area.x),
|
|
116
|
+
y: Math.max(y0, area.y),
|
|
117
|
+
width: Math.min(x1, area.x + area.width) - Math.max(x0, area.x),
|
|
118
|
+
height: Math.min(y1, area.y + area.height) - Math.max(y0, area.y)
|
|
119
|
+
};
|
|
120
|
+
const index = row * cols + col;
|
|
121
|
+
const known = dirty.get(index);
|
|
122
|
+
dirty.set(index, known ? union(known, part) : part);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
pump();
|
|
126
|
+
},
|
|
127
|
+
setViewport(next) {
|
|
128
|
+
viewport = next;
|
|
129
|
+
},
|
|
130
|
+
get pending() {
|
|
131
|
+
return dirty.size + inFlight + finished.size;
|
|
132
|
+
},
|
|
133
|
+
whenIdle() {
|
|
134
|
+
if (dirty.size === 0 && inFlight === 0 && finished.size === 0) return Promise.resolve();
|
|
135
|
+
return new Promise((resolve) => idleWaiters.push(resolve));
|
|
136
|
+
},
|
|
137
|
+
destroy() {
|
|
138
|
+
destroyed = true;
|
|
139
|
+
dirty.clear();
|
|
140
|
+
finished.clear();
|
|
141
|
+
for (const resolve of idleWaiters.splice(0)) resolve();
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
//#endregion
|
|
146
|
+
export { createTerrainScheduler };
|
|
147
|
+
|
|
148
|
+
//# sourceMappingURL=scheduler.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scheduler.js","names":[],"sources":["../../src/terrain/scheduler.ts"],"sourcesContent":["/** A rectangle in world pixels. */\nexport interface WorldRect {\n readonly x: number;\n readonly y: number;\n readonly width: number;\n readonly height: number;\n}\n\nexport interface TerrainFrame extends WorldRect {\n /** RGBA, `width * height * 4`. */\n readonly pixels: Uint8ClampedArray;\n}\n\nexport interface SchedulerOptions {\n /** World size, in pixels. */\n readonly size: readonly [width: number, height: number];\n /** Side of the squares the map is rendered in. Defaults to 512. */\n readonly chunk?: number;\n /** How many renders may be in flight at once. Defaults to 1. */\n readonly concurrency?: number;\n /** Renders a rectangle. May resolve in any order relative to other renders. */\n readonly render: (rect: WorldRect) => Promise<TerrainFrame>;\n /** Puts a frame where it belongs. Called in the order the renders were started, never concurrently. */\n readonly apply: (frame: TerrainFrame) => void;\n readonly onError?: (error: unknown, rect: WorldRect) => void;\n}\n\nexport interface TerrainScheduler {\n /** Something changed in this rectangle (or everywhere): it will be rendered again. */\n invalidate(rect: WorldRect | \"all\"): void;\n /** What is looked at, so that it is rendered first. `null` means no preference. */\n setViewport(viewport: WorldRect | null): void;\n /** Renders in flight or waiting. */\n readonly pending: number;\n /** Resolves when nothing is waiting or in flight. */\n whenIdle(): Promise<void>;\n destroy(): void;\n}\n\nconst intersects = (a: WorldRect, b: WorldRect): boolean =>\n a.x < b.x + b.width && b.x < a.x + a.width && a.y < b.y + b.height && b.y < a.y + a.height;\n\n/**\n * Decides what to render, in what order, and applies it safely.\n *\n * - The map is cut into chunks; a change marks the chunks it touches with the smallest rectangle that covers it.\n * - Dirty chunks are rendered in the viewport first, then by distance to its centre.\n * - Several renders run at once (a pool of workers), and finish in any order, but a frame is applied only\n * after every frame started before it: a slow, older frame can never put stale pixels over a newer one.\n * - A chunk invalidated again while it renders is rendered again afterwards.\n */\nexport function createTerrainScheduler(options: SchedulerOptions): TerrainScheduler {\n const chunk = options.chunk ?? 512;\n const concurrency = Math.max(1, options.concurrency ?? 1);\n const cols = Math.ceil(options.size[0] / chunk);\n const rows = Math.ceil(options.size[1] / chunk);\n const dirty = new Map<number, WorldRect>();\n let viewport: WorldRect | null = null;\n let started = 0;\n let applied = 0;\n let inFlight = 0;\n let destroyed = false;\n const finished = new Map<number, { frame?: TerrainFrame; rect: WorldRect; error?: unknown }>();\n const idleWaiters: Array<() => void> = [];\n\n const chunkRect = (index: number): WorldRect => {\n const col = index % cols;\n const row = Math.floor(index / cols);\n return { x: col * chunk, y: row * chunk, width: Math.min(chunk, options.size[0] - col * chunk), height: Math.min(chunk, options.size[1] - row * chunk) };\n };\n\n const union = (a: WorldRect, b: WorldRect): WorldRect => {\n const x0 = Math.min(a.x, b.x);\n const y0 = Math.min(a.y, b.y);\n return { x: x0, y: y0, width: Math.max(a.x + a.width, b.x + b.width) - x0, height: Math.max(a.y + a.height, b.y + b.height) - y0 };\n };\n\n const settleIdle = (): void => {\n if (dirty.size === 0 && inFlight === 0 && finished.size === 0) for (const resolve of idleWaiters.splice(0)) resolve();\n };\n\n const nextChunk = (): number | undefined => {\n let best: number | undefined;\n let bestScore = Infinity;\n for (const [index, rect] of dirty) {\n const area = chunkRect(index);\n const cx = viewport ? viewport.x + viewport.width / 2 : (options.size[0] / 2);\n const cy = viewport ? viewport.y + viewport.height / 2 : (options.size[1] / 2);\n const distance = Math.hypot(area.x + area.width / 2 - cx, area.y + area.height / 2 - cy);\n // What is on screen comes first, whatever its distance; then the nearest.\n const score = (viewport && intersects(rect, viewport) ? 0 : 1e9) + distance;\n if (score < bestScore) {\n bestScore = score;\n best = index;\n }\n }\n return best;\n };\n\n const flush = (): void => {\n while (finished.has(applied)) {\n const done = finished.get(applied)!;\n finished.delete(applied);\n applied += 1;\n if (destroyed) continue;\n if (done.frame) {\n // A frame that cannot be put on screen must not stop the ones that follow.\n try {\n options.apply(done.frame);\n } catch (error) {\n options.onError?.(error, done.rect);\n }\n } else options.onError?.(done.error, done.rect);\n }\n };\n\n const pump = (): void => {\n while (!destroyed && inFlight < concurrency) {\n const index = nextChunk();\n if (index === undefined) break;\n const rect = dirty.get(index)!;\n dirty.delete(index);\n const seq = started;\n started += 1;\n inFlight += 1;\n options.render(rect).then(\n (frame) => finished.set(seq, { frame, rect }),\n (error: unknown) => finished.set(seq, { error, rect }),\n ).then(() => {\n inFlight -= 1;\n flush();\n pump();\n settleIdle();\n });\n }\n settleIdle();\n };\n\n return {\n invalidate(target) {\n if (destroyed) return;\n if (target === \"all\") {\n for (let index = 0; index < cols * rows; index += 1) dirty.set(index, chunkRect(index));\n } else {\n const x0 = Math.max(0, target.x);\n const y0 = Math.max(0, target.y);\n const x1 = Math.min(options.size[0], target.x + target.width);\n const y1 = Math.min(options.size[1], target.y + target.height);\n if (x1 <= x0 || y1 <= y0) return;\n for (let row = Math.floor(y0 / chunk); row <= Math.floor((y1 - 1) / chunk); row += 1) {\n for (let col = Math.floor(x0 / chunk); col <= Math.floor((x1 - 1) / chunk); col += 1) {\n const area = chunkRect(row * cols + col);\n const part: WorldRect = {\n x: Math.max(x0, area.x),\n y: Math.max(y0, area.y),\n width: Math.min(x1, area.x + area.width) - Math.max(x0, area.x),\n height: Math.min(y1, area.y + area.height) - Math.max(y0, area.y),\n };\n const index = row * cols + col;\n const known = dirty.get(index);\n dirty.set(index, known ? union(known, part) : part);\n }\n }\n }\n pump();\n },\n setViewport(next) {\n viewport = next;\n },\n get pending() {\n return dirty.size + inFlight + finished.size;\n },\n whenIdle() {\n if (dirty.size === 0 && inFlight === 0 && finished.size === 0) return Promise.resolve();\n return new Promise<void>((resolve) => idleWaiters.push(resolve));\n },\n destroy() {\n destroyed = true;\n dirty.clear();\n finished.clear();\n for (const resolve of idleWaiters.splice(0)) resolve();\n },\n };\n}\n"],"mappings":"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"}
|