@three-flatland/normals 0.1.0-alpha.1 → 0.1.0-alpha.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,128 +1,115 @@
1
- import { resolve } from "node:path";
2
- import { existsSync, readFileSync } from "node:fs";
3
- import {
4
- directionToAngle
5
- } from "./descriptor.js";
1
+ import { directionToAngle } from "./descriptor.js";
6
2
  import { bakeNormalMapFile } from "./bake.node.js";
3
+ import { existsSync, readFileSync } from "node:fs";
4
+ import { resolve } from "node:path";
5
+ //#region src/cli.ts
7
6
  const USAGE = [
8
- "Usage:",
9
- " flatland-bake normal <input.png> [output.png] [options]",
10
- "",
11
- "Reads an RGBA PNG, derives a tangent-space normal map, and writes",
12
- "<input>.normal.png (or the given output path). The output PNG is",
13
- "stamped with the descriptor's content hash under a `flatland` tEXt chunk",
14
- "so runtime loaders can invalidate stale siblings.",
15
- "",
16
- "Options:",
17
- " --descriptor <path> JSON descriptor file \u2014 region-aware control",
18
- " (frames, tiles, cap/face splits, per-region tilt)",
19
- " --direction <dir> Single-region tilt: flat|up|down|left|right|",
20
- " north|south|east|west|up-left|\u2026 (default: flat)",
21
- " --pitch <radians> Tilt angle from flat (default: \u03C0/4)",
22
- " --bump <mode> alpha|none (default: alpha)",
23
- " --strength <n> Gradient multiplier before normalization (default: 1)",
24
- "",
25
- "Flat flags build a zero-region descriptor whose defaults apply to the",
26
- "whole texture. When --descriptor is also provided, flat flags override",
27
- "the descriptor-level defaults; existing regions are untouched."
7
+ "Usage:",
8
+ " flatland-bake normal <input.png> [output.png] [options]",
9
+ "",
10
+ "Reads an RGBA PNG, derives a tangent-space normal map, and writes",
11
+ "<input>.normal.png (or the given output path). The output PNG is",
12
+ "stamped with the descriptor's content hash under a `flatland` tEXt chunk",
13
+ "so runtime loaders can invalidate stale siblings.",
14
+ "",
15
+ "Options:",
16
+ " --descriptor <path> JSON descriptor file region-aware control",
17
+ " (frames, tiles, cap/face splits, per-region tilt)",
18
+ " --direction <dir> Single-region tilt: flat|up|down|left|right|",
19
+ " north|south|east|west|up-left|… (default: flat)",
20
+ " --pitch <radians> Tilt angle from flat (default: π/4)",
21
+ " --bump <mode> alpha|none (default: alpha)",
22
+ " --strength <n> Gradient multiplier before normalization (default: 1)",
23
+ "",
24
+ "Flat flags build a zero-region descriptor whose defaults apply to the",
25
+ "whole texture. When --descriptor is also provided, flat flags override",
26
+ "the descriptor-level defaults; existing regions are untouched."
28
27
  ].join("\n");
