@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":"editing.js","names":[],"sources":["../../src/terrain/editing.ts"],"sourcesContent":["import type { Unsubscribe } from \"../model/result\";\nimport type { InteractionController, Tool } from \"../interaction/controller\";\nimport type { PointerInput } from \"../interaction/input\";\nimport type { IntentDefinition } from \"../interaction/intents\";\nimport type { PipelineStep } from \"../interaction/pipeline\";\nimport { tryReadTerrain } from \"./data\";\nimport { covers, readRect, shapeBounds } from \"./mask\";\nimport { MAX_PAINT_COST, paintCost } from \"./patches\";\nimport { NO_MATERIAL, type TerrainShape } from \"./types\";\n\ndeclare module \"../interaction/intents\" {\n interface IntentRegistry {\n /** Paint a shape of the terrain with a material, or erase it with `null`. */\n \"terrain.paint\": { readonly shape: TerrainShape; readonly material: string | null };\n }\n}\n\n/** The `terrain.paint` intent: one patch, after the rules of the pipeline. */\nexport const terrainPaintIntent: IntentDefinition<\"terrain.paint\"> = {\n resolve(intent, { state }) {\n if (!tryReadTerrain(state)) return { ok: false, reason: \"no-terrain\", message: \"This map has no terrain\" };\n return { type: \"terrain.paint\", shape: intent.shape, material: intent.material };\n },\n};\n\n/** Registers the intent, unless a feature already did. */\nexport function registerTerrainIntents(controller: InteractionController): Unsubscribe {\n return controller.hasIntent(\"terrain.paint\") ? () => undefined : controller.registerIntent(\"terrain.paint\", terrainPaintIntent);\n}\n\n/**\n * Only paint over certain materials: `requireTerrain([\"grass\", \"dirt\"])` lets a game till grass and nothing else.\n * Checks every pixel the shape would cover. Pixels outside the map count as wrong ground.\n */\nexport function requireTerrain(materials: readonly string[]): PipelineStep<\"terrain.paint\"> {\n return (intent, context, next) => {\n const terrain = tryReadTerrain(context.state);\n if (!terrain) return context.reject(\"no-terrain\", \"This map has no terrain\");\n const allowed = new Set(materials.map((id) => terrain.materials.findIndex((material) => material.id === id)).filter((index) => index >= 0));\n const bounds = shapeBounds(terrain.mask, intent.shape);\n if (!bounds) return context.reject(\"outside-map\", \"Nothing of this lies on the map\");\n // The check below tests every pixel against the shape: bound it as the patch does.\n if (paintCost(terrain.mask, intent.shape) > MAX_PAINT_COST) return context.reject(\"too-large\", \"This is too large to paint at once\");\n // One read of the whole region: reading pixel by pixel would decode a chunk for each of them.\n const values = readRect(terrain.mask, bounds);\n for (let y = bounds.y; y < bounds.y + bounds.height; y += 1) {\n for (let x = bounds.x; x < bounds.x + bounds.width; x += 1) {\n if (!covers(terrain.mask, intent.shape, x, y)) continue;\n const value = values[(y - bounds.y) * bounds.width + (x - bounds.x)]!;\n if (value === NO_MATERIAL || !allowed.has(value)) {\n return context.reject(\"wrong-ground\", `Can only be painted over ${materials.join(\", \")}`);\n }\n }\n }\n return next();\n };\n}\n\n/** Refuses shapes that reach outside the map. Without it they are clipped. */\nexport function withinTerrain(): PipelineStep<\"terrain.paint\"> {\n return (intent, context, next) => {\n const terrain = tryReadTerrain(context.state);\n if (!terrain) return context.reject(\"no-terrain\", \"This map has no terrain\");\n const shape = intent.shape;\n const width = terrain.mask.width * terrain.mask.scale;\n const height = terrain.mask.height * terrain.mask.scale;\n const box = shape.type === \"fill\" ? { x0: 0, y0: 0, x1: width, y1: height } : shape.type === \"circle\" ? { x0: shape.x - shape.radius, y0: shape.y - shape.radius, x1: shape.x + shape.radius, y1: shape.y + shape.radius }\n : shape.type === \"rect\" ? { x0: shape.x, y0: shape.y, x1: shape.x + shape.width, y1: shape.y + shape.height }\n : { x0: Math.min(...shape.points.map((p) => p[0])) - shape.radius, y0: Math.min(...shape.points.map((p) => p[1])) - shape.radius, x1: Math.max(...shape.points.map((p) => p[0])) + shape.radius, y1: Math.max(...shape.points.map((p) => p[1])) + shape.radius };\n if (box.x0 < 0 || box.y0 < 0 || box.x1 > width || box.y1 > height) return context.reject(\"outside-map\", \"This reaches outside the map\");\n return next();\n };\n}\n\n/** Keeps a brush from being bigger than a game wants. */\nexport function maxRadius(limit: number): PipelineStep<\"terrain.paint\"> {\n return (intent, _context, next) => {\n const shape = intent.shape;\n if ((shape.type === \"circle\" || shape.type === \"stroke\") && shape.radius > limit) return next({ ...intent, shape: { ...shape, radius: limit } });\n return next();\n };\n}\n\nexport interface BrushOptions {\n /** Material to paint, or `null` to erase. */\n readonly material: string | null;\n /** Radius in world pixels. */\n readonly radius: number;\n /** Distance between two points of a stroke, in world pixels. Defaults to a third of the radius. */\n readonly spacing?: number;\n}\n\nexport interface BrushApi {\n /** Starts painting with the pointer: pressing paints, dragging paints a line, releasing ends the stroke. */\n start(options: BrushOptions): void;\n stop(): void;\n readonly active: BrushOptions | null;\n}\n\nexport interface BrushTool {\n readonly tool: Tool;\n readonly api: BrushApi;\n}\n\nlet strokes = 0;\n\n/**\n * The tool that paints. Each press-drag-release is one undo step, however many patches it made. It only\n * submits intents: whatever rules the pipeline has, and wherever the model commits (here or on an authority),\n * apply as for anything else.\n */\nexport function terrainBrush(options: { readonly modes?: readonly string[] } = {}): BrushTool {\n let brush: BrushOptions | null = null;\n let last: { x: number; y: number } | null = null;\n let key = \"\";\n\n const paintAt = (controller: InteractionController, shape: TerrainShape): void => {\n if (!brush) return;\n controller.submit({ type: \"terrain.paint\", shape, material: brush.material }, { coalesce: key });\n };\n\n const end = (): void => {\n last = null;\n key = \"\";\n };\n\n const tool: Tool = {\n id: \"terrain-brush\",\n modes: options.modes ?? [\"edit\"],\n // Painting comes before selecting: while a brush is in hand, a press paints.\n priority: 90,\n pointerDown(input: PointerInput, controller) {\n if (!brush || input.button !== 0) return false;\n strokes += 1;\n key = `terrain-stroke-${strokes}`;\n last = { x: input.x, y: input.y };\n paintAt(controller, { type: \"circle\", x: input.x, y: input.y, radius: brush.radius });\n return true;\n },\n pointerMove(input: PointerInput, controller) {\n if (!brush || !last) return false;\n const spacing = brush.spacing ?? Math.max(1, brush.radius / 3);\n if (Math.hypot(input.x - last.x, input.y - last.y) < spacing) return true;\n paintAt(controller, { type: \"stroke\", points: [[last.x, last.y], [input.x, input.y]], radius: brush.radius });\n last = { x: input.x, y: input.y };\n return true;\n },\n pointerUp: end,\n cancel: end,\n };\n\n return {\n tool,\n api: {\n start(next) {\n brush = next;\n },\n stop() {\n brush = null;\n end();\n },\n get active() {\n return brush;\n },\n },\n };\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export * from './types';
|
|
2
|
+
export * from './rle';
|
|
3
|
+
export * from './mask';
|
|
4
|
+
export * from './data';
|
|
5
|
+
export * from './patches';
|
|
6
|
+
export * from './define';
|
|
7
|
+
export * from './scheduler';
|
|
8
|
+
export * from './kernel';
|
|
9
|
+
export * from './editing';
|
|
10
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/terrain/index.ts"],"names":[],"mappings":"AAAA,cAAc,SAAS,CAAC;AACxB,cAAc,OAAO,CAAC;AACtB,cAAc,QAAQ,CAAC;AACvB,cAAc,QAAQ,CAAC;AACvB,cAAc,WAAW,CAAC;AAC1B,cAAc,UAAU,CAAC;AACzB,cAAc,aAAa,CAAC;AAC5B,cAAc,UAAU,CAAC;AACzB,cAAc,WAAW,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { NO_MATERIAL, TERRAIN_DATA_VERSION, TERRAIN_EXTENSION_ID } from "./types.js";
|
|
2
|
+
import { readTerrain, terrainIssues, tryReadTerrain } from "./data.js";
|
|
3
|
+
import { decodeRuns, encodeRuns } from "./rle.js";
|
|
4
|
+
import { chunkCount, clipRect, covers, createMask, paintShape, readAll, readChunk, readPixel, readRect, shapeBounds, writeRect } from "./mask.js";
|
|
5
|
+
import { blendRadius, controlPixels, dirtyOfPatch, globalOverlayMaterials, growRect, overlayMaterials, renderMargin, toKernelDocument, widestBlend, writeControl } from "./kernel.js";
|
|
6
|
+
import { createTerrainScheduler } from "./scheduler.js";
|
|
7
|
+
import { MAX_PAINT_COST, paintCost, terrainPatchHandlers } from "./patches.js";
|
|
8
|
+
import { maxRadius, registerTerrainIntents, requireTerrain, terrainBrush, terrainPaintIntent, withinTerrain } from "./editing.js";
|
|
9
|
+
import { defineTerrain, paint } from "./define.js";
|
|
10
|
+
export { MAX_PAINT_COST, NO_MATERIAL, TERRAIN_DATA_VERSION, TERRAIN_EXTENSION_ID, blendRadius, chunkCount, clipRect, controlPixels, covers, createMask, createTerrainScheduler, decodeRuns, defineTerrain, dirtyOfPatch, encodeRuns, globalOverlayMaterials, growRect, maxRadius, overlayMaterials, paint, paintCost, paintShape, readAll, readChunk, readPixel, readRect, readTerrain, registerTerrainIntents, renderMargin, requireTerrain, shapeBounds, terrainBrush, terrainIssues, terrainPaintIntent, terrainPatchHandlers, toKernelDocument, tryReadTerrain, widestBlend, withinTerrain, writeControl, writeRect };
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { RasterImage, TerrainMapDocument } from '@rpgjs/render-map2d';
|
|
2
|
+
import { MapPatch } from '../model/patches';
|
|
3
|
+
import { WorldRect } from './scheduler';
|
|
4
|
+
import { MaskRect, TerrainData, TerrainMode } from './types';
|
|
5
|
+
/**
|
|
6
|
+
* What the terrain feature feeds the kernel (`@rpgjs/render-map2d`) with. Types only: this file
|
|
7
|
+
* never imports the kernel at runtime, so the data model stays usable without it.
|
|
8
|
+
*/
|
|
9
|
+
/** Where a material's texture comes from: the decoded image, and the part of it to use. */
|
|
10
|
+
export interface MaterialSource {
|
|
11
|
+
readonly src: string;
|
|
12
|
+
readonly frame?: readonly [x: number, y: number, width: number, height: number];
|
|
13
|
+
}
|
|
14
|
+
/** A message-friendly description of the terrain to render. */
|
|
15
|
+
export interface TerrainRenderInit {
|
|
16
|
+
readonly data: TerrainData;
|
|
17
|
+
/** World size, in pixels. */
|
|
18
|
+
readonly size: readonly [width: number, height: number];
|
|
19
|
+
readonly tileSize: number;
|
|
20
|
+
/** By material id. */
|
|
21
|
+
readonly sources: {
|
|
22
|
+
readonly [materialId: string]: MaterialSource;
|
|
23
|
+
};
|
|
24
|
+
/** Decoded images, by `src`. */
|
|
25
|
+
readonly images: {
|
|
26
|
+
readonly [src: string]: RasterImage;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
/** How far a material's mode blends into its surroundings, in pixels. */
|
|
30
|
+
export declare function blendRadius(mode: TerrainMode | undefined): number;
|
|
31
|
+
/**
|
|
32
|
+
* How far a changed mask changes pixels: the widest blend of the materials and transitions, or the reach
|
|
33
|
+
* of an overlay (nine-slice, local presets), plus a few pixels for the tails of the soft mask.
|
|
34
|
+
*/
|
|
35
|
+
export declare function widestBlend(data: TerrainData): number;
|
|
36
|
+
/**
|
|
37
|
+
* The margin to give the kernel around a region so that it gets the pixels of a larger render: the widest
|
|
38
|
+
* blend, or how far a hole or a wall reaches (its height or depth) if that is more.
|
|
39
|
+
*/
|
|
40
|
+
export declare function renderMargin(data: TerrainData): number;
|
|
41
|
+
/**
|
|
42
|
+
* Materials drawn as an overlay of the whole mask: the kernel caches it, so a change of the mask has to drop
|
|
43
|
+
* that cache (the render host does).
|
|
44
|
+
*/
|
|
45
|
+
export declare function overlayMaterials(data: TerrainData): readonly number[];
|
|
46
|
+
/** Those whose pixels may change anywhere when the mask changes under them: everything is rendered again. */
|
|
47
|
+
export declare function globalOverlayMaterials(data: TerrainData): readonly number[];
|
|
48
|
+
/** The mask as the kernel's control image: material index in red and green, opaque where something is painted. */
|
|
49
|
+
export declare function controlPixels(data: TerrainData): Uint8ClampedArray;
|
|
50
|
+
/** Writes values over a rectangle of a control image (what `terrain.paint` and `terrain.region` change). */
|
|
51
|
+
export declare function writeControl(pixels: Uint8ClampedArray, maskWidth: number, rect: MaskRect, values: ArrayLike<number>): void;
|
|
52
|
+
/** The document the kernel renders. `controlPixels` is the control image, kept up to date by the caller. */
|
|
53
|
+
export declare function toKernelDocument(init: TerrainRenderInit, control: Uint8ClampedArray): TerrainMapDocument;
|
|
54
|
+
/** What to render again after a patch, in world pixels. `"all"` when the patch can change anything. */
|
|
55
|
+
export declare function dirtyOfPatch(patch: MapPatch, data: TerrainData, grow: number): WorldRect | "all" | "none";
|
|
56
|
+
export declare const growRect: (rect: WorldRect, by: number) => WorldRect;
|
|
57
|
+
//# sourceMappingURL=kernel.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"kernel.d.ts","sourceRoot":"","sources":["../../src/terrain/kernel.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,kBAAkB,EAAqB,MAAM,qBAAqB,CAAC;AAC9F,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAEjD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAC7C,OAAO,EAAe,KAAK,QAAQ,EAAE,KAAK,WAAW,EAAE,KAAK,WAAW,EAAE,MAAM,SAAS,CAAC;AAEzF;;;GAGG;AAEH,2FAA2F;AAC3F,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,KAAK,CAAC,EAAE,SAAS,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;CACjF;AAED,+DAA+D;AAC/D,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC;IAC3B,6BAA6B;IAC7B,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;IACxD,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,sBAAsB;IACtB,QAAQ,CAAC,OAAO,EAAE;QAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,GAAG,cAAc,CAAA;KAAE,CAAC;IACpE,gCAAgC;IAChC,QAAQ,CAAC,MAAM,EAAE;QAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,GAAG,WAAW,CAAA;KAAE,CAAC;CAC1D;AAKD,yEAAyE;AACzE,wBAAgB,WAAW,CAAC,IAAI,EAAE,WAAW,GAAG,SAAS,GAAG,MAAM,CAejE;AAeD;;;GAGG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,WAAW,GAAG,MAAM,CAQrD;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,WAAW,GAAG,MAAM,CAOtD;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,WAAW,GAAG,SAAS,MAAM,EAAE,CAMrE;AAED,6GAA6G;AAC7G,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,WAAW,GAAG,SAAS,MAAM,EAAE,CAM3E;AAED,kHAAkH;AAClH,wBAAgB,aAAa,CAAC,IAAI,EAAE,WAAW,GAAG,iBAAiB,CAUlE;AAED,4GAA4G;AAC5G,wBAAgB,YAAY,CAAC,MAAM,EAAE,iBAAiB,EAAE,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,CAAC,MAAM,CAAC,GAAG,IAAI,CAW1H;AAID,4GAA4G;AAC5G,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,iBAAiB,EAAE,OAAO,EAAE,iBAAiB,GAAG,kBAAkB,CA+BxG;AAED,uGAAuG;AACvG,wBAAgB,YAAY,CAAC,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,KAAK,GAAG,MAAM,CAgBzG;AAED,eAAO,MAAM,QAAQ,GAAI,MAAM,SAAS,EAAE,IAAI,MAAM,KAAG,SAA2G,CAAC"}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import "./types.js";
|
|
2
|
+
import { readAll, shapeBounds } from "./mask.js";
|
|
3
|
+
//#region src/terrain/kernel.ts
|
|
4
|
+
var DEFAULT_BLEND = 18;
|
|
5
|
+
var WATER_BLEND = 16;
|
|
6
|
+
/** How far a material's mode blends into its surroundings, in pixels. */
|
|
7
|
+
function blendRadius(mode) {
|
|
8
|
+
if (!mode) return DEFAULT_BLEND;
|
|
9
|
+
switch (mode.type) {
|
|
10
|
+
case "hard":
|
|
11
|
+
case "nine-slice": return 0;
|
|
12
|
+
case "fade": return typeof mode.width === "number" && mode.width > 0 ? mode.width : DEFAULT_BLEND;
|
|
13
|
+
case "water": return mode.border === false ? 8 : WATER_BLEND;
|
|
14
|
+
case "custom": {
|
|
15
|
+
const width = Number(mode.params?.blendRadius ?? mode.params?.width);
|
|
16
|
+
return Number.isFinite(width) && width >= 0 ? width : DEFAULT_BLEND;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/** Presets of the kernel whose pixels depend only on what is near: a change shows up within this many pixels. */
|
|
21
|
+
var LOCAL_OVERLAY_REACH = 32;
|
|
22
|
+
var LOCAL_PRESETS = /* @__PURE__ */ new Set([
|
|
23
|
+
"grass-edge",
|
|
24
|
+
"carpet",
|
|
25
|
+
"carpet-border",
|
|
26
|
+
"road-town-sidewalk",
|
|
27
|
+
"water"
|
|
28
|
+
]);
|
|
29
|
+
/** Whether a material is drawn as an overlay computed on the whole mask (nine-slice and presets). */
|
|
30
|
+
var isOverlay = (mode) => mode?.type === "nine-slice" || mode?.type === "custom";
|
|
31
|
+
/**
|
|
32
|
+
* Whether a material's overlay may change far from an edit: presets that are not known to be local
|
|
33
|
+
* (lane marks follow the whole road, a game's own preset is unknown) count as global.
|
|
34
|
+
*/
|
|
35
|
+
var isGlobalOverlay = (mode) => mode?.type === "custom" && !LOCAL_PRESETS.has(mode.shaderKey);
|
|
36
|
+
/**
|
|
37
|
+
* How far a changed mask changes pixels: the widest blend of the materials and transitions, or the reach
|
|
38
|
+
* of an overlay (nine-slice, local presets), plus a few pixels for the tails of the soft mask.
|
|
39
|
+
*/
|
|
40
|
+
function widestBlend(data) {
|
|
41
|
+
let widest = 0;
|
|
42
|
+
for (const material of data.materials) {
|
|
43
|
+
widest = Math.max(widest, blendRadius(material.mode));
|
|
44
|
+
if (isOverlay(material.mode) && !isGlobalOverlay(material.mode)) widest = Math.max(widest, LOCAL_OVERLAY_REACH);
|
|
45
|
+
}
|
|
46
|
+
for (const transition of data.transitions ?? []) widest = Math.max(widest, blendRadius(transition.mode));
|
|
47
|
+
return Math.ceil(widest) + 8;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* The margin to give the kernel around a region so that it gets the pixels of a larger render: the widest
|
|
51
|
+
* blend, or how far a hole or a wall reaches (its height or depth) if that is more.
|
|
52
|
+
*/
|
|
53
|
+
function renderMargin(data) {
|
|
54
|
+
let reach = 0;
|
|
55
|
+
for (const feature of data.morphology ?? []) {
|
|
56
|
+
const params = feature.params ?? {};
|
|
57
|
+
reach = Math.max(reach, Number(params.height ?? params.depth) || 0);
|
|
58
|
+
}
|
|
59
|
+
return Math.max(widestBlend(data), Math.ceil(reach) + 8);
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Materials drawn as an overlay of the whole mask: the kernel caches it, so a change of the mask has to drop
|
|
63
|
+
* that cache (the render host does).
|
|
64
|
+
*/
|
|
65
|
+
function overlayMaterials(data) {
|
|
66
|
+
const indexes = [];
|
|
67
|
+
data.materials.forEach((material, index) => {
|
|
68
|
+
if (isOverlay(material.mode)) indexes.push(index);
|
|
69
|
+
});
|
|
70
|
+
return indexes;
|
|
71
|
+
}
|
|
72
|
+
/** Those whose pixels may change anywhere when the mask changes under them: everything is rendered again. */
|
|
73
|
+
function globalOverlayMaterials(data) {
|
|
74
|
+
const indexes = [];
|
|
75
|
+
data.materials.forEach((material, index) => {
|
|
76
|
+
if (isGlobalOverlay(material.mode)) indexes.push(index);
|
|
77
|
+
});
|
|
78
|
+
return indexes;
|
|
79
|
+
}
|
|
80
|
+
/** The mask as the kernel's control image: material index in red and green, opaque where something is painted. */
|
|
81
|
+
function controlPixels(data) {
|
|
82
|
+
const values = readAll(data.mask);
|
|
83
|
+
const pixels = new Uint8ClampedArray(values.length * 4);
|
|
84
|
+
for (let i = 0; i < values.length; i += 1) {
|
|
85
|
+
const value = values[i];
|
|
86
|
+
pixels[i * 4] = value & 255;
|
|
87
|
+
pixels[i * 4 + 1] = value >> 8;
|
|
88
|
+
pixels[i * 4 + 3] = value === 65535 ? 0 : 255;
|
|
89
|
+
}
|
|
90
|
+
return pixels;
|
|
91
|
+
}
|
|
92
|
+
/** Writes values over a rectangle of a control image (what `terrain.paint` and `terrain.region` change). */
|
|
93
|
+
function writeControl(pixels, maskWidth, rect, values) {
|
|
94
|
+
for (let y = 0; y < rect.height; y += 1) for (let x = 0; x < rect.width; x += 1) {
|
|
95
|
+
const value = values[y * rect.width + x];
|
|
96
|
+
const offset = ((rect.y + y) * maskWidth + rect.x + x) * 4;
|
|
97
|
+
pixels[offset] = value & 255;
|
|
98
|
+
pixels[offset + 1] = value >> 8;
|
|
99
|
+
pixels[offset + 2] = 0;
|
|
100
|
+
pixels[offset + 3] = value === 65535 ? 0 : 255;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
var toKernelMode = (mode) => mode;
|
|
104
|
+
/** The document the kernel renders. `controlPixels` is the control image, kept up to date by the caller. */
|
|
105
|
+
function toKernelDocument(init, control) {
|
|
106
|
+
const { data } = init;
|
|
107
|
+
return {
|
|
108
|
+
version: 1,
|
|
109
|
+
width: init.size[0],
|
|
110
|
+
height: init.size[1],
|
|
111
|
+
tileSize: init.tileSize,
|
|
112
|
+
textureGrid: {
|
|
113
|
+
columns: 1,
|
|
114
|
+
rows: 1,
|
|
115
|
+
tileSize: 1
|
|
116
|
+
},
|
|
117
|
+
textures: data.materials.map((material, index) => {
|
|
118
|
+
const source = init.sources[material.id];
|
|
119
|
+
const image = source ? init.images[source.src] : void 0;
|
|
120
|
+
const frame = source?.frame ?? (image ? [
|
|
121
|
+
0,
|
|
122
|
+
0,
|
|
123
|
+
image.width,
|
|
124
|
+
image.height
|
|
125
|
+
] : void 0);
|
|
126
|
+
return {
|
|
127
|
+
id: material.id,
|
|
128
|
+
index,
|
|
129
|
+
label: material.id,
|
|
130
|
+
...source ? { source: source.src } : {},
|
|
131
|
+
...frame ? { sourceRect: {
|
|
132
|
+
x: frame[0],
|
|
133
|
+
y: frame[1],
|
|
134
|
+
width: frame[2],
|
|
135
|
+
height: frame[3]
|
|
136
|
+
} } : {},
|
|
137
|
+
...material.priority !== void 0 ? { priority: material.priority } : { priority: index },
|
|
138
|
+
...material.collision !== void 0 ? { collision: material.collision } : {},
|
|
139
|
+
...material.specialType !== void 0 ? { specialType: material.specialType } : {},
|
|
140
|
+
...material.renderTileSize !== void 0 ? { renderTileSize: material.renderTileSize } : {},
|
|
141
|
+
...material.renderingStyle !== void 0 ? { renderingStyle: material.renderingStyle } : {},
|
|
142
|
+
...toKernelMode(material.mode) ? { defaultRenderMode: toKernelMode(material.mode) } : {}
|
|
143
|
+
};
|
|
144
|
+
}),
|
|
145
|
+
transitions: (data.transitions ?? []).map((rule) => ({
|
|
146
|
+
from: rule.from,
|
|
147
|
+
to: rule.to,
|
|
148
|
+
mode: toKernelMode(rule.mode),
|
|
149
|
+
...rule.priority !== void 0 ? { priority: rule.priority } : {}
|
|
150
|
+
})),
|
|
151
|
+
controlTexture: {
|
|
152
|
+
width: data.mask.width,
|
|
153
|
+
height: data.mask.height,
|
|
154
|
+
palette: data.materials.map((m) => m.id),
|
|
155
|
+
pixels: control
|
|
156
|
+
},
|
|
157
|
+
morphology: data.morphology ?? [],
|
|
158
|
+
waterAnimation: data.water ?? {
|
|
159
|
+
enabled: false,
|
|
160
|
+
speed: 1,
|
|
161
|
+
intensity: 1,
|
|
162
|
+
direction: 0
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
/** What to render again after a patch, in world pixels. `"all"` when the patch can change anything. */
|
|
167
|
+
function dirtyOfPatch(patch, data, grow) {
|
|
168
|
+
const scale = data.mask.scale;
|
|
169
|
+
const fromMask = (rect) => rect ? growRect({
|
|
170
|
+
x: rect.x * scale,
|
|
171
|
+
y: rect.y * scale,
|
|
172
|
+
width: rect.width * scale,
|
|
173
|
+
height: rect.height * scale
|
|
174
|
+
}, grow) : "none";
|
|
175
|
+
switch (patch.type) {
|
|
176
|
+
case "terrain.paint": return fromMask(shapeBounds(data.mask, patch.shape));
|
|
177
|
+
case "terrain.region": return fromMask({
|
|
178
|
+
x: patch.x,
|
|
179
|
+
y: patch.y,
|
|
180
|
+
width: patch.width,
|
|
181
|
+
height: patch.height
|
|
182
|
+
});
|
|
183
|
+
case "terrain.materials":
|
|
184
|
+
case "terrain.morphology":
|
|
185
|
+
case "extension.set": return "all";
|
|
186
|
+
default: return "none";
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
var growRect = (rect, by) => ({
|
|
190
|
+
x: rect.x - by,
|
|
191
|
+
y: rect.y - by,
|
|
192
|
+
width: rect.width + by * 2,
|
|
193
|
+
height: rect.height + by * 2
|
|
194
|
+
});
|
|
195
|
+
//#endregion
|
|
196
|
+
export { blendRadius, controlPixels, dirtyOfPatch, globalOverlayMaterials, growRect, overlayMaterials, renderMargin, toKernelDocument, widestBlend, writeControl };
|
|
197
|
+
|
|
198
|
+
//# sourceMappingURL=kernel.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"kernel.js","names":[],"sources":["../../src/terrain/kernel.ts"],"sourcesContent":["import type { RasterImage, TerrainMapDocument, TerrainRenderMode } from \"@rpgjs/render-map2d\";\nimport type { MapPatch } from \"../model/patches\";\nimport { readAll, shapeBounds } from \"./mask\";\nimport type { WorldRect } from \"./scheduler\";\nimport { NO_MATERIAL, type MaskRect, type TerrainData, type TerrainMode } from \"./types\";\n\n/**\n * What the terrain feature feeds the kernel (`@rpgjs/render-map2d`) with. Types only: this file\n * never imports the kernel at runtime, so the data model stays usable without it.\n */\n\n/** Where a material's texture comes from: the decoded image, and the part of it to use. */\nexport interface MaterialSource {\n readonly src: string;\n readonly frame?: readonly [x: number, y: number, width: number, height: number];\n}\n\n/** A message-friendly description of the terrain to render. */\nexport interface TerrainRenderInit {\n readonly data: TerrainData;\n /** World size, in pixels. */\n readonly size: readonly [width: number, height: number];\n readonly tileSize: number;\n /** By material id. */\n readonly sources: { readonly [materialId: string]: MaterialSource };\n /** Decoded images, by `src`. */\n readonly images: { readonly [src: string]: RasterImage };\n}\n\nconst DEFAULT_BLEND = 18;\nconst WATER_BLEND = 16;\n\n/** How far a material's mode blends into its surroundings, in pixels. */\nexport function blendRadius(mode: TerrainMode | undefined): number {\n if (!mode) return DEFAULT_BLEND;\n switch (mode.type) {\n case \"hard\":\n case \"nine-slice\":\n return 0;\n case \"fade\":\n return typeof mode.width === \"number\" && mode.width > 0 ? mode.width : DEFAULT_BLEND;\n case \"water\":\n return mode.border === false ? 8 : WATER_BLEND;\n case \"custom\": {\n const width = Number(mode.params?.blendRadius ?? mode.params?.width);\n return Number.isFinite(width) && width >= 0 ? width : DEFAULT_BLEND;\n }\n }\n}\n\n/** Presets of the kernel whose pixels depend only on what is near: a change shows up within this many pixels. */\nconst LOCAL_OVERLAY_REACH = 32;\nconst LOCAL_PRESETS = new Set([\"grass-edge\", \"carpet\", \"carpet-border\", \"road-town-sidewalk\", \"water\"]);\n\n/** Whether a material is drawn as an overlay computed on the whole mask (nine-slice and presets). */\nconst isOverlay = (mode: TerrainMode | undefined): boolean => mode?.type === \"nine-slice\" || mode?.type === \"custom\";\n\n/**\n * Whether a material's overlay may change far from an edit: presets that are not known to be local\n * (lane marks follow the whole road, a game's own preset is unknown) count as global.\n */\nconst isGlobalOverlay = (mode: TerrainMode | undefined): boolean => mode?.type === \"custom\" && !LOCAL_PRESETS.has(mode.shaderKey);\n\n/**\n * How far a changed mask changes pixels: the widest blend of the materials and transitions, or the reach\n * of an overlay (nine-slice, local presets), plus a few pixels for the tails of the soft mask.\n */\nexport function widestBlend(data: TerrainData): number {\n let widest = 0;\n for (const material of data.materials) {\n widest = Math.max(widest, blendRadius(material.mode));\n if (isOverlay(material.mode) && !isGlobalOverlay(material.mode)) widest = Math.max(widest, LOCAL_OVERLAY_REACH);\n }\n for (const transition of data.transitions ?? []) widest = Math.max(widest, blendRadius(transition.mode));\n return Math.ceil(widest) + 8;\n}\n\n/**\n * The margin to give the kernel around a region so that it gets the pixels of a larger render: the widest\n * blend, or how far a hole or a wall reaches (its height or depth) if that is more.\n */\nexport function renderMargin(data: TerrainData): number {\n let reach = 0;\n for (const feature of data.morphology ?? []) {\n const params = (feature.params ?? {}) as { height?: unknown; depth?: unknown };\n reach = Math.max(reach, Number(params.height ?? params.depth) || 0);\n }\n return Math.max(widestBlend(data), Math.ceil(reach) + 8);\n}\n\n/**\n * Materials drawn as an overlay of the whole mask: the kernel caches it, so a change of the mask has to drop\n * that cache (the render host does).\n */\nexport function overlayMaterials(data: TerrainData): readonly number[] {\n const indexes: number[] = [];\n data.materials.forEach((material, index) => {\n if (isOverlay(material.mode)) indexes.push(index);\n });\n return indexes;\n}\n\n/** Those whose pixels may change anywhere when the mask changes under them: everything is rendered again. */\nexport function globalOverlayMaterials(data: TerrainData): readonly number[] {\n const indexes: number[] = [];\n data.materials.forEach((material, index) => {\n if (isGlobalOverlay(material.mode)) indexes.push(index);\n });\n return indexes;\n}\n\n/** The mask as the kernel's control image: material index in red and green, opaque where something is painted. */\nexport function controlPixels(data: TerrainData): Uint8ClampedArray {\n const values = readAll(data.mask);\n const pixels = new Uint8ClampedArray(values.length * 4);\n for (let i = 0; i < values.length; i += 1) {\n const value = values[i]!;\n pixels[i * 4] = value & 255;\n pixels[i * 4 + 1] = value >> 8;\n pixels[i * 4 + 3] = value === NO_MATERIAL ? 0 : 255;\n }\n return pixels;\n}\n\n/** Writes values over a rectangle of a control image (what `terrain.paint` and `terrain.region` change). */\nexport function writeControl(pixels: Uint8ClampedArray, maskWidth: number, rect: MaskRect, values: ArrayLike<number>): void {\n for (let y = 0; y < rect.height; y += 1) {\n for (let x = 0; x < rect.width; x += 1) {\n const value = values[y * rect.width + x]!;\n const offset = ((rect.y + y) * maskWidth + rect.x + x) * 4;\n pixels[offset] = value & 255;\n pixels[offset + 1] = value >> 8;\n pixels[offset + 2] = 0;\n pixels[offset + 3] = value === NO_MATERIAL ? 0 : 255;\n }\n }\n}\n\nconst toKernelMode = (mode: TerrainMode | undefined): TerrainRenderMode | undefined => mode as TerrainRenderMode | undefined;\n\n/** The document the kernel renders. `controlPixels` is the control image, kept up to date by the caller. */\nexport function toKernelDocument(init: TerrainRenderInit, control: Uint8ClampedArray): TerrainMapDocument {\n const { data } = init;\n return {\n version: 1,\n width: init.size[0],\n height: init.size[1],\n tileSize: init.tileSize,\n textureGrid: { columns: 1, rows: 1, tileSize: 1 },\n textures: data.materials.map((material, index) => {\n const source = init.sources[material.id];\n const image = source ? init.images[source.src] : undefined;\n const frame = source?.frame ?? (image ? [0, 0, image.width, image.height] as const : undefined);\n return {\n id: material.id,\n index,\n label: material.id,\n ...(source ? { source: source.src } : {}),\n ...(frame ? { sourceRect: { x: frame[0], y: frame[1], width: frame[2], height: frame[3] } } : {}),\n ...(material.priority !== undefined ? { priority: material.priority } : { priority: index }),\n ...(material.collision !== undefined ? { collision: material.collision } : {}),\n ...(material.specialType !== undefined ? { specialType: material.specialType } : {}),\n ...(material.renderTileSize !== undefined ? { renderTileSize: material.renderTileSize } : {}),\n ...(material.renderingStyle !== undefined ? { renderingStyle: material.renderingStyle } : {}),\n ...(toKernelMode(material.mode) ? { defaultRenderMode: toKernelMode(material.mode)! } : {}),\n };\n }),\n transitions: (data.transitions ?? []).map((rule) => ({ from: rule.from, to: rule.to, mode: toKernelMode(rule.mode)!, ...(rule.priority !== undefined ? { priority: rule.priority } : {}) })),\n controlTexture: { width: data.mask.width, height: data.mask.height, palette: data.materials.map((m) => m.id), pixels: control },\n morphology: (data.morphology ?? []) as unknown as TerrainMapDocument[\"morphology\"],\n waterAnimation: data.water ?? { enabled: false, speed: 1, intensity: 1, direction: 0 },\n };\n}\n\n/** What to render again after a patch, in world pixels. `\"all\"` when the patch can change anything. */\nexport function dirtyOfPatch(patch: MapPatch, data: TerrainData, grow: number): WorldRect | \"all\" | \"none\" {\n const scale = data.mask.scale;\n const fromMask = (rect: MaskRect | undefined): WorldRect | \"none\" =>\n rect ? growRect({ x: rect.x * scale, y: rect.y * scale, width: rect.width * scale, height: rect.height * scale }, grow) : \"none\";\n switch (patch.type) {\n case \"terrain.paint\":\n return fromMask(shapeBounds(data.mask, patch.shape));\n case \"terrain.region\":\n return fromMask({ x: patch.x, y: patch.y, width: patch.width, height: patch.height });\n case \"terrain.materials\":\n case \"terrain.morphology\":\n case \"extension.set\":\n return \"all\";\n default:\n return \"none\";\n }\n}\n\nexport const growRect = (rect: WorldRect, by: number): WorldRect => ({ x: rect.x - by, y: rect.y - by, width: rect.width + by * 2, height: rect.height + by * 2 });\n"],"mappings":"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"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { MaskRect, TerrainData, TerrainMask, TerrainShape } from './types';
|
|
2
|
+
export declare const chunkCount: (mask: TerrainMask) => {
|
|
3
|
+
cols: number;
|
|
4
|
+
rows: number;
|
|
5
|
+
};
|
|
6
|
+
/** The values of a chunk. A chunk that is not stored is flat. */
|
|
7
|
+
export declare function readChunk(mask: TerrainMask, col: number, row: number): Uint16Array;
|
|
8
|
+
/** Material index at a mask pixel. */
|
|
9
|
+
export declare function readPixel(mask: TerrainMask, x: number, y: number): number;
|
|
10
|
+
/** Whole mask as one array, row by row. For the kernel and for tests: it is as big as the map. */
|
|
11
|
+
export declare function readAll(mask: TerrainMask): Uint16Array;
|
|
12
|
+
/** Values of a rectangle, row by row. The rectangle is clipped by the caller. */
|
|
13
|
+
export declare function readRect(mask: TerrainMask, rect: MaskRect): Uint16Array;
|
|
14
|
+
export declare function clipRect(mask: TerrainMask, rect: MaskRect): MaskRect | undefined;
|
|
15
|
+
/** Bounds of a shape in mask pixels, clipped to the mask. `undefined` when it is entirely outside. */
|
|
16
|
+
export declare function shapeBounds(mask: TerrainMask, shape: TerrainShape): MaskRect | undefined;
|
|
17
|
+
/** Whether the centre of a mask pixel is inside the shape. */
|
|
18
|
+
export declare function covers(mask: TerrainMask, shape: TerrainShape, x: number, y: number): boolean;
|
|
19
|
+
export interface PaintResult {
|
|
20
|
+
readonly mask: TerrainMask;
|
|
21
|
+
/** Smallest rectangle (in mask pixels) that contains every pixel that changed. Undefined when none did. */
|
|
22
|
+
readonly changed?: MaskRect;
|
|
23
|
+
/** The values of `changed` before the change, for the inverse. */
|
|
24
|
+
readonly before?: Uint16Array;
|
|
25
|
+
}
|
|
26
|
+
/** Sets the material under a shape. Chunks that do not change are shared with the input. */
|
|
27
|
+
export declare function paintShape(mask: TerrainMask, shape: TerrainShape, material: number): PaintResult;
|
|
28
|
+
/** Writes values over a rectangle (an inverse, or an imported region). Chunks that do not change are shared. */
|
|
29
|
+
export declare function writeRect(mask: TerrainMask, rect: MaskRect, values: ArrayLike<number>): PaintResult;
|
|
30
|
+
/** A new, flat mask. */
|
|
31
|
+
export declare function createMask(width: number, height: number, options?: {
|
|
32
|
+
scale?: number;
|
|
33
|
+
chunk?: number;
|
|
34
|
+
fill?: number;
|
|
35
|
+
}): TerrainMask;
|
|
36
|
+
export type { TerrainData };
|
|
37
|
+
//# sourceMappingURL=mask.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mask.d.ts","sourceRoot":"","sources":["../../src/terrain/mask.ts"],"names":[],"mappings":"AAAA,OAAO,EAAe,KAAK,QAAQ,EAAE,KAAK,WAAW,EAAE,KAAK,WAAW,EAAE,KAAK,YAAY,EAAE,MAAM,SAAS,CAAC;AAM5G,eAAO,MAAM,UAAU,GAAI,MAAM,WAAW,KAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAGzE,CAAC;AAEH,iEAAiE;AACjE,wBAAgB,SAAS,CAAC,IAAI,EAAE,WAAW,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,WAAW,CAIlF;AAED,sCAAsC;AACtC,wBAAgB,SAAS,CAAC,IAAI,EAAE,WAAW,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAGzE;AAED,kGAAkG;AAClG,wBAAgB,OAAO,CAAC,IAAI,EAAE,WAAW,GAAG,WAAW,CActD;AAED,iFAAiF;AACjF,wBAAgB,QAAQ,CAAC,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,QAAQ,GAAG,WAAW,CAoBvE;AAED,wBAAgB,QAAQ,CAAC,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,QAAQ,GAAG,QAAQ,GAAG,SAAS,CAMhF;AAED,sGAAsG;AACtG,wBAAgB,WAAW,CAAC,IAAI,EAAE,WAAW,EAAE,KAAK,EAAE,YAAY,GAAG,QAAQ,GAAG,SAAS,CAkBxF;AAYD,8DAA8D;AAC9D,wBAAgB,MAAM,CAAC,IAAI,EAAE,WAAW,EAAE,KAAK,EAAE,YAAY,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAqB5F;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC;IAC3B,2GAA2G;IAC3G,QAAQ,CAAC,OAAO,CAAC,EAAE,QAAQ,CAAC;IAC5B,kEAAkE;IAClE,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAC;CAC/B;AAED,4FAA4F;AAC5F,wBAAgB,UAAU,CAAC,IAAI,EAAE,WAAW,EAAE,KAAK,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,GAAG,WAAW,CA0ChG;AAED,gHAAgH;AAChH,wBAAgB,SAAS,CAAC,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,CAAC,MAAM,CAAC,GAAG,WAAW,CAsCnG;AAED,wBAAwB;AACxB,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,GAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAO,GAAG,WAAW,CAUtI;AAED,YAAY,EAAE,WAAW,EAAE,CAAC"}
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { NO_MATERIAL } from "./types.js";
|
|
2
|
+
import { decodeRuns, encodeRuns } from "./rle.js";
|
|
3
|
+
//#region src/terrain/mask.ts
|
|
4
|
+
/** Pure operations on the chunked mask. Nothing here mutates its input. */
|
|
5
|
+
var key = (col, row) => `${col},${row}`;
|
|
6
|
+
var chunkCount = (mask) => ({
|
|
7
|
+
cols: Math.ceil(mask.width / mask.chunk),
|
|
8
|
+
rows: Math.ceil(mask.height / mask.chunk)
|
|
9
|
+
});
|
|
10
|
+
/** The values of a chunk. A chunk that is not stored is flat. */
|
|
11
|
+
function readChunk(mask, col, row) {
|
|
12
|
+
const stored = mask.chunks[key(col, row)];
|
|
13
|
+
if (stored === void 0) return new Uint16Array(mask.chunk * mask.chunk).fill(mask.fill);
|
|
14
|
+
return decodeRuns(stored, mask.chunk * mask.chunk);
|
|
15
|
+
}
|
|
16
|
+
/** Material index at a mask pixel. */
|
|
17
|
+
function readPixel(mask, x, y) {
|
|
18
|
+
if (x < 0 || y < 0 || x >= mask.width || y >= mask.height) return NO_MATERIAL;
|
|
19
|
+
return readChunk(mask, Math.floor(x / mask.chunk), Math.floor(y / mask.chunk))[y % mask.chunk * mask.chunk + x % mask.chunk];
|
|
20
|
+
}
|
|
21
|
+
/** Whole mask as one array, row by row. For the kernel and for tests: it is as big as the map. */
|
|
22
|
+
function readAll(mask) {
|
|
23
|
+
const out = new Uint16Array(mask.width * mask.height);
|
|
24
|
+
const { cols, rows } = chunkCount(mask);
|
|
25
|
+
for (let row = 0; row < rows; row += 1) for (let col = 0; col < cols; col += 1) {
|
|
26
|
+
const chunk = readChunk(mask, col, row);
|
|
27
|
+
const width = Math.min(mask.chunk, mask.width - col * mask.chunk);
|
|
28
|
+
const height = Math.min(mask.chunk, mask.height - row * mask.chunk);
|
|
29
|
+
for (let y = 0; y < height; y += 1) out.set(chunk.subarray(y * mask.chunk, y * mask.chunk + width), (row * mask.chunk + y) * mask.width + col * mask.chunk);
|
|
30
|
+
}
|
|
31
|
+
return out;
|
|
32
|
+
}
|
|
33
|
+
/** Values of a rectangle, row by row. The rectangle is clipped by the caller. */
|
|
34
|
+
function readRect(mask, rect) {
|
|
35
|
+
const out = new Uint16Array(rect.width * rect.height);
|
|
36
|
+
const c0 = Math.floor(rect.x / mask.chunk);
|
|
37
|
+
const c1 = Math.floor((rect.x + rect.width - 1) / mask.chunk);
|
|
38
|
+
const r0 = Math.floor(rect.y / mask.chunk);
|
|
39
|
+
const r1 = Math.floor((rect.y + rect.height - 1) / mask.chunk);
|
|
40
|
+
for (let row = r0; row <= r1; row += 1) for (let col = c0; col <= c1; col += 1) {
|
|
41
|
+
const chunk = readChunk(mask, col, row);
|
|
42
|
+
const x0 = Math.max(rect.x, col * mask.chunk);
|
|
43
|
+
const x1 = Math.min(rect.x + rect.width, (col + 1) * mask.chunk);
|
|
44
|
+
const y0 = Math.max(rect.y, row * mask.chunk);
|
|
45
|
+
const y1 = Math.min(rect.y + rect.height, (row + 1) * mask.chunk);
|
|
46
|
+
for (let y = y0; y < y1; y += 1) {
|
|
47
|
+
const from = (y - row * mask.chunk) * mask.chunk + (x0 - col * mask.chunk);
|
|
48
|
+
out.set(chunk.subarray(from, from + (x1 - x0)), (y - rect.y) * rect.width + (x0 - rect.x));
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
function clipRect(mask, rect) {
|
|
54
|
+
const x0 = Math.max(0, Math.floor(rect.x));
|
|
55
|
+
const y0 = Math.max(0, Math.floor(rect.y));
|
|
56
|
+
const x1 = Math.min(mask.width, Math.ceil(rect.x + rect.width));
|
|
57
|
+
const y1 = Math.min(mask.height, Math.ceil(rect.y + rect.height));
|
|
58
|
+
return x1 > x0 && y1 > y0 ? {
|
|
59
|
+
x: x0,
|
|
60
|
+
y: y0,
|
|
61
|
+
width: x1 - x0,
|
|
62
|
+
height: y1 - y0
|
|
63
|
+
} : void 0;
|
|
64
|
+
}
|
|
65
|
+
/** Bounds of a shape in mask pixels, clipped to the mask. `undefined` when it is entirely outside. */
|
|
66
|
+
function shapeBounds(mask, shape) {
|
|
67
|
+
const s = mask.scale;
|
|
68
|
+
switch (shape.type) {
|
|
69
|
+
case "fill": return {
|
|
70
|
+
x: 0,
|
|
71
|
+
y: 0,
|
|
72
|
+
width: mask.width,
|
|
73
|
+
height: mask.height
|
|
74
|
+
};
|
|
75
|
+
case "circle": return clipRect(mask, {
|
|
76
|
+
x: (shape.x - shape.radius) / s,
|
|
77
|
+
y: (shape.y - shape.radius) / s,
|
|
78
|
+
width: shape.radius * 2 / s + 1,
|
|
79
|
+
height: shape.radius * 2 / s + 1
|
|
80
|
+
});
|
|
81
|
+
case "rect": return clipRect(mask, {
|
|
82
|
+
x: shape.x / s,
|
|
83
|
+
y: shape.y / s,
|
|
84
|
+
width: shape.width / s,
|
|
85
|
+
height: shape.height / s
|
|
86
|
+
});
|
|
87
|
+
case "stroke": {
|
|
88
|
+
if (shape.points.length === 0) return void 0;
|
|
89
|
+
const xs = shape.points.map((p) => p[0]);
|
|
90
|
+
const ys = shape.points.map((p) => p[1]);
|
|
91
|
+
const x0 = Math.min(...xs) - shape.radius;
|
|
92
|
+
const y0 = Math.min(...ys) - shape.radius;
|
|
93
|
+
return clipRect(mask, {
|
|
94
|
+
x: x0 / s,
|
|
95
|
+
y: y0 / s,
|
|
96
|
+
width: (Math.max(...xs) - Math.min(...xs) + shape.radius * 2) / s + 1,
|
|
97
|
+
height: (Math.max(...ys) - Math.min(...ys) + shape.radius * 2) / s + 1
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
var distanceToSegment2 = (px, py, ax, ay, bx, by) => {
|
|
103
|
+
const dx = bx - ax;
|
|
104
|
+
const dy = by - ay;
|
|
105
|
+
const length2 = dx * dx + dy * dy;
|
|
106
|
+
const t = length2 === 0 ? 0 : Math.max(0, Math.min(1, ((px - ax) * dx + (py - ay) * dy) / length2));
|
|
107
|
+
const cx = ax + t * dx;
|
|
108
|
+
const cy = ay + t * dy;
|
|
109
|
+
return (px - cx) ** 2 + (py - cy) ** 2;
|
|
110
|
+
};
|
|
111
|
+
/** Whether the centre of a mask pixel is inside the shape. */
|
|
112
|
+
function covers(mask, shape, x, y) {
|
|
113
|
+
const wx = (x + .5) * mask.scale;
|
|
114
|
+
const wy = (y + .5) * mask.scale;
|
|
115
|
+
switch (shape.type) {
|
|
116
|
+
case "fill": return true;
|
|
117
|
+
case "circle": return (wx - shape.x) ** 2 + (wy - shape.y) ** 2 <= shape.radius ** 2;
|
|
118
|
+
case "rect": return wx >= shape.x && wx < shape.x + shape.width && wy >= shape.y && wy < shape.y + shape.height;
|
|
119
|
+
case "stroke": {
|
|
120
|
+
const r2 = shape.radius ** 2;
|
|
121
|
+
if (shape.points.length === 1) return (wx - shape.points[0][0]) ** 2 + (wy - shape.points[0][1]) ** 2 <= r2;
|
|
122
|
+
for (let i = 1; i < shape.points.length; i += 1) {
|
|
123
|
+
const a = shape.points[i - 1];
|
|
124
|
+
const b = shape.points[i];
|
|
125
|
+
if (distanceToSegment2(wx, wy, a[0], a[1], b[0], b[1]) <= r2) return true;
|
|
126
|
+
}
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/** Sets the material under a shape. Chunks that do not change are shared with the input. */
|
|
132
|
+
function paintShape(mask, shape, material) {
|
|
133
|
+
const bounds = shapeBounds(mask, shape);
|
|
134
|
+
if (!bounds) return { mask };
|
|
135
|
+
const c0 = Math.floor(bounds.x / mask.chunk);
|
|
136
|
+
const c1 = Math.floor((bounds.x + bounds.width - 1) / mask.chunk);
|
|
137
|
+
const r0 = Math.floor(bounds.y / mask.chunk);
|
|
138
|
+
const r1 = Math.floor((bounds.y + bounds.height - 1) / mask.chunk);
|
|
139
|
+
const next = { ...mask.chunks };
|
|
140
|
+
let minX = Infinity, minY = Infinity, maxX = -1, maxY = -1;
|
|
141
|
+
for (let row = r0; row <= r1; row += 1) for (let col = c0; col <= c1; col += 1) {
|
|
142
|
+
const values = readChunk(mask, col, row);
|
|
143
|
+
let dirty = false;
|
|
144
|
+
const x0 = Math.max(bounds.x, col * mask.chunk);
|
|
145
|
+
const x1 = Math.min(bounds.x + bounds.width, (col + 1) * mask.chunk);
|
|
146
|
+
const y0 = Math.max(bounds.y, row * mask.chunk);
|
|
147
|
+
const y1 = Math.min(bounds.y + bounds.height, (row + 1) * mask.chunk);
|
|
148
|
+
for (let y = y0; y < y1; y += 1) for (let x = x0; x < x1; x += 1) {
|
|
149
|
+
if (!covers(mask, shape, x, y)) continue;
|
|
150
|
+
const index = (y - row * mask.chunk) * mask.chunk + (x - col * mask.chunk);
|
|
151
|
+
if (values[index] === material) continue;
|
|
152
|
+
values[index] = material;
|
|
153
|
+
dirty = true;
|
|
154
|
+
if (x < minX) minX = x;
|
|
155
|
+
if (x > maxX) maxX = x;
|
|
156
|
+
if (y < minY) minY = y;
|
|
157
|
+
if (y > maxY) maxY = y;
|
|
158
|
+
}
|
|
159
|
+
if (!dirty) continue;
|
|
160
|
+
const encoded = encodeRuns(values);
|
|
161
|
+
if (values.every((value) => value === mask.fill)) delete next[key(col, row)];
|
|
162
|
+
else next[key(col, row)] = encoded;
|
|
163
|
+
}
|
|
164
|
+
if (maxX < 0) return { mask };
|
|
165
|
+
const changed = {
|
|
166
|
+
x: minX,
|
|
167
|
+
y: minY,
|
|
168
|
+
width: maxX - minX + 1,
|
|
169
|
+
height: maxY - minY + 1
|
|
170
|
+
};
|
|
171
|
+
const before = readRect(mask, changed);
|
|
172
|
+
return {
|
|
173
|
+
mask: {
|
|
174
|
+
...mask,
|
|
175
|
+
chunks: next
|
|
176
|
+
},
|
|
177
|
+
changed,
|
|
178
|
+
before
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
/** Writes values over a rectangle (an inverse, or an imported region). Chunks that do not change are shared. */
|
|
182
|
+
function writeRect(mask, rect, values) {
|
|
183
|
+
const clipped = clipRect(mask, rect);
|
|
184
|
+
if (!clipped || clipped.x !== rect.x || clipped.y !== rect.y || clipped.width !== rect.width || clipped.height !== rect.height) return { mask };
|
|
185
|
+
const c0 = Math.floor(rect.x / mask.chunk);
|
|
186
|
+
const c1 = Math.floor((rect.x + rect.width - 1) / mask.chunk);
|
|
187
|
+
const r0 = Math.floor(rect.y / mask.chunk);
|
|
188
|
+
const r1 = Math.floor((rect.y + rect.height - 1) / mask.chunk);
|
|
189
|
+
const next = { ...mask.chunks };
|
|
190
|
+
let minX = Infinity, minY = Infinity, maxX = -1, maxY = -1;
|
|
191
|
+
for (let row = r0; row <= r1; row += 1) for (let col = c0; col <= c1; col += 1) {
|
|
192
|
+
const chunk = readChunk(mask, col, row);
|
|
193
|
+
let dirty = false;
|
|
194
|
+
const x0 = Math.max(rect.x, col * mask.chunk);
|
|
195
|
+
const x1 = Math.min(rect.x + rect.width, (col + 1) * mask.chunk);
|
|
196
|
+
const y0 = Math.max(rect.y, row * mask.chunk);
|
|
197
|
+
const y1 = Math.min(rect.y + rect.height, (row + 1) * mask.chunk);
|
|
198
|
+
for (let y = y0; y < y1; y += 1) for (let x = x0; x < x1; x += 1) {
|
|
199
|
+
const value = values[(y - rect.y) * rect.width + (x - rect.x)];
|
|
200
|
+
const index = (y - row * mask.chunk) * mask.chunk + (x - col * mask.chunk);
|
|
201
|
+
if (chunk[index] === value) continue;
|
|
202
|
+
chunk[index] = value;
|
|
203
|
+
dirty = true;
|
|
204
|
+
if (x < minX) minX = x;
|
|
205
|
+
if (x > maxX) maxX = x;
|
|
206
|
+
if (y < minY) minY = y;
|
|
207
|
+
if (y > maxY) maxY = y;
|
|
208
|
+
}
|
|
209
|
+
if (!dirty) continue;
|
|
210
|
+
if (chunk.every((value) => value === mask.fill)) delete next[key(col, row)];
|
|
211
|
+
else next[key(col, row)] = encodeRuns(chunk);
|
|
212
|
+
}
|
|
213
|
+
if (maxX < 0) return { mask };
|
|
214
|
+
const changed = {
|
|
215
|
+
x: minX,
|
|
216
|
+
y: minY,
|
|
217
|
+
width: maxX - minX + 1,
|
|
218
|
+
height: maxY - minY + 1
|
|
219
|
+
};
|
|
220
|
+
return {
|
|
221
|
+
mask: {
|
|
222
|
+
...mask,
|
|
223
|
+
chunks: next
|
|
224
|
+
},
|
|
225
|
+
changed,
|
|
226
|
+
before: readRect(mask, changed)
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
/** A new, flat mask. */
|
|
230
|
+
function createMask(width, height, options = {}) {
|
|
231
|
+
const scale = options.scale ?? 1;
|
|
232
|
+
return {
|
|
233
|
+
scale,
|
|
234
|
+
width: Math.ceil(width / scale),
|
|
235
|
+
height: Math.ceil(height / scale),
|
|
236
|
+
chunk: options.chunk ?? 256,
|
|
237
|
+
fill: options.fill ?? 65535,
|
|
238
|
+
chunks: {}
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
//#endregion
|
|
242
|
+
export { chunkCount, clipRect, covers, createMask, paintShape, readAll, readChunk, readPixel, readRect, shapeBounds, writeRect };
|
|
243
|
+
|
|
244
|
+
//# sourceMappingURL=mask.js.map
|