29
28
  const baker = {
30
- name: "normal",
31
- description: "Bake a tangent-space normal map from a sprite PNG",
32
- usage() {
33
- return USAGE;
34
- },
35
- run(args) {
36
- if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
37
- process.stdout.write(USAGE + "\n");
38
- return Promise.resolve(args.length === 0 ? 1 : 0);
39
- }
40
- const flags = {};
41
- const positional = [];
42
- for (let i = 0; i < args.length; i++) {
43
- const arg = args[i];
44
- if (arg === "--strength") {
45
- const raw = args[++i];
46
- const n = Number(raw);
47
- if (raw === void 0 || !Number.isFinite(n)) {
48
- process.stderr.write("--strength requires a number\n");
49
- return Promise.resolve(1);
50
- }
51
- flags.strength = n;
52
- } else if (arg === "--direction") {
53
- const raw = args[++i];
54
- if (raw === void 0) {
55
- process.stderr.write("--direction requires a name\n");
56
- return Promise.resolve(1);
57
- }
58
- try {
59
- directionToAngle(raw);
60
- } catch (err) {
61
- process.stderr.write(
62
- `--direction: ${err instanceof Error ? err.message : String(err)}
63
- `
64
- );
65
- return Promise.resolve(1);
66
- }
67
- flags.direction = raw;
68
- } else if (arg === "--pitch") {
69
- const raw = args[++i];
70
- const n = Number(raw);
71
- if (raw === void 0 || !Number.isFinite(n)) {
72
- process.stderr.write("--pitch requires a number (radians)\n");
73
- return Promise.resolve(1);
74
- }
75
- flags.pitch = n;
76
- } else if (arg === "--bump") {
77
- const raw = args[++i];
78
- if (raw !== "alpha" && raw !== "none") {
79
- process.stderr.write('--bump must be "alpha" or "none"\n');
80
- return Promise.resolve(1);
81
- }
82
- flags.bump = raw;
83
- } else if (arg === "--descriptor" || arg === "-d") {
84
- const raw = args[++i];
85
- if (raw === void 0) {
86
- process.stderr.write("--descriptor requires a path\n");
87
- return Promise.resolve(1);
88
- }
89
- flags.descriptorPath = resolve(raw);
90
- if (!existsSync(flags.descriptorPath)) {
91
- process.stderr.write(`descriptor not found: ${flags.descriptorPath}
92
- `);
93
- return Promise.resolve(1);
94
- }
95
- } else {
96
- positional.push(arg);
97
- }
98
- }
99
- const [inputArg, outputArg] = positional;
100
- if (!inputArg) {
101
- process.stderr.write(
102
- "missing <input.png>. Run `flatland-bake normal --help`.\n"
103
- );
104
- return Promise.resolve(1);
105
- }
106
- const inputPath = resolve(inputArg);
107
- if (!existsSync(inputPath)) {
108
- process.stderr.write(`input not found: ${inputPath}
109
- `);
110
- return Promise.resolve(1);
111
- }
112
- const descriptor = flags.descriptorPath ? JSON.parse(readFileSync(flags.descriptorPath, "utf8")) : {};
113
- if (flags.direction !== void 0) descriptor.direction = flags.direction;
114
- if (flags.pitch !== void 0) descriptor.pitch = flags.pitch;
115
- if (flags.bump !== void 0) descriptor.bump = flags.bump;
116
- if (flags.strength !== void 0) descriptor.strength = flags.strength;
117
- const outputPath = outputArg ? resolve(outputArg) : void 0;
118
- const written = bakeNormalMapFile(inputPath, descriptor, outputPath);
119
- process.stdout.write(`wrote ${written}
120
- `);
121
- return Promise.resolve(0);
122
- }
123
- };
124
- var cli_default = baker;
125
- export {
126
- cli_default as default
29
+ name: "normal",
30
+ description: "Bake a tangent-space normal map from a sprite PNG",
31
+ usage() {
32
+ return USAGE;
33
+ },
34
+ run(args) {
35
+ if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
36
+ process.stdout.write(USAGE + "\n");
37
+ return Promise.resolve(args.length === 0 ? 1 : 0);
38
+ }
39
+ const flags = {};
40
+ const positional = [];
41
+ for (let i = 0; i < args.length; i++) {
42
+ const arg = args[i];
43
+ if (arg === "--strength") {
44
+ const raw = args[++i];
45
+ const n = Number(raw);
46
+ if (raw === void 0 || !Number.isFinite(n)) {
47
+ process.stderr.write("--strength requires a number\n");
48
+ return Promise.resolve(1);
49
+ }
50
+ flags.strength = n;
51
+ } else if (arg === "--direction") {
52
+ const raw = args[++i];
53
+ if (raw === void 0) {
54
+ process.stderr.write("--direction requires a name\n");
55
+ return Promise.resolve(1);
56
+ }
57
+ try {
58
+ directionToAngle(raw);
59
+ } catch (err) {
60
+ process.stderr.write(`--direction: ${err instanceof Error ? err.message : String(err)}\n`);
61
+ return Promise.resolve(1);
62
+ }
63
+ flags.direction = raw;
64
+ } else if (arg === "--pitch") {
65
+ const raw = args[++i];
66
+ const n = Number(raw);
67
+ if (raw === void 0 || !Number.isFinite(n)) {
68
+ process.stderr.write("--pitch requires a number (radians)\n");
69
+ return Promise.resolve(1);
70
+ }
71
+ flags.pitch = n;
72
+ } else if (arg === "--bump") {
73
+ const raw = args[++i];
74
+ if (raw !== "alpha" && raw !== "none") {
75
+ process.stderr.write("--bump must be \"alpha\" or \"none\"\n");
76
+ return Promise.resolve(1);
77
+ }
78
+ flags.bump = raw;
79
+ } else if (arg === "--descriptor" || arg === "-d") {
80
+ const raw = args[++i];
81
+ if (raw === void 0) {
82
+ process.stderr.write("--descriptor requires a path\n");
83
+ return Promise.resolve(1);
84
+ }
85
+ flags.descriptorPath = resolve(raw);
86
+ if (!existsSync(flags.descriptorPath)) {
87
+ process.stderr.write(`descriptor not found: ${flags.descriptorPath}\n`);
88
+ return Promise.resolve(1);
89
+ }
90
+ } else positional.push(arg);
91
+ }
92
+ const [inputArg, outputArg] = positional;
93
+ if (!inputArg) {
94
+ process.stderr.write("missing <input.png>. Run `flatland-bake normal --help`.\n");
95
+ return Promise.resolve(1);
96
+ }
97
+ const inputPath = resolve(inputArg);
98
+ if (!existsSync(inputPath)) {
99
+ process.stderr.write(`input not found: ${inputPath}\n`);
100
+ return Promise.resolve(1);
101
+ }
102
+ const descriptor = flags.descriptorPath ? JSON.parse(readFileSync(flags.descriptorPath, "utf8")) : {};
103
+ if (flags.direction !== void 0) descriptor.direction = flags.direction;
104
+ if (flags.pitch !== void 0) descriptor.pitch = flags.pitch;
105
+ if (flags.bump !== void 0) descriptor.bump = flags.bump;
106
+ if (flags.strength !== void 0) descriptor.strength = flags.strength;
107
+ const written = bakeNormalMapFile(inputPath, descriptor, outputArg ? resolve(outputArg) : void 0);
108
+ process.stdout.write(`wrote ${written}\n`);
109
+ return Promise.resolve(0);
110
+ }
127
111
  };
112
+ //#endregion
113
+ export { baker as default };
114
+
128
115
  //# sourceMappingURL=cli.js.map
package/dist/cli.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/cli.ts"],"sourcesContent":["import { resolve } from 'node:path'\nimport { existsSync, readFileSync } from 'node:fs'\nimport type { Baker } from '@three-flatland/bake'\nimport {\n directionToAngle,\n type NormalDirection,\n type NormalSourceDescriptor,\n} from './descriptor.js'\nimport { bakeNormalMapFile } from './bake.node.js'\n\nconst USAGE = [\n 'Usage:',\n ' flatland-bake normal <input.png> [output.png] [options]',\n '',\n 'Reads an RGBA PNG, derives a tangent-space normal map, and writes',\n '<input>.normal.png (or the given output path). The output PNG is',\n \"stamped with the descriptor's content hash under a `flatland` tEXt chunk\",\n 'so runtime loaders can invalidate stale siblings.',\n '',\n 'Options:',\n ' --descriptor <path> JSON descriptor file — region-aware control',\n ' (frames, tiles, cap/face splits, per-region tilt)',\n ' --direction <dir> Single-region tilt: flat|up|down|left|right|',\n ' north|south|east|west|up-left|… (default: flat)',\n ' --pitch <radians> Tilt angle from flat (default: π/4)',\n ' --bump <mode> alpha|none (default: alpha)',\n ' --strength <n> Gradient multiplier before normalization (default: 1)',\n '',\n 'Flat flags build a zero-region descriptor whose defaults apply to the',\n 'whole texture. When --descriptor is also provided, flat flags override',\n 'the descriptor-level defaults; existing regions are untouched.',\n].join('\\n')\n\nconst baker: Baker = {\n name: 'normal',\n description: 'Bake a tangent-space normal map from a sprite PNG',\n\n usage() {\n return USAGE\n },\n\n run(args) {\n if (args.length === 0 || args.includes('--help') || args.includes('-h')) {\n process.stdout.write(USAGE + '\\n')\n return Promise.resolve(args.length === 0 ? 1 : 0)\n }\n\n const flags: {\n strength?: number\n direction?: NormalDirection\n pitch?: number\n bump?: 'alpha' | 'none'\n descriptorPath?: string\n } = {}\n const positional: string[] = []\n\n for (let i = 0; i < args.length; i++) {\n const arg = args[i]!\n if (arg === '--strength') {\n const raw = args[++i]\n const n = Number(raw)\n if (raw === undefined || !Number.isFinite(n)) {\n process.stderr.write('--strength requires a number\\n')\n return Promise.resolve(1)\n }\n flags.strength = n\n } else if (arg === '--direction') {\n const raw = args[++i]\n if (raw === undefined) {\n process.stderr.write('--direction requires a name\\n')\n return Promise.resolve(1)\n }\n try {\n directionToAngle(raw as NormalDirection)\n } catch (err) {\n process.stderr.write(\n `--direction: ${err instanceof Error ? err.message : String(err)}\\n`\n )\n return Promise.resolve(1)\n }\n flags.direction = raw as NormalDirection\n } else if (arg === '--pitch') {\n const raw = args[++i]\n const n = Number(raw)\n if (raw === undefined || !Number.isFinite(n)) {\n process.stderr.write('--pitch requires a number (radians)\\n')\n return Promise.resolve(1)\n }\n flags.pitch = n\n } else if (arg === '--bump') {\n const raw = args[++i]\n if (raw !== 'alpha' && raw !== 'none') {\n process.stderr.write('--bump must be \"alpha\" or \"none\"\\n')\n return Promise.resolve(1)\n }\n flags.bump = raw\n } else if (arg === '--descriptor' || arg === '-d') {\n const raw = args[++i]\n if (raw === undefined) {\n process.stderr.write('--descriptor requires a path\\n')\n return Promise.resolve(1)\n }\n flags.descriptorPath = resolve(raw)\n if (!existsSync(flags.descriptorPath)) {\n process.stderr.write(`descriptor not found: ${flags.descriptorPath}\\n`)\n return Promise.resolve(1)\n }\n } else {\n positional.push(arg)\n }\n }\n\n const [inputArg, outputArg] = positional\n if (!inputArg) {\n process.stderr.write(\n 'missing <input.png>. Run `flatland-bake normal --help`.\\n'\n )\n return Promise.resolve(1)\n }\n\n const inputPath = resolve(inputArg)\n if (!existsSync(inputPath)) {\n process.stderr.write(`input not found: ${inputPath}\\n`)\n return Promise.resolve(1)\n }\n\n // Build the effective descriptor: start from the file (if any),\n // then overlay flat flags as top-level defaults. Existing regions\n // inside the descriptor are preserved verbatim.\n const descriptor: NormalSourceDescriptor = flags.descriptorPath\n ? (JSON.parse(readFileSync(flags.descriptorPath, 'utf8')) as NormalSourceDescriptor)\n : {}\n\n if (flags.direction !== undefined) descriptor.direction = flags.direction\n if (flags.pitch !== undefined) descriptor.pitch = flags.pitch\n if (flags.bump !== undefined) descriptor.bump = flags.bump\n if (flags.strength !== undefined) descriptor.strength = flags.strength\n\n const outputPath = outputArg ? resolve(outputArg) : undefined\n const written = bakeNormalMapFile(inputPath, descriptor, outputPath)\n process.stdout.write(`wrote ${written}\\n`)\n return Promise.resolve(0)\n },\n}\n\nexport default baker\n"],"mappings":"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","names":[]}
1
+ {"version":3,"file":"cli.js","names":[],"sources":["../src/cli.ts"],"sourcesContent":["import { resolve } from 'node:path'\nimport { existsSync, readFileSync } from 'node:fs'\nimport type { Baker } from '@three-flatland/bake'\nimport { directionToAngle, type NormalDirection, type NormalSourceDescriptor } from './descriptor.js'\nimport { bakeNormalMapFile } from './bake.node.js'\n\nconst USAGE = [\n 'Usage:',\n ' flatland-bake normal <input.png> [output.png] [options]',\n '',\n 'Reads an RGBA PNG, derives a tangent-space normal map, and writes',\n '<input>.normal.png (or the given output path). The output PNG is',\n \"stamped with the descriptor's content hash under a `flatland` tEXt chunk\",\n 'so runtime loaders can invalidate stale siblings.',\n '',\n 'Options:',\n ' --descriptor <path> JSON descriptor file — region-aware control',\n ' (frames, tiles, cap/face splits, per-region tilt)',\n ' --direction <dir> Single-region tilt: flat|up|down|left|right|',\n ' north|south|east|west|up-left|… (default: flat)',\n ' --pitch <radians> Tilt angle from flat (default: π/4)',\n ' --bump <mode> alpha|none (default: alpha)',\n ' --strength <n> Gradient multiplier before normalization (default: 1)',\n '',\n 'Flat flags build a zero-region descriptor whose defaults apply to the',\n 'whole texture. When --descriptor is also provided, flat flags override',\n 'the descriptor-level defaults; existing regions are untouched.',\n].join('\\n')\n\nconst baker: Baker = {\n name: 'normal',\n description: 'Bake a tangent-space normal map from a sprite PNG',\n\n usage() {\n return USAGE\n },\n\n run(args) {\n if (args.length === 0 || args.includes('--help') || args.includes('-h')) {\n process.stdout.write(USAGE + '\\n')\n return Promise.resolve(args.length === 0 ? 1 : 0)\n }\n\n const flags: {\n strength?: number\n direction?: NormalDirection\n pitch?: number\n bump?: 'alpha' | 'none'\n descriptorPath?: string\n } = {}\n const positional: string[] = []\n\n for (let i = 0; i < args.length; i++) {\n const arg = args[i]!\n if (arg === '--strength') {\n const raw = args[++i]\n const n = Number(raw)\n if (raw === undefined || !Number.isFinite(n)) {\n process.stderr.write('--strength requires a number\\n')\n return Promise.resolve(1)\n }\n flags.strength = n\n } else if (arg === '--direction') {\n const raw = args[++i]\n if (raw === undefined) {\n process.stderr.write('--direction requires a name\\n')\n return Promise.resolve(1)\n }\n try {\n directionToAngle(raw as NormalDirection)\n } catch (err) {\n process.stderr.write(`--direction: ${err instanceof Error ? err.message : String(err)}\\n`)\n return Promise.resolve(1)\n }\n flags.direction = raw as NormalDirection\n } else if (arg === '--pitch') {\n const raw = args[++i]\n const n = Number(raw)\n if (raw === undefined || !Number.isFinite(n)) {\n process.stderr.write('--pitch requires a number (radians)\\n')\n return Promise.resolve(1)\n }\n flags.pitch = n\n } else if (arg === '--bump') {\n const raw = args[++i]\n if (raw !== 'alpha' && raw !== 'none') {\n process.stderr.write('--bump must be \"alpha\" or \"none\"\\n')\n return Promise.resolve(1)\n }\n flags.bump = raw\n } else if (arg === '--descriptor' || arg === '-d') {\n const raw = args[++i]\n if (raw === undefined) {\n process.stderr.write('--descriptor requires a path\\n')\n return Promise.resolve(1)\n }\n flags.descriptorPath = resolve(raw)\n if (!existsSync(flags.descriptorPath)) {\n process.stderr.write(`descriptor not found: ${flags.descriptorPath}\\n`)\n return Promise.resolve(1)\n }\n } else {\n positional.push(arg)\n }\n }\n\n const [inputArg, outputArg] = positional\n if (!inputArg) {\n process.stderr.write('missing <input.png>. Run `flatland-bake normal --help`.\\n')\n return Promise.resolve(1)\n }\n\n const inputPath = resolve(inputArg)\n if (!existsSync(inputPath)) {\n process.stderr.write(`input not found: ${inputPath}\\n`)\n return Promise.resolve(1)\n }\n\n // Build the effective descriptor: start from the file (if any),\n // then overlay flat flags as top-level defaults. Existing regions\n // inside the descriptor are preserved verbatim.\n const descriptor: NormalSourceDescriptor = flags.descriptorPath\n ? (JSON.parse(readFileSync(flags.descriptorPath, 'utf8')) as NormalSourceDescriptor)\n : {}\n\n if (flags.direction !== undefined) descriptor.direction = flags.direction\n if (flags.pitch !== undefined) descriptor.pitch = flags.pitch\n if (flags.bump !== undefined) descriptor.bump = flags.bump\n if (flags.strength !== undefined) descriptor.strength = flags.strength\n\n const outputPath = outputArg ? resolve(outputArg) : undefined\n const written = bakeNormalMapFile(inputPath, descriptor, outputPath)\n process.stdout.write(`wrote ${written}\\n`)\n return Promise.resolve(0)\n },\n}\n\nexport default baker\n"],"mappings":"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"}
@@ -0,0 +1,129 @@
1
+ //#region src/descriptor.d.ts
2
+ /**
3
+ * Normal source descriptor — the shared shape used across three
4
+ * surfaces:
5
+ *
6
+ * 1. Library API `bakeNormalMap(pixels, w, h, descriptor)`
7
+ * 2. CLI `flatland-bake normal --descriptor <file>`
8
+ * 3. Loader API `LDtkLoader.load(url, { normals: descriptor })`
9
+ *
10
+ * The descriptor carries enough information for the baker to emit a
11
+ * 1:1 co-registered normal map: where the regions live, how each
12
+ * region tilts, and what per-texel bump source to use.
13
+ *
14
+ * All types are pure and browser-safe — consumed identically by node
15
+ * bakers and browser runtime loaders.
16
+ */
17
+ /**
18
+ * Screen-relative direction the surface normal's XY component points.
19
+ *
20
+ * Mental model: imagine the baked normal as an arrow. `NormalDirection`
21
+ * is the screen-space direction that arrow tilts toward — equivalently,
22
+ * the direction the tile's visible face is "pointing." For a wall at
23
+ * the top of the map (visible face toward the camera below), that's
24
+ * `'south'` / `'down'`.
25
+ *
26
+ * Cardinal and compass aliases are equivalent; the canonical form in
27
+ * the codebase is NSEW. Numbers are interpreted as radians in standard
28
+ * math convention (0 = +X / right, CCW positive).
29
+ */
30
+ type NormalDirection = 'flat' | 'up' | 'north' | 'down' | 'south' | 'left' | 'west' | 'right' | 'east' | 'up-left' | 'north-west' | 'up-right' | 'north-east' | 'down-left' | 'south-west' | 'down-right' | 'south-east' | number;
31
+ /**
32
+ * Resolve a `NormalDirection` to an angle in radians, or `null` when
33
+ * the direction is `'flat'` (no tilt).
34
+ *
35
+ * Convention: 0 = +X / right, π/2 = +Y / up, π = -X / left,
36
+ * -π/2 = -Y / down. Matches `Math.atan2` output.
37
+ */
38
+ declare function directionToAngle(direction: NormalDirection | undefined): number | null;
39
+ /**
40
+ * Per-texel bump source inside a region. Each non-'none' mode runs a
41
+ * central-difference gradient on the named channel; the result is
42
+ * treated as a height field where high values are raised and low
43
+ * values are sunken. Negative `strength` inverts (dark = raised).
44
+ *
45
+ * - `'alpha'` — gradient on the source alpha channel (default).
46
+ * Preserves sprite silhouette edges — the classic path for
47
+ * transparent sprites. Solid opaque regions produce no bump.
48
+ * - `'luminance'` — gradient on Rec. 709 luminance `(0.2126·R +
49
+ * 0.7152·G + 0.0722·B)`. Brick faces read as raised; dark mortar
50
+ * lines read as sunken grooves. Right mode for solid opaque
51
+ * tilesets.
52
+ * - `'red'` / `'green'` / `'blue'` — gradient on a single color
53
+ * channel. Use when your art treats one channel as a height map
54
+ * (e.g., packing height into the red channel of a data texture).
55
+ * - `'none'` — flat fill at the region's tilt direction. No per-texel
56
+ * variation; cheapest and useful for uniform surfaces.
57
+ */
58
+ type NormalBump = 'alpha' | 'luminance' | 'red' | 'green' | 'blue' | 'none';
59
+ interface NormalRegion {
60
+ x: number;
61
+ y: number;
62
+ w: number;
63
+ h: number;
64
+ /** Per-texel bump source. Default inherits from descriptor (`'alpha'`). */
65
+ bump?: NormalBump;
66
+ /** Base tilt direction. Default inherits from descriptor (`'flat'`). */
67
+ direction?: NormalDirection;
68
+ /**
69
+ * Tilt angle in radians from flat. Ignored when `direction === 'flat'`.
70
+ * Default inherits from descriptor, which defaults to `Math.PI / 4`.
71
+ */
72
+ pitch?: number;
73
+ /** Gradient strength multiplier for this region. Default 1. */
74
+ strength?: number;
75
+ /**
76
+ * World-space elevation of the region in [0, 1], where 0 = ground
77
+ * plane (floor) and 1 = top-of-wall (cap). Baked into the normal
78
+ * atlas's B channel and consumed by the light pass to compute
79
+ * per-fragment light direction (`L.z = lightHeight − elevation`).
80
+ *
81
+ * Default inherits from the descriptor, which defaults to 0. Cap
82
+ * regions typically set 1; tilted face regions typically set 0.5.
83
+ */
84
+ elevation?: number;
85
+ }
86
+ interface NormalSourceDescriptor {
87
+ /** Reserved for future schema evolution. Currently always `1`. */
88
+ version?: 1;
89
+ /** Default bump source for regions that don't specify one. Default `'alpha'`. */
90
+ bump?: NormalBump;
91
+ /** Default tilt for regions that don't specify one. Default `'flat'`. */
92
+ direction?: NormalDirection;
93
+ /** Default tilt pitch in radians. Default `Math.PI / 4`. */
94
+ pitch?: number;
95
+ /** Default gradient strength. Default `1`. */
96
+ strength?: number;
97
+ /** Default elevation for regions that don't specify one. Default 0 (ground). */
98
+ elevation?: number;
99
+ /**
100
+ * Regions of the source texture. When omitted, the whole texture is
101
+ * treated as a single region inheriting all descriptor defaults.
102
+ */
103
+ regions?: NormalRegion[];
104
+ }
105
+ declare const DEFAULT_PITCH: number;
106
+ declare const DEFAULT_STRENGTH = 1;
107
+ declare const DEFAULT_BUMP: NormalBump;
108
+ declare const DEFAULT_ELEVATION = 0;
109
+ /**
110
+ * Merge a region against its parent descriptor defaults. Returns a
111
+ * fully-populated region suitable for direct consumption by the baker.
112
+ */
113
+ interface ResolvedNormalRegion {
114
+ x: number;
115
+ y: number;
116
+ w: number;
117
+ h: number;
118
+ bump: NormalBump;
119
+ /** Tilt angle in radians, or `null` when the region is flat. */
120
+ angle: number | null;
121
+ pitch: number;
122
+ strength: number;
123
+ /** World-space elevation in [0, 1]. Written to the output B channel. */
124
+ elevation: number;
125
+ }
126
+ declare function resolveRegion(region: NormalRegion, descriptor?: NormalSourceDescriptor): ResolvedNormalRegion;
127
+ //#endregion
128
+ export { DEFAULT_BUMP, DEFAULT_ELEVATION, DEFAULT_PITCH, DEFAULT_STRENGTH, NormalBump, NormalDirection, NormalRegion, NormalSourceDescriptor, ResolvedNormalRegion, directionToAngle, resolveRegion };
129
+ //# sourceMappingURL=descriptor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"descriptor.d.ts","names":[],"sources":["../src/descriptor.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;KA+BY;;;;;;;;iBA2BI,iBAAiB,WAAW;;;;;;;;;;;;;;;;;;;;KAsDhC;UAIK;EACf;EACA;EACA;EACA;;EAEA,OAAO;;EAEP,YAAY;;;;;EAKZ;;EAEA;;;;;;;;;;EAUA;;UAGe;;EAEf;;EAEA,OAAO;;EAEP,YAAY;;EAEZ;;EAEA;;EAEA;;;;;EAKA,UAAU;;cAKC;cACA;cACA,cAAc;cACd;;;;;UAMI;EACf;EACA;EACA;EACA;EACA,MAAM;;EAEN;EACA;EACA;;EAEA;;iBAGc,cAAc,QAAQ,cAAc,aAAY,yBAA8B"}
@@ -1,61 +1,54 @@
1
+ //#region src/descriptor.ts
2
+ /**
3
+ * Resolve a `NormalDirection` to an angle in radians, or `null` when
4
+ * the direction is `'flat'` (no tilt).
5
+ *
6
+ * Convention: 0 = +X / right, π/2 = +Y / up, π = -X / left,
7
+ * -π/2 = -Y / down. Matches `Math.atan2` output.
8
+ */
1
9
  function directionToAngle(direction) {
2
- if (direction === void 0 || direction === "flat") return null;
3
- if (typeof direction === "number") return direction;
4
- switch (direction) {
5
- case "right":
6
- case "east":
7
- return 0;
8
- case "up":
9
- case "north":
10
- return Math.PI / 2;
11
- case "left":
12
- case "west":
13
- return Math.PI;
14
- case "down":
15
- case "south":
16
- return -Math.PI / 2;
17
- case "up-right":
18
- case "north-east":
19
- return Math.PI / 4;
20
- case "up-left":
21
- case "north-west":
22
- return 3 * Math.PI / 4;
23
- case "down-right":
24
- case "south-east":
25
- return -Math.PI / 4;
26
- case "down-left":
27
- case "south-west":
28
- return -3 * Math.PI / 4;
29
- }
30
- throw new Error(`unknown NormalDirection: ${String(direction)}`);
10
+ if (direction === void 0 || direction === "flat") return null;
11
+ if (typeof direction === "number") return direction;
12
+ switch (direction) {
13
+ case "right":
14
+ case "east": return 0;
15
+ case "up":
16
+ case "north": return Math.PI / 2;
17
+ case "left":
18
+ case "west": return Math.PI;
19
+ case "down":
20
+ case "south": return -Math.PI / 2;
21
+ case "up-right":
22
+ case "north-east": return Math.PI / 4;
23
+ case "up-left":
24
+ case "north-west": return 3 * Math.PI / 4;
25
+ case "down-right":
26
+ case "south-east": return -Math.PI / 4;
27
+ case "down-left":
28
+ case "south-west": return -3 * Math.PI / 4;
29
+ }
30
+ throw new Error(`unknown NormalDirection: ${String(direction)}`);
31
31
  }
32
32
  const DEFAULT_PITCH = Math.PI / 4;
33
33
  const DEFAULT_STRENGTH = 1;
34
34
  const DEFAULT_BUMP = "alpha";
35
35
  const DEFAULT_ELEVATION = 0;
36
36
  function resolveRegion(region, descriptor = {}) {
37
- const direction = region.direction ?? descriptor.direction ?? "flat";
38
- const angle = directionToAngle(direction);
39
- const elevation = region.elevation ?? descriptor.elevation ?? DEFAULT_ELEVATION;
40
- return {
41
- x: region.x,
42
- y: region.y,
43
- w: region.w,
44
- h: region.h,
45
- bump: region.bump ?? descriptor.bump ?? DEFAULT_BUMP,
46
- angle,
47
- pitch: region.pitch ?? descriptor.pitch ?? DEFAULT_PITCH,
48
- strength: region.strength ?? descriptor.strength ?? DEFAULT_STRENGTH,
49
- // Clamp to [0, 1] so stored B channel can't overflow.
50
- elevation: Math.max(0, Math.min(1, elevation))
51
- };
37
+ const angle = directionToAngle(region.direction ?? descriptor.direction ?? "flat");
38
+ const elevation = region.elevation ?? descriptor.elevation ?? 0;
39
+ return {
40
+ x: region.x,
41
+ y: region.y,
42
+ w: region.w,
43
+ h: region.h,
44
+ bump: region.bump ?? descriptor.bump ?? "alpha",
45
+ angle,
46
+ pitch: region.pitch ?? descriptor.pitch ?? DEFAULT_PITCH,
47
+ strength: region.strength ?? descriptor.strength ?? 1,
48
+ elevation: Math.max(0, Math.min(1, elevation))
49
+ };
52
50
  }
53
- export {
54
- DEFAULT_BUMP,
55
- DEFAULT_ELEVATION,
56
- DEFAULT_PITCH,
57
- DEFAULT_STRENGTH,
58
- directionToAngle,
59
- resolveRegion
60
- };
51
+ //#endregion
52
+ export { DEFAULT_BUMP, DEFAULT_ELEVATION, DEFAULT_PITCH, DEFAULT_STRENGTH, directionToAngle, resolveRegion };
53
+
61
54
  //# sourceMappingURL=descriptor.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/descriptor.ts"],"sourcesContent":["/**\n * Normal source descriptor — the shared shape used across three\n * surfaces:\n *\n * 1. Library API `bakeNormalMap(pixels, w, h, descriptor)`\n * 2. CLI `flatland-bake normal --descriptor <file>`\n * 3. Loader API `LDtkLoader.load(url, { normals: descriptor })`\n *\n * The descriptor carries enough information for the baker to emit a\n * 1:1 co-registered normal map: where the regions live, how each\n * region tilts, and what per-texel bump source to use.\n *\n * All types are pure and browser-safe — consumed identically by node\n * bakers and browser runtime loaders.\n */\n\n// ─── Direction ────────────────────────────────────────────────────────────\n\n/**\n * Screen-relative direction the surface normal's XY component points.\n *\n * Mental model: imagine the baked normal as an arrow. `NormalDirection`\n * is the screen-space direction that arrow tilts toward — equivalently,\n * the direction the tile's visible face is \"pointing.\" For a wall at\n * the top of the map (visible face toward the camera below), that's\n * `'south'` / `'down'`.\n *\n * Cardinal and compass aliases are equivalent; the canonical form in\n * the codebase is NSEW. Numbers are interpreted as radians in standard\n * math convention (0 = +X / right, CCW positive).\n */\nexport type NormalDirection =\n | 'flat'\n | 'up'\n | 'north'\n | 'down'\n | 'south'\n | 'left'\n | 'west'\n | 'right'\n | 'east'\n | 'up-left'\n | 'north-west'\n | 'up-right'\n | 'north-east'\n | 'down-left'\n | 'south-west'\n | 'down-right'\n | 'south-east'\n | number\n\n/**\n * Resolve a `NormalDirection` to an angle in radians, or `null` when\n * the direction is `'flat'` (no tilt).\n *\n * Convention: 0 = +X / right, π/2 = +Y / up, π = -X / left,\n * -π/2 = -Y / down. Matches `Math.atan2` output.\n */\nexport function directionToAngle(\n direction: NormalDirection | undefined\n): number | null {\n if (direction === undefined || direction === 'flat') return null\n if (typeof direction === 'number') return direction\n\n switch (direction) {\n case 'right':\n case 'east':\n return 0\n case 'up':\n case 'north':\n return Math.PI / 2\n case 'left':\n case 'west':\n return Math.PI\n case 'down':\n case 'south':\n return -Math.PI / 2\n case 'up-right':\n case 'north-east':\n return Math.PI / 4\n case 'up-left':\n case 'north-west':\n return (3 * Math.PI) / 4\n case 'down-right':\n case 'south-east':\n return -Math.PI / 4\n case 'down-left':\n case 'south-west':\n return (-3 * Math.PI) / 4\n }\n throw new Error(`unknown NormalDirection: ${String(direction)}`)\n}\n\n// ─── Bump ─────────────────────────────────────────────────────────────────\n\n/**\n * Per-texel bump source inside a region. Each non-'none' mode runs a\n * central-difference gradient on the named channel; the result is\n * treated as a height field where high values are raised and low\n * values are sunken. Negative `strength` inverts (dark = raised).\n *\n * - `'alpha'` — gradient on the source alpha channel (default).\n * Preserves sprite silhouette edges — the classic path for\n * transparent sprites. Solid opaque regions produce no bump.\n * - `'luminance'` — gradient on Rec. 709 luminance `(0.2126·R +\n * 0.7152·G + 0.0722·B)`. Brick faces read as raised; dark mortar\n * lines read as sunken grooves. Right mode for solid opaque\n * tilesets.\n * - `'red'` / `'green'` / `'blue'` — gradient on a single color\n * channel. Use when your art treats one channel as a height map\n * (e.g., packing height into the red channel of a data texture).\n * - `'none'` — flat fill at the region's tilt direction. No per-texel\n * variation; cheapest and useful for uniform surfaces.\n */\nexport type NormalBump =\n | 'alpha'\n | 'luminance'\n | 'red'\n | 'green'\n | 'blue'\n | 'none'\n\n// ─── Regions + descriptor ─────────────────────────────────────────────────\n\nexport interface NormalRegion {\n x: number\n y: number\n w: number\n h: number\n /** Per-texel bump source. Default inherits from descriptor (`'alpha'`). */\n bump?: NormalBump\n /** Base tilt direction. Default inherits from descriptor (`'flat'`). */\n direction?: NormalDirection\n /**\n * Tilt angle in radians from flat. Ignored when `direction === 'flat'`.\n * Default inherits from descriptor, which defaults to `Math.PI / 4`.\n */\n pitch?: number\n /** Gradient strength multiplier for this region. Default 1. */\n strength?: number\n /**\n * World-space elevation of the region in [0, 1], where 0 = ground\n * plane (floor) and 1 = top-of-wall (cap). Baked into the normal\n * atlas's B channel and consumed by the light pass to compute\n * per-fragment light direction (`L.z = lightHeight − elevation`).\n *\n * Default inherits from the descriptor, which defaults to 0. Cap\n * regions typically set 1; tilted face regions typically set 0.5.\n */\n elevation?: number\n}\n\nexport interface NormalSourceDescriptor {\n /** Reserved for future schema evolution. Currently always `1`. */\n version?: 1\n /** Default bump source for regions that don't specify one. Default `'alpha'`. */\n bump?: NormalBump\n /** Default tilt for regions that don't specify one. Default `'flat'`. */\n direction?: NormalDirection\n /** Default tilt pitch in radians. Default `Math.PI / 4`. */\n pitch?: number\n /** Default gradient strength. Default `1`. */\n strength?: number\n /** Default elevation for regions that don't specify one. Default 0 (ground). */\n elevation?: number\n /**\n * Regions of the source texture. When omitted, the whole texture is\n * treated as a single region inheriting all descriptor defaults.\n */\n regions?: NormalRegion[]\n}\n\n// ─── Defaults ─────────────────────────────────────────────────────────────\n\nexport const DEFAULT_PITCH = Math.PI / 4\nexport const DEFAULT_STRENGTH = 1\nexport const DEFAULT_BUMP: NormalBump = 'alpha'\nexport const DEFAULT_ELEVATION = 0\n\n/**\n * Merge a region against its parent descriptor defaults. Returns a\n * fully-populated region suitable for direct consumption by the baker.\n */\nexport interface ResolvedNormalRegion {\n x: number\n y: number\n w: number\n h: number\n bump: NormalBump\n /** Tilt angle in radians, or `null` when the region is flat. */\n angle: number | null\n pitch: number\n strength: number\n /** World-space elevation in [0, 1]. Written to the output B channel. */\n elevation: number\n}\n\nexport function resolveRegion(\n region: NormalRegion,\n descriptor: NormalSourceDescriptor = {}\n): ResolvedNormalRegion {\n const direction = region.direction ?? descriptor.direction ?? 'flat'\n const angle = directionToAngle(direction)\n const elevation =\n region.elevation ?? descriptor.elevation ?? DEFAULT_ELEVATION\n return {\n x: region.x,\n y: region.y,\n w: region.w,\n h: region.h,\n bump: region.bump ?? descriptor.bump ?? DEFAULT_BUMP,\n angle,\n pitch: region.pitch ?? descriptor.pitch ?? DEFAULT_PITCH,\n strength: region.strength ?? descriptor.strength ?? DEFAULT_STRENGTH,\n // Clamp to [0, 1] so stored B channel can't overflow.\n elevation: Math.max(0, Math.min(1, elevation)),\n }\n}\n"],"mappings":"AA0DO,SAAS,iBACd,WACe;AACf,MAAI,cAAc,UAAa,cAAc,OAAQ,QAAO;AAC5D,MAAI,OAAO,cAAc,SAAU,QAAO;AAE1C,UAAQ,WAAW;AAAA,IACjB,KAAK;AAAA,IACL,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AAAA,IACL,KAAK;AACH,aAAO,KAAK,KAAK;AAAA,IACnB,KAAK;AAAA,IACL,KAAK;AACH,aAAO,KAAK;AAAA,IACd,KAAK;AAAA,IACL,KAAK;AACH,aAAO,CAAC,KAAK,KAAK;AAAA,IACpB,KAAK;AAAA,IACL,KAAK;AACH,aAAO,KAAK,KAAK;AAAA,IACnB,KAAK;AAAA,IACL,KAAK;AACH,aAAQ,IAAI,KAAK,KAAM;AAAA,IACzB,KAAK;AAAA,IACL,KAAK;AACH,aAAO,CAAC,KAAK,KAAK;AAAA,IACpB,KAAK;AAAA,IACL,KAAK;AACH,aAAQ,KAAK,KAAK,KAAM;AAAA,EAC5B;AACA,QAAM,IAAI,MAAM,4BAA4B,OAAO,SAAS,CAAC,EAAE;AACjE;AAmFO,MAAM,gBAAgB,KAAK,KAAK;AAChC,MAAM,mBAAmB;AACzB,MAAM,eAA2B;AACjC,MAAM,oBAAoB;AAoB1B,SAAS,cACd,QACA,aAAqC,CAAC,GAChB;AACtB,QAAM,YAAY,OAAO,aAAa,WAAW,aAAa;AAC9D,QAAM,QAAQ,iBAAiB,SAAS;AACxC,QAAM,YACJ,OAAO,aAAa,WAAW,aAAa;AAC9C,SAAO;AAAA,IACL,GAAG,OAAO;AAAA,IACV,GAAG,OAAO;AAAA,IACV,GAAG,OAAO;AAAA,IACV,GAAG,OAAO;AAAA,IACV,MAAM,OAAO,QAAQ,WAAW,QAAQ;AAAA,IACxC;AAAA,IACA,OAAO,OAAO,SAAS,WAAW,SAAS;AAAA,IAC3C,UAAU,OAAO,YAAY,WAAW,YAAY;AAAA;AAAA,IAEpD,WAAW,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,SAAS,CAAC;AAAA,EAC/C;AACF;","names":[]}
1
+ {"version":3,"file":"descriptor.js","names":[],"sources":["../src/descriptor.ts"],"sourcesContent":["/**\n * Normal source descriptor — the shared shape used across three\n * surfaces:\n *\n * 1. Library API `bakeNormalMap(pixels, w, h, descriptor)`\n * 2. CLI `flatland-bake normal --descriptor <file>`\n * 3. Loader API `LDtkLoader.load(url, { normals: descriptor })`\n *\n * The descriptor carries enough information for the baker to emit a\n * 1:1 co-registered normal map: where the regions live, how each\n * region tilts, and what per-texel bump source to use.\n *\n * All types are pure and browser-safe — consumed identically by node\n * bakers and browser runtime loaders.\n */\n\n// ─── Direction ────────────────────────────────────────────────────────────\n\n/**\n * Screen-relative direction the surface normal's XY component points.\n *\n * Mental model: imagine the baked normal as an arrow. `NormalDirection`\n * is the screen-space direction that arrow tilts toward — equivalently,\n * the direction the tile's visible face is \"pointing.\" For a wall at\n * the top of the map (visible face toward the camera below), that's\n * `'south'` / `'down'`.\n *\n * Cardinal and compass aliases are equivalent; the canonical form in\n * the codebase is NSEW. Numbers are interpreted as radians in standard\n * math convention (0 = +X / right, CCW positive).\n */\nexport type NormalDirection =\n | 'flat'\n | 'up'\n | 'north'\n | 'down'\n | 'south'\n | 'left'\n | 'west'\n | 'right'\n | 'east'\n | 'up-left'\n | 'north-west'\n | 'up-right'\n | 'north-east'\n | 'down-left'\n | 'south-west'\n | 'down-right'\n | 'south-east'\n | number\n\n/**\n * Resolve a `NormalDirection` to an angle in radians, or `null` when\n * the direction is `'flat'` (no tilt).\n *\n * Convention: 0 = +X / right, π/2 = +Y / up, π = -X / left,\n * -π/2 = -Y / down. Matches `Math.atan2` output.\n */\nexport function directionToAngle(direction: NormalDirection | undefined): number | null {\n if (direction === undefined || direction === 'flat') return null\n if (typeof direction === 'number') return direction\n\n switch (direction) {\n case 'right':\n case 'east':\n return 0\n case 'up':\n case 'north':\n return Math.PI / 2\n case 'left':\n case 'west':\n return Math.PI\n case 'down':\n case 'south':\n return -Math.PI / 2\n case 'up-right':\n case 'north-east':\n return Math.PI / 4\n case 'up-left':\n case 'north-west':\n return (3 * Math.PI) / 4\n case 'down-right':\n case 'south-east':\n return -Math.PI / 4\n case 'down-left':\n case 'south-west':\n return (-3 * Math.PI) / 4\n }\n throw new Error(`unknown NormalDirection: ${String(direction)}`)\n}\n\n// ─── Bump ─────────────────────────────────────────────────────────────────\n\n/**\n * Per-texel bump source inside a region. Each non-'none' mode runs a\n * central-difference gradient on the named channel; the result is\n * treated as a height field where high values are raised and low\n * values are sunken. Negative `strength` inverts (dark = raised).\n *\n * - `'alpha'` — gradient on the source alpha channel (default).\n * Preserves sprite silhouette edges — the classic path for\n * transparent sprites. Solid opaque regions produce no bump.\n * - `'luminance'` — gradient on Rec. 709 luminance `(0.2126·R +\n * 0.7152·G + 0.0722·B)`. Brick faces read as raised; dark mortar\n * lines read as sunken grooves. Right mode for solid opaque\n * tilesets.\n * - `'red'` / `'green'` / `'blue'` — gradient on a single color\n * channel. Use when your art treats one channel as a height map\n * (e.g., packing height into the red channel of a data texture).\n * - `'none'` — flat fill at the region's tilt direction. No per-texel\n * variation; cheapest and useful for uniform surfaces.\n */\nexport type NormalBump = 'alpha' | 'luminance' | 'red' | 'green' | 'blue' | 'none'\n\n// ─── Regions + descriptor ─────────────────────────────────────────────────\n\nexport interface NormalRegion {\n x: number\n y: number\n w: number\n h: number\n /** Per-texel bump source. Default inherits from descriptor (`'alpha'`). */\n bump?: NormalBump\n /** Base tilt direction. Default inherits from descriptor (`'flat'`). */\n direction?: NormalDirection\n /**\n * Tilt angle in radians from flat. Ignored when `direction === 'flat'`.\n * Default inherits from descriptor, which defaults to `Math.PI / 4`.\n */\n pitch?: number\n /** Gradient strength multiplier for this region. Default 1. */\n strength?: number\n /**\n * World-space elevation of the region in [0, 1], where 0 = ground\n * plane (floor) and 1 = top-of-wall (cap). Baked into the normal\n * atlas's B channel and consumed by the light pass to compute\n * per-fragment light direction (`L.z = lightHeight − elevation`).\n *\n * Default inherits from the descriptor, which defaults to 0. Cap\n * regions typically set 1; tilted face regions typically set 0.5.\n */\n elevation?: number\n}\n\nexport interface NormalSourceDescriptor {\n /** Reserved for future schema evolution. Currently always `1`. */\n version?: 1\n /** Default bump source for regions that don't specify one. Default `'alpha'`. */\n bump?: NormalBump\n /** Default tilt for regions that don't specify one. Default `'flat'`. */\n direction?: NormalDirection\n /** Default tilt pitch in radians. Default `Math.PI / 4`. */\n pitch?: number\n /** Default gradient strength. Default `1`. */\n strength?: number\n /** Default elevation for regions that don't specify one. Default 0 (ground). */\n elevation?: number\n /**\n * Regions of the source texture. When omitted, the whole texture is\n * treated as a single region inheriting all descriptor defaults.\n */\n regions?: NormalRegion[]\n}\n\n// ─── Defaults ─────────────────────────────────────────────────────────────\n\nexport const DEFAULT_PITCH = Math.PI / 4\nexport const DEFAULT_STRENGTH = 1\nexport const DEFAULT_BUMP: NormalBump = 'alpha'\nexport const DEFAULT_ELEVATION = 0\n\n/**\n * Merge a region against its parent descriptor defaults. Returns a\n * fully-populated region suitable for direct consumption by the baker.\n */\nexport interface ResolvedNormalRegion {\n x: number\n y: number\n w: number\n h: number\n bump: NormalBump\n /** Tilt angle in radians, or `null` when the region is flat. */\n angle: number | null\n pitch: number\n strength: number\n /** World-space elevation in [0, 1]. Written to the output B channel. */\n elevation: number\n}\n\nexport function resolveRegion(region: NormalRegion, descriptor: NormalSourceDescriptor = {}): ResolvedNormalRegion {\n const direction = region.direction ?? descriptor.direction ?? 'flat'\n const angle = directionToAngle(direction)\n const elevation = region.elevation ?? descriptor.elevation ?? DEFAULT_ELEVATION\n return {\n x: region.x,\n y: region.y,\n w: region.w,\n h: region.h,\n bump: region.bump ?? descriptor.bump ?? DEFAULT_BUMP,\n angle,\n pitch: region.pitch ?? descriptor.pitch ?? DEFAULT_PITCH,\n strength: region.strength ?? descriptor.strength ?? DEFAULT_STRENGTH,\n // Clamp to [0, 1] so stored B channel can't overflow.\n elevation: Math.max(0, Math.min(1, elevation)),\n }\n}\n"],"mappings":";;;;;;;;AA0DA,SAAgB,iBAAiB,WAAuD;CACtF,IAAI,cAAc,KAAA,KAAa,cAAc,QAAQ,OAAO;CAC5D,IAAI,OAAO,cAAc,UAAU,OAAO;CAE1C,QAAQ,WAAR;EACE,KAAK;EACL,KAAK,QACH,OAAO;EACT,KAAK;EACL,KAAK,SACH,OAAO,KAAK,KAAK;EACnB,KAAK;EACL,KAAK,QACH,OAAO,KAAK;EACd,KAAK;EACL,KAAK,SACH,OAAO,CAAC,KAAK,KAAK;EACpB,KAAK;EACL,KAAK,cACH,OAAO,KAAK,KAAK;EACnB,KAAK;EACL,KAAK,cACH,OAAQ,IAAI,KAAK,KAAM;EACzB,KAAK;EACL,KAAK,cACH,OAAO,CAAC,KAAK,KAAK;EACpB,KAAK;EACL,KAAK,cACH,OAAQ,KAAK,KAAK,KAAM;CAC5B;CACA,MAAM,IAAI,MAAM,4BAA4B,OAAO,SAAS,GAAG;AACjE;AA6EA,MAAa,gBAAgB,KAAK,KAAK;AACvC,MAAa,mBAAmB;AAChC,MAAa,eAA2B;AACxC,MAAa,oBAAoB;AAoBjC,SAAgB,cAAc,QAAsB,aAAqC,CAAC,GAAyB;CAEjH,MAAM,QAAQ,iBADI,OAAO,aAAa,WAAW,aAAa,MACtB;CACxC,MAAM,YAAY,OAAO,aAAa,WAAW,aAAA;CACjD,OAAO;EACL,GAAG,OAAO;EACV,GAAG,OAAO;EACV,GAAG,OAAO;EACV,GAAG,OAAO;EACV,MAAM,OAAO,QAAQ,WAAW,QAAA;EAChC;EACA,OAAO,OAAO,SAAS,WAAW,SAAS;EAC3C,UAAU,OAAO,YAAY,WAAW,YAAA;EAExC,WAAW,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,SAAS,CAAC;CAC/C;AACF"}