@developerz.ai/ts23d 0.1.0 → 0.2.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.
Files changed (162) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/CLAUDE.md +1 -1
  3. package/README.md +10 -5
  4. package/dist/{chunk-a8fxakcj.js → chunk-6b05mnmx.js} +2 -2
  5. package/dist/{chunk-kgvbpqqw.js → chunk-7rtfjh9f.js} +6 -5
  6. package/dist/chunk-7rtfjh9f.js.map +10 -0
  7. package/dist/{chunk-1ypfs7ga.js → chunk-8j4c28jt.js} +790 -276
  8. package/dist/chunk-8j4c28jt.js.map +46 -0
  9. package/dist/{chunk-wtqhq529.js → chunk-d1fv6hrm.js} +6 -6
  10. package/dist/{chunk-wtqhq529.js.map → chunk-d1fv6hrm.js.map} +1 -1
  11. package/dist/{chunk-gcyp9b4d.js → chunk-dbq4qf52.js} +362 -215
  12. package/dist/chunk-dbq4qf52.js.map +25 -0
  13. package/dist/{chunk-xnt2sdhf.js → chunk-drna2rny.js} +2 -2
  14. package/dist/{chunk-ravbb06q.js → chunk-dxvypr7y.js} +2 -2
  15. package/dist/{chunk-11v28977.js → chunk-f6yme003.js} +85 -71
  16. package/dist/{chunk-11v28977.js.map → chunk-f6yme003.js.map} +6 -6
  17. package/dist/{chunk-4ddzsqtc.js → chunk-hg7pf34j.js} +4 -3
  18. package/dist/{chunk-4ddzsqtc.js.map → chunk-hg7pf34j.js.map} +5 -5
  19. package/dist/{chunk-w1zyhj40.js → chunk-jq4e17tj.js} +2 -2
  20. package/dist/{chunk-kvcep7ft.js → chunk-y63c6zcv.js} +967 -218
  21. package/dist/chunk-y63c6zcv.js.map +53 -0
  22. package/dist/cli/args.d.ts +7 -1
  23. package/dist/cli/bin.js +66 -25
  24. package/dist/cli/bin.js.map +6 -5
  25. package/dist/cli/manifest.d.ts +5 -1
  26. package/dist/cli/texture-cache.d.ts +4 -0
  27. package/dist/core/definition.d.ts +7 -0
  28. package/dist/core/errors.d.ts +1 -1
  29. package/dist/core/index.js +1 -1
  30. package/dist/export/credits.d.ts +4 -0
  31. package/dist/export/glb-options.d.ts +48 -0
  32. package/dist/export/glb.d.ts +5 -35
  33. package/dist/export/hierarchy.d.ts +23 -0
  34. package/dist/export/index.d.ts +2 -1
  35. package/dist/export/index.js +21 -7
  36. package/dist/export/index.js.map +1 -1
  37. package/dist/export/lod-coverage.d.ts +12 -0
  38. package/dist/export/metadata.d.ts +2 -0
  39. package/dist/export/version.d.ts +2 -2
  40. package/dist/grammar/index.js +2 -2
  41. package/dist/index.js +63 -17
  42. package/dist/index.js.map +1 -1
  43. package/dist/kinds/head-armour.d.ts +3 -7
  44. package/dist/kinds/head-bust.d.ts +3 -2
  45. package/dist/kinds/head-face.d.ts +3 -1
  46. package/dist/kinds/head-garb.d.ts +4 -0
  47. package/dist/kinds/head-mesh.d.ts +4 -0
  48. package/dist/kinds/head-mouth.d.ts +4 -0
  49. package/dist/kinds/head-params.d.ts +9 -1
  50. package/dist/kinds/head-tone.d.ts +10 -0
  51. package/dist/kinds/head-torso.d.ts +35 -0
  52. package/dist/kinds/index.js +6 -6
  53. package/dist/material/cc0.d.ts +4 -0
  54. package/dist/material/define.d.ts +34 -0
  55. package/dist/material/generator.d.ts +1 -1
  56. package/dist/material/index.d.ts +8 -3
  57. package/dist/material/index.js +39 -7
  58. package/dist/material/index.js.map +1 -1
  59. package/dist/material/library.d.ts +7 -0
  60. package/dist/material/material-def.d.ts +30 -0
  61. package/dist/material/paint/canvas.d.ts +7 -0
  62. package/dist/material/paint/default-sheet.d.ts +3 -1
  63. package/dist/material/paint/fabric.d.ts +4 -0
  64. package/dist/material/paint/image-strip.d.ts +4 -0
  65. package/dist/material/paint/kit.d.ts +5 -0
  66. package/dist/material/paint/metal.d.ts +3 -0
  67. package/dist/material/paint/orm.d.ts +1 -1
  68. package/dist/material/paint/strip.d.ts +1 -1
  69. package/dist/material/paint/types.d.ts +32 -2
  70. package/dist/material/palette.d.ts +2 -0
  71. package/dist/material/slots.d.ts +5 -0
  72. package/dist/material/texture-source.d.ts +34 -0
  73. package/dist/material/trim-sheet.d.ts +20 -4
  74. package/dist/neural/index.js +2 -2
  75. package/dist/passes/index.js +2 -2
  76. package/dist/primitives/index.js +3 -3
  77. package/dist/qa/index.js +2 -2
  78. package/dist/render/beauty.d.ts +1 -1
  79. package/dist/render/index.js +3 -3
  80. package/dist/render/lighting.d.ts +7 -0
  81. package/dist/render/shade.d.ts +1 -1
  82. package/dist/testing/index.js +5 -5
  83. package/docs/README.md +4 -0
  84. package/docs/architecture/gltf-capabilities.md +38 -1
  85. package/docs/architecture/materials.md +151 -0
  86. package/docs/idea/10-sculpting.md +166 -0
  87. package/docs/idea/11-materials-authoring.md +132 -0
  88. package/docs/idea/12-blender-bridge.md +136 -0
  89. package/docs/idea/13-asset-pipeline-research.md +89 -0
  90. package/docs/plans/roadmap.md +12 -0
  91. package/package.json +2 -1
  92. package/recipes/boxcar/README.md +7 -0
  93. package/recipes/boxcar/boxcar.ts +118 -34
  94. package/recipes/goblin_head/README.md +27 -14
  95. package/recipes/goblin_head/goblin_head.ts +11 -9
  96. package/recipes/human_head/README.md +8 -4
  97. package/recipes/human_head/human_head.ts +4 -3
  98. package/recipes/material_swatches/material_swatches.ts +11 -5
  99. package/recipes/orc_head/README.md +32 -20
  100. package/recipes/orc_head/orc_head.ts +9 -7
  101. package/src/cli/args.ts +16 -3
  102. package/src/cli/manifest.ts +6 -0
  103. package/src/cli/run.ts +33 -9
  104. package/src/cli/texture-cache.ts +24 -0
  105. package/src/core/asset.ts +1 -0
  106. package/src/core/definition.ts +7 -0
  107. package/src/core/errors.ts +6 -1
  108. package/src/export/credits.ts +25 -0
  109. package/src/export/glb-options.ts +53 -0
  110. package/src/export/glb.ts +100 -113
  111. package/src/export/hierarchy.ts +122 -0
  112. package/src/export/index.ts +17 -1
  113. package/src/export/lod-coverage.ts +71 -0
  114. package/src/export/metadata.ts +3 -0
  115. package/src/export/version.ts +1 -1
  116. package/src/kinds/head-armour.ts +103 -73
  117. package/src/kinds/head-bust.ts +49 -86
  118. package/src/kinds/head-face.ts +94 -74
  119. package/src/kinds/head-garb.ts +181 -0
  120. package/src/kinds/head-hair.ts +74 -33
  121. package/src/kinds/head-mesh.ts +43 -5
  122. package/src/kinds/head-mouth.ts +111 -0
  123. package/src/kinds/head-params.ts +11 -2
  124. package/src/kinds/head-presets.ts +30 -27
  125. package/src/kinds/head-tone.ts +52 -0
  126. package/src/kinds/head-torso.ts +230 -0
  127. package/src/kinds/head.ts +8 -3
  128. package/src/material/cc0.ts +41 -0
  129. package/src/material/define.ts +130 -0
  130. package/src/material/generator.ts +15 -8
  131. package/src/material/index.ts +34 -3
  132. package/src/material/library.ts +196 -0
  133. package/src/material/material-def.ts +84 -0
  134. package/src/material/paint/canvas.ts +11 -1
  135. package/src/material/paint/compose.ts +1 -1
  136. package/src/material/paint/decode-png.ts +19 -13
  137. package/src/material/paint/default-sheet.ts +40 -28
  138. package/src/material/paint/fabric.ts +63 -0
  139. package/src/material/paint/image-strip.ts +88 -0
  140. package/src/material/paint/kit.ts +37 -0
  141. package/src/material/paint/masks.ts +3 -1
  142. package/src/material/paint/metal.ts +109 -0
  143. package/src/material/paint/orm.ts +14 -1
  144. package/src/material/paint/planks.ts +49 -4
  145. package/src/material/paint/strip.ts +35 -29
  146. package/src/material/paint/types.ts +45 -2
  147. package/src/material/paint/validate.ts +19 -3
  148. package/src/material/palette.ts +3 -0
  149. package/src/material/slots.ts +18 -0
  150. package/src/material/texture-source.ts +86 -0
  151. package/src/material/trim-sheet.ts +79 -13
  152. package/src/render/beauty.ts +2 -2
  153. package/src/render/lighting.ts +26 -0
  154. package/src/render/shade.ts +10 -5
  155. package/dist/chunk-1ypfs7ga.js.map +0 -42
  156. package/dist/chunk-gcyp9b4d.js.map +0 -22
  157. package/dist/chunk-kgvbpqqw.js.map +0 -10
  158. package/dist/chunk-kvcep7ft.js.map +0 -43
  159. /package/dist/{chunk-a8fxakcj.js.map → chunk-6b05mnmx.js.map} +0 -0
  160. /package/dist/{chunk-xnt2sdhf.js.map → chunk-drna2rny.js.map} +0 -0
  161. /package/dist/{chunk-ravbb06q.js.map → chunk-dxvypr7y.js.map} +0 -0
  162. /package/dist/{chunk-w1zyhj40.js.map → chunk-jq4e17tj.js.map} +0 -0
@@ -1,8 +1,590 @@
1
1
  import {
2
2
  Ts23dError2,
3
3
  createRng2
4
- } from "./chunk-4ddzsqtc.js";
4
+ } from "./chunk-hg7pf34j.js";
5
5
 
6
+ // src/material/cc0.ts
7
+ var PH = "https://dl.polyhaven.org/file/ph-assets/Textures/png/1k/wood_planks_grey";
8
+ var CC0_WOOD_PLANKS_GREY2 = {
9
+ id: "ph_wood_planks_grey",
10
+ kind: "flat",
11
+ scaleMetres: 0.6,
12
+ roughness: 0.85,
13
+ description: "Photo-scanned weathered grey planks (Poly Haven, CC0).",
14
+ sources: ["https://polyhaven.com/a/wood_planks_grey"],
15
+ texture: {
16
+ source: "polyhaven",
17
+ id: "wood_planks_grey",
18
+ license: "CC0-1.0",
19
+ page: "https://polyhaven.com/a/wood_planks_grey",
20
+ author: "Rob Tuytel",
21
+ metres: [1.5, 1.5],
22
+ maps: {
23
+ albedo: {
24
+ url: `${PH}/wood_planks_grey_diff_1k.png`,
25
+ sha256: "3e6aeb842edd813759fdc96a6ffe6b695c888280f42501de589442311749ac5d"
26
+ },
27
+ normal: {
28
+ url: `${PH}/wood_planks_grey_nor_gl_1k.png`,
29
+ sha256: "a9f2ad602eca2d59e84f203b1087e819e62ae517532bb25f7eb02a13c826e08b"
30
+ },
31
+ arm: {
32
+ url: `${PH}/wood_planks_grey_arm_1k.png`,
33
+ sha256: "9b81e9a0b4f9db0a920c4c2ce8ff13e0411b3624d4de455acdabe41e3fafe20e"
34
+ }
35
+ }
36
+ }
37
+ };
38
+ var CC0_MATERIALS2 = [CC0_WOOD_PLANKS_GREY2];
39
+ // src/material/library.ts
40
+ var PB = "https://physicallybased.info";
41
+ var ROBLOX = "https://create.roblox.com/docs/art/modeling/material-reference";
42
+ var PBR_GUIDE = "https://www.adobe.com/learn/substance-3d-designer/web/the-pbr-guide-part-2";
43
+ var MATERIAL_LIBRARY2 = {
44
+ oak: {
45
+ id: "oak",
46
+ kind: "planks",
47
+ scaleMetres: 0.6,
48
+ roughness: 0.68,
49
+ albedo: "#a27b52",
50
+ albedo2: "#6a482b",
51
+ params: { width: 0.14, rings: 0.55, rays: 0.6, knots: 0.6 },
52
+ description: "Oak boards: warm brown, dark latewood rings, pale ray flecks.",
53
+ sources: [PBR_GUIDE, ROBLOX]
54
+ },
55
+ pine: {
56
+ id: "pine",
57
+ kind: "planks",
58
+ scaleMetres: 0.6,
59
+ roughness: 0.72,
60
+ albedo: "#d2b183",
61
+ albedo2: "#9b6a3c",
62
+ params: { width: 0.16, rings: 0.75, knots: 2.5 },
63
+ description: "Pine boards: pale yellow, strong orange latewood, frequent knots.",
64
+ sources: [PBR_GUIDE, ROBLOX]
65
+ },
66
+ wood_weathered: {
67
+ id: "wood_weathered",
68
+ kind: "planks",
69
+ scaleMetres: 0.6,
70
+ roughness: 0.86,
71
+ albedo: "#9b968c",
72
+ albedo2: "#4f463c",
73
+ params: { width: 0.2, rings: 0.35, checks: 1, knots: 1.5, grime: 0.15 },
74
+ description: "Barn-grey weathered boards: silvered, eroded grain, open checks.",
75
+ sources: [ROBLOX]
76
+ },
77
+ wood_painted: {
78
+ id: "wood_painted",
79
+ kind: "planks",
80
+ scaleMetres: 0.6,
81
+ roughness: 0.84,
82
+ albedo: "#ededed",
83
+ albedo2: "#a08c74",
84
+ params: { width: 0.14, paint: 1, paintRough: 0.62, chips: 0.38, knots: 0.8, grime: 0.3 },
85
+ description: "Painted boards (vertex colour = paint) worn through to grey wood at the edges.",
86
+ sources: [ROBLOX]
87
+ },
88
+ iron_rust: {
89
+ id: "iron_rust",
90
+ kind: "metal",
91
+ scaleMetres: 0.5,
92
+ roughness: 0.55,
93
+ metalness: 0.9,
94
+ albedo: "#8e8a85",
95
+ albedo2: "#764323",
96
+ params: { oxide: 0.62, cast: 0.25, streaks: 0.5 },
97
+ description: "Weathered wrought/rolled iron, rust blooming in patches and runs.",
98
+ sources: [PB, ROBLOX]
99
+ },
100
+ iron_cast: {
101
+ id: "iron_cast",
102
+ kind: "metal",
103
+ scaleMetres: 0.5,
104
+ roughness: 0.68,
105
+ metalness: 0.75,
106
+ albedo: "#6c6966",
107
+ albedo2: "#55372a",
108
+ params: { cast: 1, oxide: 0.35 },
109
+ description: "Sand-cast iron (wheels, frames): dark pitted skin, dull oxide.",
110
+ sources: [PB]
111
+ },
112
+ steel_painted: {
113
+ id: "steel_painted",
114
+ kind: "metal",
115
+ scaleMetres: 0.5,
116
+ roughness: 0.4,
117
+ metalness: 1,
118
+ albedo: "#eeeeee",
119
+ albedo2: "#5a3b2a",
120
+ params: { paint: 1, paintRough: 0.42, chips: 0.2, streaks: 0.2, grime: 0.2 },
121
+ description: "Painted steel (vertex colour = paint): chips show rusty steel beneath.",
122
+ sources: [ROBLOX]
123
+ },
124
+ steel_galvanised: {
125
+ id: "steel_galvanised",
126
+ kind: "metal",
127
+ scaleMetres: 0.5,
128
+ roughness: 0.42,
129
+ metalness: 1,
130
+ albedo: "#d5dadc",
131
+ albedo2: "#9a9c98",
132
+ params: { spangle: 1, oxide: 0.12 },
133
+ description: "Hot-dip galvanised steel: zinc spangle, white-rust bloom.",
134
+ sources: [PB]
135
+ },
136
+ brass: {
137
+ id: "brass",
138
+ kind: "metal",
139
+ scaleMetres: 0.5,
140
+ roughness: 0.28,
141
+ metalness: 1,
142
+ albedo: "#f5e5ae",
143
+ albedo2: "#6b5a3a",
144
+ params: { brush: 0.3, oxide: 0.1 },
145
+ description: "Brass fittings: warm gold reflectance, dark tarnish in patches.",
146
+ sources: [PB, ROBLOX]
147
+ },
148
+ copper: {
149
+ id: "copper",
150
+ kind: "metal",
151
+ scaleMetres: 0.5,
152
+ roughness: 0.32,
153
+ metalness: 1,
154
+ albedo: "#f2b89c",
155
+ albedo2: "#6fa58f",
156
+ params: { oxide: 0.15 },
157
+ description: "Copper: salmon reflectance with verdigris patches.",
158
+ sources: [PB]
159
+ },
160
+ aluminium: {
161
+ id: "aluminium",
162
+ kind: "metal",
163
+ scaleMetres: 0.5,
164
+ roughness: 0.34,
165
+ metalness: 1,
166
+ albedo: "#e6e8e9",
167
+ params: { brush: 1 },
168
+ description: "Brushed aluminium: bright, cool, streaked along U.",
169
+ sources: [PB]
170
+ },
171
+ chrome: {
172
+ id: "chrome",
173
+ kind: "metal",
174
+ scaleMetres: 0.5,
175
+ roughness: 0.07,
176
+ metalness: 1,
177
+ albedo: "#d3d9dc",
178
+ description: "Chrome plate: near-mirror.",
179
+ sources: [PB]
180
+ },
181
+ rubber: {
182
+ id: "rubber",
183
+ kind: "noise",
184
+ scaleMetres: 0.5,
185
+ roughness: 0.78,
186
+ albedo: "#2c2c2c",
187
+ params: { tone: 0.95, mottle: 0.06, relief: 0.0002, size: 0.05, grime: 0.2 },
188
+ description: "Vulcanised rubber (tyres, hoses, seals).",
189
+ sources: [PB, ROBLOX]
190
+ },
191
+ canvas: {
192
+ id: "canvas",
193
+ kind: "weave",
194
+ scaleMetres: 1,
195
+ roughness: 0.88,
196
+ albedo: "#cbbea2",
197
+ albedo2: "#8a7c62",
198
+ params: { pitch: 0.004, folds: 0.6, stains: 0.35 },
199
+ description: "Cotton duck canvas: natural, weave relief, folds and water stains.",
200
+ sources: [ROBLOX]
201
+ },
202
+ leather: {
203
+ id: "leather",
204
+ kind: "leather",
205
+ scaleMetres: 0.5,
206
+ roughness: 0.58,
207
+ albedo: "#7c5438",
208
+ albedo2: "#3e281a",
209
+ params: { creases: 0.5, wear: 0.4 },
210
+ description: "Worn brown leather: pebble grain, dark creases, burnished wear.",
211
+ sources: [ROBLOX]
212
+ },
213
+ plastic: {
214
+ id: "plastic",
215
+ kind: "noise",
216
+ scaleMetres: 0.5,
217
+ roughness: 0.35,
218
+ albedo: "#ececec",
219
+ params: { tone: 0.97, mottle: 0.01, relief: 0.00004, size: 0.2 },
220
+ description: "Moulded plastic (vertex colour = the plastic colour): satin, even.",
221
+ sources: [ROBLOX]
222
+ }
223
+ };
224
+
225
+ // src/material/palette.ts
226
+ function hex2(value) {
227
+ const n = Number.parseInt(value.replace("#", ""), 16);
228
+ const lin = (c) => {
229
+ const s = c / 255;
230
+ return s <= 0.04045 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;
231
+ };
232
+ return [lin(n >> 16 & 255), lin(n >> 8 & 255), lin(n & 255)];
233
+ }
234
+ var NATURAL2 = [1, 1, 1];
235
+ function definePalette2(entries) {
236
+ return Object.fromEntries(Object.entries(entries).map(([k, v]) => [k, hex2(v)]));
237
+ }
238
+ var palettes2 = {
239
+ victorianRail: definePalette2({
240
+ paint: "#2f4a3a",
241
+ trim: "#b08d57",
242
+ iron: "#2b2b2e",
243
+ brass: "#c9a24a",
244
+ red: "#7a2a22"
245
+ }),
246
+ artDeco: definePalette2({
247
+ limestone: "#d8cfbd",
248
+ steel: "#9aa3ad",
249
+ glass: "#4a6a80",
250
+ bronze: "#8a6a3c"
251
+ }),
252
+ modernGlass: definePalette2({
253
+ glass: "#6f8fa8",
254
+ aluminium: "#c4c9cf",
255
+ concrete: "#a7a39b"
256
+ }),
257
+ harbour: definePalette2({ hull: "#1f2f45", deck: "#8a6f4e", white: "#e8e6df", red: "#a1302a" })
258
+ };
259
+
260
+ // src/material/texture-source.ts
261
+ var SHA = /^[0-9a-f]{64}$/;
262
+ var provided = new Map;
263
+ function provideTextures2(cache) {
264
+ for (const [k, v] of cache)
265
+ provided.set(k, v);
266
+ }
267
+ function textureBytes(file, where) {
268
+ const hit = provided.get(file.sha256);
269
+ if (hit)
270
+ return hit;
271
+ throw new Ts23dError2("T3D_TEXTURE_MISSING", {
272
+ cause: `${where}: texture ${file.url} (sha256 ${file.sha256.slice(0, 12)}…) is not in the cache`,
273
+ fix: "run `bun run textures <module defining the material>` once (downloads + verifies into node_modules/.cache/ts23d/textures), then provideTextures(await loadTextureCache()) before painting"
274
+ });
275
+ }
276
+ function validateTexture(id, t) {
277
+ const fail = (cause, fix) => {
278
+ throw new Ts23dError2("T3D_MATERIAL_INVALID", { cause: `material '${id}': ${cause}`, fix });
279
+ };
280
+ if (!["polyhaven", "ambientcg", "file"].includes(t.source))
281
+ fail(`texture.source '${t.source}'`, "use 'polyhaven', 'ambientcg' or 'file'");
282
+ if (t.license !== "CC0-1.0")
283
+ fail(`texture.license '${t.license}'`, "only CC0-1.0 sets are accepted (Poly Haven and ambientCG are CC0)");
284
+ if (!(t.metres.length === 2 && t.metres.every((m) => Number.isFinite(m) && m > 0)))
285
+ fail(`texture.metres [${t.metres}]`, "give the real size the image covers, e.g. [1.5, 1.5]");
286
+ for (const [map, f] of Object.entries(t.maps)) {
287
+ if (!f)
288
+ continue;
289
+ if (!SHA.test(f.sha256))
290
+ fail(`texture.maps.${map}.sha256 '${f.sha256}'`, "lower-case hex sha256 of the file (sha256sum)");
291
+ if (!/\.png$/i.test(f.url))
292
+ fail(`texture.maps.${map}.url '${f.url}'`, "use the PNG download (8- or 16-bit); JPEG is not decoded");
293
+ }
294
+ }
295
+ var creditOf2 = (t) => `${t.id}${t.author ? ` by ${t.author}` : ""} (${t.source}, ${t.license}) ${t.page}`;
296
+
297
+ // src/material/material-def.ts
298
+ var ID = /^[a-z][a-z0-9_]{1,31}$/;
299
+ var unit = (v) => Number.isFinite(v) && v >= 0 && v <= 1;
300
+ function fail(id, cause, fix) {
301
+ throw new Ts23dError2("T3D_MATERIAL_INVALID", { cause: `material '${id}': ${cause}`, fix });
302
+ }
303
+ function colour(id, key, v) {
304
+ if (v === undefined)
305
+ return;
306
+ if (typeof v === "string") {
307
+ if (!/^#?[0-9a-fA-F]{6}$/.test(v))
308
+ fail(id, `${key} '${v}' is not a hex colour`, "use '#rrggbb'");
309
+ return hex2(v);
310
+ }
311
+ if (!(v.length === 3 && v.every(unit)))
312
+ fail(id, `${key} [${v}] is not linear RGB 0..1`, "use '#rrggbb' or three numbers in 0..1");
313
+ return v;
314
+ }
315
+ function toStripRecipe(d) {
316
+ const id = String(d.id);
317
+ if (!ID.test(id))
318
+ fail(id, "id is not snake_case (2–32 chars, starts with a letter)", "e.g. 'teak_deck'");
319
+ if (!unit(d.roughness))
320
+ fail(id, `roughness ${d.roughness}`, "roughness is 0..1");
321
+ if (d.metalness !== undefined && !unit(d.metalness))
322
+ fail(id, `metalness ${d.metalness}`, "metalness is 0..1 (0 dielectric, 1 metal)");
323
+ const scale = d.scaleMetres ?? 1;
324
+ if (!(Number.isFinite(scale) && scale > 0))
325
+ fail(id, `scaleMetres ${scale}`, "scaleMetres is the face height in metres, > 0");
326
+ if (d.paint !== undefined && typeof d.paint !== "function")
327
+ fail(id, "paint is not a function", "paint is (surface, kit) => void");
328
+ for (const [k, v] of Object.entries(d.params ?? {}))
329
+ if (!Number.isFinite(v))
330
+ fail(id, `params.${k} is ${v}`, "params are finite numbers");
331
+ if (d.texture)
332
+ validateTexture(id, d.texture);
333
+ const albedo = colour(id, "albedo", d.albedo);
334
+ const albedo2 = colour(id, "albedo2", d.albedo2);
335
+ return {
336
+ kind: d.kind ?? "noise",
337
+ scaleMetres: scale,
338
+ roughness: d.roughness,
339
+ ...d.metalness !== undefined ? { metalness: d.metalness } : {},
340
+ ...d.params ? { params: d.params } : {},
341
+ ...albedo ? { albedo } : {},
342
+ ...albedo2 ? { albedo2 } : {},
343
+ ...d.paint ? { paint: d.paint } : {},
344
+ ...d.texture ? { texture: d.texture } : {}
345
+ };
346
+ }
347
+
348
+ // src/material/paint/default-sheet.ts
349
+ var leaf = { kind: "flat", scaleMetres: 1, roughness: 0.6 };
350
+ var library = Object.fromEntries(Object.values(MATERIAL_LIBRARY2).map((d) => [d.id, toStripRecipe(d)]));
351
+ var HOUSE_STRIPS = {
352
+ plain: { kind: "noise", scaleMetres: 1, roughness: 0.6 },
353
+ brick: { kind: "bond", scaleMetres: 0.45, roughness: 0.85, params: { grime: 0.2 } },
354
+ stone: {
355
+ kind: "bond",
356
+ scaleMetres: 0.9,
357
+ roughness: 0.9,
358
+ params: {
359
+ course: 0.22,
360
+ length: 0.4,
361
+ mortar: 0.016,
362
+ depth: 0.012,
363
+ bevel: 0.025,
364
+ irregular: 0.5,
365
+ relief: 0.004,
366
+ variation: 0.3,
367
+ grime: 0.2
368
+ }
369
+ },
370
+ slate: { kind: "slate", scaleMetres: 0.66, roughness: 0.7 },
371
+ iron: { kind: "plates", scaleMetres: 0.5, roughness: 0.55, metalness: 0 },
372
+ wood: { kind: "planks", scaleMetres: 0.6, roughness: 0.75 },
373
+ glass: { kind: "glass", scaleMetres: 1, roughness: 0.05 },
374
+ concrete: { kind: "concrete", scaleMetres: 1, roughness: 0.9, params: { grime: 0.3 } },
375
+ ...library
376
+ };
377
+ var DEFAULT_SHEET2 = {
378
+ seed: 1,
379
+ size: 2048,
380
+ strips: HOUSE_STRIPS,
381
+ cells: {
382
+ window_sash: { kind: "noise", scaleMetres: 1, roughness: 0.5 },
383
+ door_panel: { kind: "noise", scaleMetres: 1, roughness: 0.6 },
384
+ clock: { kind: "noise", scaleMetres: 1, roughness: 0.3 },
385
+ smokebox: {
386
+ kind: "plates",
387
+ scaleMetres: 1,
388
+ roughness: 0.6,
389
+ metalness: 0.2,
390
+ params: { rust: 0.3 }
391
+ },
392
+ sign: { kind: "noise", scaleMetres: 1, roughness: 0.5 },
393
+ porthole: { kind: "plates", scaleMetres: 1, roughness: 0.5 },
394
+ foliage_broadleaf: leaf,
395
+ foliage_maple: leaf,
396
+ foliage_maple_autumn: leaf,
397
+ foliage_birch: leaf,
398
+ foliage_acacia: leaf,
399
+ foliage_palm: leaf,
400
+ foliage_spruce: leaf,
401
+ foliage_spruce_snow: leaf,
402
+ foliage_willow: leaf,
403
+ foliage_reeds: leaf,
404
+ foliage_needles: leaf
405
+ }
406
+ };
407
+
408
+ // src/material/slots.ts
409
+ var custom = [];
410
+ var customIndex = (id) => custom.indexOf(id);
411
+ var customIds = () => custom;
412
+ function pushCustom(id) {
413
+ custom.push(id);
414
+ return custom.length - 1;
415
+ }
416
+ function clearCustom() {
417
+ custom.length = 0;
418
+ }
419
+
420
+ // src/material/trim-sheet.ts
421
+ var STRIP_ROWS2 = 28;
422
+ var STRIP_H = 1 / 32;
423
+ var classic = [
424
+ "plain",
425
+ "brick",
426
+ "stone",
427
+ "slate",
428
+ "iron",
429
+ "wood",
430
+ "glass",
431
+ "concrete"
432
+ ];
433
+ var material = [
434
+ "oak",
435
+ "pine",
436
+ "wood_weathered",
437
+ "wood_painted",
438
+ "iron_rust",
439
+ "iron_cast",
440
+ "steel_painted",
441
+ "steel_galvanised",
442
+ "brass",
443
+ "copper",
444
+ "aluminium",
445
+ "chrome",
446
+ "rubber",
447
+ "canvas",
448
+ "leather",
449
+ "plastic"
450
+ ];
451
+ var strips = [...classic, ...material];
452
+ var CUSTOM_STRIP_SLOTS2 = STRIP_ROWS2 - strips.length;
453
+ var decals = [
454
+ "window_sash",
455
+ "door_panel",
456
+ "clock",
457
+ "smokebox",
458
+ "sign",
459
+ "porthole"
460
+ ];
461
+ var foliage = [
462
+ "foliage_broadleaf",
463
+ "foliage_maple",
464
+ "foliage_maple_autumn",
465
+ "foliage_birch",
466
+ "foliage_acacia",
467
+ "foliage_palm",
468
+ "foliage_spruce",
469
+ "foliage_spruce_snow",
470
+ "foliage_willow",
471
+ "foliage_reeds",
472
+ "foliage_needles"
473
+ ];
474
+ var cells = [...decals, ...foliage];
475
+ var CELL = 1 / 16;
476
+ var CELL_V0 = STRIP_ROWS2 * STRIP_H;
477
+ var FOLIAGE_SLOT = 8;
478
+ var slotOf = (id) => {
479
+ const d = decals.indexOf(id);
480
+ return d >= 0 ? d : FOLIAGE_SLOT + foliage.indexOf(id);
481
+ };
482
+ var TRIM_SHEET_SIZE2 = 2048;
483
+ function stripRow2(id) {
484
+ const b = strips.indexOf(id);
485
+ if (b >= 0)
486
+ return b;
487
+ const c = customIndex(id);
488
+ return c >= 0 ? strips.length + c : -1;
489
+ }
490
+ function stripRect2(id) {
491
+ const i = stripRow2(id);
492
+ if (i < 0)
493
+ throw new Ts23dError2("T3D_MATERIAL_UNKNOWN", {
494
+ cause: `strip '${id}' is neither built in nor defined`,
495
+ fix: `use a built-in strip (${strips.join(", ")}) or register it first with defineMaterial({ id: '${id}', ... })`
496
+ });
497
+ return { u0: 0, v0: i * STRIP_H, u1: 1, v1: (i + 1) * STRIP_H, metres: 2 };
498
+ }
499
+ function cellRect2(id) {
500
+ const i = slotOf(id);
501
+ const u0 = i % 16 * CELL;
502
+ const v0 = CELL_V0 + Math.floor(i / 16) * CELL;
503
+ return { u0, v0, u1: u0 + CELL, v1: v0 + CELL, metres: 1 };
504
+ }
505
+ var STRIPS2 = strips;
506
+ var CLASSIC_STRIPS2 = classic;
507
+ var MATERIAL_STRIPS2 = material;
508
+ var CELLS2 = cells;
509
+ var DECAL_CELLS2 = decals;
510
+ var FOLIAGE_CELLS2 = foliage;
511
+ var isFoliage2 = (id) => id.startsWith("foliage_");
512
+
513
+ // src/material/define.ts
514
+ var defined = new Map;
515
+ function same(a, b) {
516
+ return JSON.stringify({ ...a, paint: 0 }) === JSON.stringify({ ...b, paint: 0 }) && a.paint === b.paint;
517
+ }
518
+ function defineMaterial2(def) {
519
+ const recipe = toStripRecipe(def);
520
+ const id = def.id;
521
+ if (STRIPS2.includes(id))
522
+ throw new Ts23dError2("T3D_MATERIAL_CONFLICT", {
523
+ cause: `material '${id}' is a built-in strip`,
524
+ fix: `pick another id (e.g. '${id}_custom'), or restyle the built-in via paintTrimSheet({ ...DEFAULT_SHEET, strips: { ...DEFAULT_SHEET.strips, ${id}: … } })`
525
+ });
526
+ const prior = defined.get(id);
527
+ if (prior) {
528
+ if (same(prior, def))
529
+ return id;
530
+ throw new Ts23dError2("T3D_MATERIAL_CONFLICT", {
531
+ cause: `material '${id}' is already defined differently`,
532
+ fix: "define each material once in a shared module and import it, or give the variant its own id"
533
+ });
534
+ }
535
+ if (customIds().length >= CUSTOM_STRIP_SLOTS2)
536
+ throw new Ts23dError2("T3D_SHEET_FULL", {
537
+ cause: `the trim sheet has ${CUSTOM_STRIP_SLOTS2} custom strip rows, all taken (${customIds().join(", ")})`,
538
+ fix: "reuse a built-in strip with a vertex colour, or restyle an unused built-in strip in your SheetRecipe"
539
+ });
540
+ defined.set(id, def);
541
+ pushCustom(id);
542
+ HOUSE_STRIPS[id] = recipe;
543
+ return id;
544
+ }
545
+ function resetMaterials2() {
546
+ for (const id of defined.keys())
547
+ delete HOUSE_STRIPS[id];
548
+ defined.clear();
549
+ clearCustom();
550
+ }
551
+ function materialOf2(id) {
552
+ const recipe = HOUSE_STRIPS[id];
553
+ const lib = MATERIAL_LIBRARY2[id];
554
+ const def = lib ?? defined.get(id);
555
+ if (!recipe || customIndex(id) < 0 && !STRIPS2.includes(id))
556
+ throw new Ts23dError2("T3D_MATERIAL_UNKNOWN", {
557
+ cause: `strip '${id}' is neither built in nor defined`,
558
+ fix: `use one of ${STRIPS2.join(", ")}, or defineMaterial({ id: '${id}', ... }) first`
559
+ });
560
+ const classic = CLASSIC_STRIPS2.includes(id);
561
+ return {
562
+ id,
563
+ origin: classic ? "classic" : lib ? "library" : "defined",
564
+ recipe,
565
+ description: def?.description ?? `classic ${recipe.kind} strip (greyscale, vertex colour tints)`,
566
+ sources: def?.sources ?? []
567
+ };
568
+ }
569
+ function textureSources2() {
570
+ return [...defined.values()].flatMap((d) => d.texture ? [d.texture] : []);
571
+ }
572
+ function stripCredits2(vs) {
573
+ const out = [];
574
+ for (const [id, d] of defined) {
575
+ if (!d.texture)
576
+ continue;
577
+ const r = stripRect2(id);
578
+ for (let i = 0;i < vs.length; i++) {
579
+ const v = vs[i] ?? -1;
580
+ if (v >= r.v0 && v <= r.v1) {
581
+ out.push(creditOf2(d.texture));
582
+ break;
583
+ }
584
+ }
585
+ }
586
+ return out;
587
+ }
6
588
  // src/material/paint/noise.ts
7
589
  function hash2(ix, iy, seed) {
8
590
  let h = Math.imul(ix | 0, 668265261) ^ Math.imul(iy | 0, 374761393) ^ seed;
@@ -53,7 +635,10 @@ function createCanvas(w, h, metresU, metresV, wrapU) {
53
635
  rough: new Float32Array(n).fill(0.8),
54
636
  metal: new Float32Array(n),
55
637
  occl: new Float32Array(n).fill(1),
56
- rgba: null
638
+ mix: new Float32Array(n),
639
+ colours: null,
640
+ rgba: null,
641
+ normalRgba: null
57
642
  };
58
643
  }
59
644
  function periodFor(c, featureMetres) {
@@ -73,63 +658,6 @@ var smooth = (e0, e1, v) => {
73
658
  return t * t * (3 - 2 * t);
74
659
  };
75
660
 
76
- // src/material/trim-sheet.ts
77
- var STRIP_H = 1 / 16;
78
- var strips = [
79
- "plain",
80
- "brick",
81
- "stone",
82
- "slate",
83
- "iron",
84
- "wood",
85
- "glass",
86
- "concrete"
87
- ];
88
- var decals = [
89
- "window_sash",
90
- "door_panel",
91
- "clock",
92
- "smokebox",
93
- "sign",
94
- "porthole"
95
- ];
96
- var foliage = [
97
- "foliage_broadleaf",
98
- "foliage_maple",
99
- "foliage_maple_autumn",
100
- "foliage_birch",
101
- "foliage_acacia",
102
- "foliage_palm",
103
- "foliage_spruce",
104
- "foliage_spruce_snow",
105
- "foliage_willow",
106
- "foliage_reeds",
107
- "foliage_needles"
108
- ];
109
- var cells = [...decals, ...foliage];
110
- var FOLIAGE_SLOT = 8;
111
- var slotOf = (id) => {
112
- const d = decals.indexOf(id);
113
- return d >= 0 ? d : FOLIAGE_SLOT + foliage.indexOf(id);
114
- };
115
- var TRIM_SHEET_SIZE2 = 1024;
116
- function stripRect2(id) {
117
- const i = strips.indexOf(id);
118
- return { u0: 0, v0: i * STRIP_H, u1: 1, v1: (i + 1) * STRIP_H, metres: 2 };
119
- }
120
- function cellRect2(id) {
121
- const i = slotOf(id);
122
- const size = 1 / 8;
123
- const u0 = i % 8 * size;
124
- const v0 = 0.5 + Math.floor(i / 8) * size;
125
- return { u0, v0, u1: u0 + size, v1: v0 + size, metres: 1 };
126
- }
127
- var STRIPS2 = strips;
128
- var CELLS2 = cells;
129
- var DECAL_CELLS2 = decals;
130
- var FOLIAGE_CELLS2 = foliage;
131
- var isFoliage2 = (id) => id.startsWith("foliage_");
132
-
133
661
  // src/material/paint/shapes.ts
134
662
  function segment(px, py, ax, ay, bx, by) {
135
663
  const dx = bx - ax;
@@ -460,10 +988,23 @@ function packOrm(c, ao) {
460
988
  }
461
989
  return out;
462
990
  }
991
+ var enc = (t) => SRGB[t <= 0 ? 0 : t >= 1 ? 4095 : Math.round(t * 4095)] ?? 255;
463
992
  function packAlbedo(c, ao) {
464
993
  if (c.rgba)
465
994
  return c.rgba;
466
995
  const out = new Uint8Array(c.w * c.h * 4);
996
+ const pair = c.colours;
997
+ if (pair) {
998
+ const [a, b] = pair;
999
+ for (let i = 0, o = 0;i < ao.length; i++, o += 4) {
1000
+ const t = (c.tone[i] ?? 1) * (0.8 + 0.2 * (ao[i] ?? 1));
1001
+ const m = Math.min(1, Math.max(0, c.mix[i] ?? 0));
1002
+ for (let k = 0;k < 3; k++)
1003
+ out[o + k] = enc(((a[k] ?? 1) * (1 - m) + (b[k] ?? 1) * m) * t);
1004
+ out[o + 3] = 255;
1005
+ }
1006
+ return out;
1007
+ }
467
1008
  for (let i = 0, o = 0;i < ao.length; i++, o += 4) {
468
1009
  const t = (c.tone[i] ?? 1) * (0.8 + 0.2 * (ao[i] ?? 1));
469
1010
  const g = SRGB[t <= 0 ? 0 : t >= 1 ? 4095 : Math.round(t * 4095)] ?? 255;
@@ -885,7 +1426,7 @@ var CHANNELS = { 0: 1, 2: 3, 3: 1, 4: 2, 6: 4 };
885
1426
  function bad(cause) {
886
1427
  throw new Ts23dError2("T3D_RECIPE_INVALID", {
887
1428
  cause: `cell image: ${cause}`,
888
- fix: "pass PNG bytes, 8 bits per channel, non-interlaced (e.g. re-save as RGBA8 PNG)"
1429
+ fix: "pass PNG bytes, 8 or 16 bits per channel, non-interlaced (e.g. re-save as RGBA8 PNG)"
889
1430
  });
890
1431
  }
891
1432
  function paeth(a, b, c) {
@@ -902,6 +1443,7 @@ function decodePng(bytes) {
902
1443
  let width = 0;
903
1444
  let height = 0;
904
1445
  let type = -1;
1446
+ let depth = 8;
905
1447
  let palette = new Uint8Array;
906
1448
  let trns = new Uint8Array;
907
1449
  const idat = [];
@@ -913,8 +1455,9 @@ function decodePng(bytes) {
913
1455
  width = dv.getUint32(o + 8);
914
1456
  height = dv.getUint32(o + 12);
915
1457
  type = body[9] ?? -1;
916
- if (body[8] !== 8)
917
- bad(`bit depth ${body[8]} (only 8 is supported)`);
1458
+ depth = body[8] ?? 0;
1459
+ if (depth !== 8 && !(depth === 16 && type !== 3))
1460
+ bad(`bit depth ${depth} (8, or 16 for non-palette images, is supported)`);
918
1461
  if (body[12] !== 0)
919
1462
  bad("interlaced");
920
1463
  } else if (name === "PLTE")
@@ -931,7 +1474,9 @@ function decodePng(bytes) {
931
1474
  if (!ch || width === 0 || height === 0)
932
1475
  bad(`colour type ${type} or empty image`);
933
1476
  const raw = new Uint8Array(inflateSync(Buffer.concat(idat)));
934
- const stride = width * ch;
1477
+ const B = depth / 8;
1478
+ const bpp = ch * B;
1479
+ const stride = width * bpp;
935
1480
  if (raw.length < height * (stride + 1))
936
1481
  bad("truncated image data");
937
1482
  const px = new Uint8Array(height * stride);
@@ -940,9 +1485,9 @@ function decodePng(bytes) {
940
1485
  const src = y * (stride + 1) + 1;
941
1486
  const row = y * stride;
942
1487
  for (let x = 0;x < stride; x++) {
943
- const a = x >= ch ? px[row + x - ch] ?? 0 : 0;
1488
+ const a = x >= bpp ? px[row + x - bpp] ?? 0 : 0;
944
1489
  const b = y > 0 ? px[row - stride + x] ?? 0 : 0;
945
- const c = x >= ch && y > 0 ? px[row - stride + x - ch] ?? 0 : 0;
1490
+ const c = x >= bpp && y > 0 ? px[row - stride + x - bpp] ?? 0 : 0;
946
1491
  const v = raw[src + x] ?? 0;
947
1492
  const pred = f === 1 ? a : f === 2 ? b : f === 3 ? a + b >> 1 : f === 4 ? paeth(a, b, c) : 0;
948
1493
  px[row + x] = v + pred & 255;
@@ -950,8 +1495,9 @@ function decodePng(bytes) {
950
1495
  }
951
1496
  const data = new Uint8Array(width * height * 4);
952
1497
  for (let i = 0;i < width * height; i++) {
953
- const s = i * ch;
954
- const g = px[s] ?? 0;
1498
+ const s = i * bpp;
1499
+ const at = (k) => px[s + k * B] ?? 0;
1500
+ const g = at(0);
955
1501
  const o = i * 4;
956
1502
  if (type === 3) {
957
1503
  data[o] = palette[g * 3] ?? 0;
@@ -960,14 +1506,14 @@ function decodePng(bytes) {
960
1506
  data[o + 3] = trns[g] ?? 255;
961
1507
  } else if (ch >= 3) {
962
1508
  data[o] = g;
963
- data[o + 1] = px[s + 1] ?? 0;
964
- data[o + 2] = px[s + 2] ?? 0;
965
- data[o + 3] = ch === 4 ? px[s + 3] ?? 255 : 255;
1509
+ data[o + 1] = at(1);
1510
+ data[o + 2] = at(2);
1511
+ data[o + 3] = ch === 4 ? at(3) : 255;
966
1512
  } else {
967
1513
  data[o] = g;
968
1514
  data[o + 1] = g;
969
1515
  data[o + 2] = g;
970
- data[o + 3] = ch === 2 ? px[s + 1] ?? 255 : 255;
1516
+ data[o + 3] = ch === 2 ? at(1) : 255;
971
1517
  }
972
1518
  }
973
1519
  return { width, height, data };
@@ -1169,6 +1715,158 @@ function paintConcrete(c, r, seed) {
1169
1715
  }
1170
1716
  }
1171
1717
 
1718
+ // src/material/paint/fabric.ts
1719
+ function paintWeave(c, r, seed) {
1720
+ const p = r.params;
1721
+ const pu = c.metresU / c.w;
1722
+ const pv = c.metresV / c.h;
1723
+ const pitchU = Math.max(param(p, "pitch", 0.004), 2 * pu);
1724
+ const pitchV = Math.max(param(p, "pitch", 0.004), 2 * pv);
1725
+ const folds = param(p, "folds", 0.6);
1726
+ const stains = param(p, "stains", 0.3);
1727
+ for (let y = 0;y < c.h; y++) {
1728
+ const ym = (y + 0.5) * pv;
1729
+ const wy = Math.sin(ym / pitchV * Math.PI * 2);
1730
+ for (let x = 0;x < c.w; x++) {
1731
+ const i = y * c.w + x;
1732
+ const xm = (x + 0.5) * pu;
1733
+ const wx = Math.sin(xm / pitchU * Math.PI * 2);
1734
+ const over = Math.floor(xm / pitchU) + Math.floor(ym / pitchV);
1735
+ const thread = over % 2 === 0 ? Math.abs(wy) : Math.abs(wx);
1736
+ const slub = noiseAt(c, xm, ym, 0.05, 0.01, seed, 2);
1737
+ const fold = noiseAt(c, xm, ym, 0.5, 0.35, seed + 1, 3);
1738
+ const stain = stains * smooth(0.1, 0.6, noiseAt(c, xm, ym, 0.2, 0.15, seed + 2, 4));
1739
+ c.height[i] = 0.0002 * thread + 0.003 * folds * fold;
1740
+ c.tone[i] = (0.9 + 0.06 * thread + 0.05 * slub) * (1 - 0.25 * stain);
1741
+ c.rough[i] = Math.min(1, r.roughness + 0.04 * slub - 0.03 * thread);
1742
+ c.metal[i] = 0;
1743
+ c.mix[i] = stain;
1744
+ }
1745
+ }
1746
+ }
1747
+ function paintLeather(c, r, seed) {
1748
+ const p = r.params;
1749
+ const pu = c.metresU / c.w;
1750
+ const pv = c.metresV / c.h;
1751
+ const pebble = param(p, "pebble", 0.004);
1752
+ const creases = param(p, "creases", 0.5);
1753
+ const wear = param(p, "wear", 0.4);
1754
+ for (let y = 0;y < c.h; y++) {
1755
+ const ym = (y + 0.5) * pv;
1756
+ for (let x = 0;x < c.w; x++) {
1757
+ const i = y * c.w + x;
1758
+ const xm = (x + 0.5) * pu;
1759
+ const grain = noiseAt(c, xm, ym, pebble, pebble, seed, 3);
1760
+ const crease = smooth(0.4, 0.6, noiseAt(c, xm, ym, 0.12, 0.012, seed + 1, 3)) * creases;
1761
+ const rub = wear * smooth(0, 0.6, noiseAt(c, xm, ym, 0.3, 0.3, seed + 2, 4));
1762
+ c.height[i] = 0.00025 * grain - 0.0008 * crease;
1763
+ c.tone[i] = (0.95 + 0.05 * grain) * (1 - 0.45 * crease) * (1 + 0.12 * rub);
1764
+ c.rough[i] = Math.min(1, Math.max(0, r.roughness + 0.06 * grain - 0.2 * rub + 0.1 * crease));
1765
+ c.metal[i] = 0;
1766
+ c.mix[i] = crease;
1767
+ }
1768
+ }
1769
+ }
1770
+
1771
+ // src/material/paint/image-strip.ts
1772
+ var decoded = new Map;
1773
+ function image(f, where) {
1774
+ const hit = decoded.get(f.sha256);
1775
+ if (hit)
1776
+ return hit;
1777
+ const img = decodePng(textureBytes(f, where));
1778
+ decoded.set(f.sha256, img);
1779
+ return img;
1780
+ }
1781
+ function resample(c, t, img, fn) {
1782
+ const reps = Math.max(1, Math.round(c.metresU / (t.metres[0] ?? 1)));
1783
+ const su = c.metresU / reps;
1784
+ const sv = t.metres[1] ?? 1;
1785
+ const W = img.width;
1786
+ const H = img.height;
1787
+ const px = new Float32Array(4);
1788
+ for (let y = 0;y < c.h; y++) {
1789
+ const v0 = y * c.metresV / c.h / sv * H;
1790
+ const v1 = (y + 1) * c.metresV / c.h / sv * H;
1791
+ for (let x = 0;x < c.w; x++) {
1792
+ const u0 = x * c.metresU / c.w / su * W;
1793
+ const u1 = (x + 1) * c.metresU / c.w / su * W;
1794
+ px.fill(0);
1795
+ let n = 0;
1796
+ for (let sy = Math.floor(v0);sy < Math.max(Math.floor(v0) + 1, Math.ceil(v1)); sy++) {
1797
+ const row = H - 1 - (sy % H + H) % H;
1798
+ for (let sx = Math.floor(u0);sx < Math.max(Math.floor(u0) + 1, Math.ceil(u1)); sx++) {
1799
+ const o = (row * W + (sx % W + W) % W) * 4;
1800
+ for (let k = 0;k < 4; k++)
1801
+ px[k] = (px[k] ?? 0) + (img.data[o + k] ?? 0);
1802
+ n++;
1803
+ }
1804
+ }
1805
+ for (let k = 0;k < 4; k++)
1806
+ px[k] = (px[k] ?? 0) / n;
1807
+ fn(y * c.w + x, px);
1808
+ }
1809
+ }
1810
+ }
1811
+ function paintImageStrip(c, r, t, where) {
1812
+ const albedo = new Uint8Array(c.w * c.h * 4);
1813
+ resample(c, t, image(t.maps.albedo, where), (i, p) => {
1814
+ albedo.set([p[0] ?? 0, p[1] ?? 0, p[2] ?? 0, 255], i * 4);
1815
+ });
1816
+ c.rgba = albedo;
1817
+ c.height.fill(0);
1818
+ if (t.maps.normal) {
1819
+ const nrm = new Uint8Array(c.w * c.h * 4);
1820
+ resample(c, t, image(t.maps.normal, where), (i, p) => {
1821
+ const x = (p[0] ?? 128) / 127.5 - 1;
1822
+ const y = -((p[1] ?? 128) / 127.5 - 1);
1823
+ const z = (p[2] ?? 255) / 127.5 - 1;
1824
+ const l = Math.hypot(x, y, z) || 1;
1825
+ nrm.set([x / l * 127.5 + 127.5, y / l * 127.5 + 127.5, z / l * 127.5 + 127.5, 255], i * 4);
1826
+ });
1827
+ c.normalRgba = nrm;
1828
+ }
1829
+ if (t.maps.arm) {
1830
+ resample(c, t, image(t.maps.arm, where), (i, p) => {
1831
+ c.occl[i] = (p[0] ?? 255) / 255;
1832
+ c.rough[i] = (p[1] ?? 204) / 255;
1833
+ c.metal[i] = (p[2] ?? 0) / 255;
1834
+ });
1835
+ } else {
1836
+ c.rough.fill(r.roughness);
1837
+ c.metal.fill(r.metalness ?? 0);
1838
+ }
1839
+ }
1840
+
1841
+ // src/material/paint/kit.ts
1842
+ function paintKit(c, seed) {
1843
+ const pu = c.metresU / c.w;
1844
+ const pv = c.metresV / c.h;
1845
+ return {
1846
+ seed,
1847
+ noise: (xm, ym, sizeU, sizeV, salt = 0, octaves = 4) => noiseAt(c, xm, ym, sizeU, sizeV, seed + salt * 7919, octaves),
1848
+ hash: (a, b, salt = 0) => hash2(a, b, seed + salt),
1849
+ smooth,
1850
+ each(fn) {
1851
+ for (let y = 0;y < c.h; y++) {
1852
+ const ym = (y + 0.5) * pv;
1853
+ for (let x = 0;x < c.w; x++)
1854
+ fn(y * c.w + x, (x + 0.5) * pu, ym);
1855
+ }
1856
+ }
1857
+ };
1858
+ }
1859
+ function checkFinite(c, where) {
1860
+ const layers = { height: c.height, tone: c.tone, rough: c.rough, metal: c.metal, mix: c.mix };
1861
+ for (const [name, a] of Object.entries(layers))
1862
+ for (let i = 0;i < a.length; i++)
1863
+ if (!Number.isFinite(a[i] ?? 0))
1864
+ throw new Ts23dError2("T3D_MATERIAL_INVALID", {
1865
+ cause: `${where}: the painter wrote ${a[i]} into '${name}' at texel ${i}`,
1866
+ fix: "write finite numbers only (guard divisions; clamp with Math.min/Math.max)"
1867
+ });
1868
+ }
1869
+
1172
1870
  // src/material/paint/masks.ts
1173
1871
  function rustStreaks(c, amount, seed) {
1174
1872
  if (amount <= 0)
@@ -1188,6 +1886,7 @@ function rustStreaks(c, amount, seed) {
1188
1886
  c.tone[i] = (c.tone[i] ?? 1) * (1 - 0.4 * m);
1189
1887
  c.rough[i] = Math.min(1, (c.rough[i] ?? 0.8) + 0.2 * m);
1190
1888
  c.metal[i] = (c.metal[i] ?? 0) * (1 - m);
1889
+ c.mix[i] = Math.min(1, (c.mix[i] ?? 0) + m);
1191
1890
  }
1192
1891
  }
1193
1892
  }
@@ -1211,6 +1910,99 @@ function grime(c, amount, seed) {
1211
1910
  }
1212
1911
  }
1213
1912
 
1913
+ // src/material/paint/metal.ts
1914
+ function crystal(xm, ym, size, cellsU, seed) {
1915
+ const gx = Math.floor(xm / size);
1916
+ const gy = Math.floor(ym / size);
1917
+ let best = 1e9;
1918
+ let id = 0;
1919
+ for (let j = -1;j <= 1; j++)
1920
+ for (let i = -1;i <= 1; i++) {
1921
+ const cx = ((gx + i) % cellsU + cellsU) % cellsU;
1922
+ const px = (gx + i + hash2(cx, gy + j, seed)) * size;
1923
+ const py = (gy + j + hash2(cx, gy + j, seed + 1)) * size;
1924
+ const d = (xm - px) ** 2 + (ym - py) ** 2;
1925
+ if (d < best) {
1926
+ best = d;
1927
+ id = hash2(cx, gy + j, seed + 2) * 1e6;
1928
+ }
1929
+ }
1930
+ return id;
1931
+ }
1932
+ function paintMetal(c, r, seed) {
1933
+ const p = r.params;
1934
+ const brush = param(p, "brush", 0);
1935
+ const cast = param(p, "cast", 0);
1936
+ const spangle = param(p, "spangle", 0);
1937
+ const oxide = param(p, "oxide", 0);
1938
+ const paint = param(p, "paint", 0);
1939
+ const paintRough = param(p, "paintRough", 0.45);
1940
+ const chips = param(p, "chips", 0.25);
1941
+ const size = param(p, "crystal", 0.03);
1942
+ const metal = r.metalness ?? 1;
1943
+ const cellsU = Math.max(1, Math.round(c.metresU / size));
1944
+ const cs = c.metresU / cellsU;
1945
+ const pu = c.metresU / c.w;
1946
+ const pv = c.metresV / c.h;
1947
+ for (let y = 0;y < c.h; y++) {
1948
+ const ym = (y + 0.5) * pv;
1949
+ for (let x = 0;x < c.w; x++) {
1950
+ const i = y * c.w + x;
1951
+ const xm = (x + 0.5) * pu;
1952
+ const big = noiseAt(c, xm, ym, 0.25, 0.25, seed, 4);
1953
+ const fine = noiseAt(c, xm, ym, 0.01, 0.01, seed + 1, 2);
1954
+ let tone = 1 + 0.05 * big;
1955
+ let rough = r.roughness + 0.06 * big + 0.03 * fine;
1956
+ let h = 0.00015 * big;
1957
+ if (brush > 0) {
1958
+ const s = noiseAt(c, xm, ym, 0.6, 0.002, seed + 2, 3);
1959
+ tone *= 1 + 0.06 * brush * s;
1960
+ rough += 0.08 * brush * s;
1961
+ h += 0.00003 * brush * s;
1962
+ }
1963
+ if (cast > 0) {
1964
+ const pit = noiseAt(c, xm, ym, 0.012, 0.012, seed + 3, 3);
1965
+ const lump = noiseAt(c, xm, ym, 0.08, 0.08, seed + 4, 3);
1966
+ h += cast * (0.0006 * pit + 0.0008 * lump);
1967
+ rough += cast * (0.1 + 0.12 * pit);
1968
+ tone *= 1 + cast * (0.16 * pit + 0.08 * lump - 0.04);
1969
+ }
1970
+ if (spangle > 0) {
1971
+ const k = crystal(xm, ym, cs, cellsU, seed + 5);
1972
+ const v = hash2(k, 7, seed) * 2 - 1;
1973
+ tone *= 1 + 0.12 * spangle * v;
1974
+ rough += 0.12 * spangle * v;
1975
+ }
1976
+ let ox = 0;
1977
+ if (oxide > 0) {
1978
+ const blot = noiseAt(c, xm, ym, 0.18, 0.18, seed + 6, 5);
1979
+ const grain = noiseAt(c, xm, ym, 0.02, 0.02, seed + 7, 3);
1980
+ ox = smooth(0.45, 0.75, blot * 0.5 + 0.5 + oxide - 0.5 + 0.15 * grain);
1981
+ h += ox * 0.0004 * (1 + grain);
1982
+ }
1983
+ let m = metal * (1 - ox);
1984
+ let mix = ox;
1985
+ rough = rough * (1 - ox) + (0.88 + 0.06 * fine) * ox;
1986
+ if (paint > 0) {
1987
+ const n = noiseAt(c, xm, ym, 0.05, 0.05, seed + 8, 4);
1988
+ const chip = smooth(0.42, 0.5, n * 0.5 + 0.5 - (1 - chips) * 0.5 + 0.1);
1989
+ const film = paint * (1 - chip);
1990
+ m *= 1 - film;
1991
+ mix = mix * (1 - film) + chip * paint;
1992
+ rough = rough * (1 - film) + (paintRough + 0.05 * big) * film;
1993
+ tone = tone * (1 - film) + (0.96 + 0.03 * big) * film;
1994
+ h -= chip * paint * 0.00015;
1995
+ }
1996
+ c.height[i] = h;
1997
+ c.tone[i] = tone;
1998
+ c.rough[i] = Math.min(1, Math.max(0, rough));
1999
+ c.metal[i] = Math.min(1, Math.max(0, m));
2000
+ c.mix[i] = mix;
2001
+ }
2002
+ }
2003
+ rustStreaks(c, param(p, "streaks", 0), seed + 77);
2004
+ }
2005
+
1214
2006
  // src/material/paint/planks.ts
1215
2007
  function paintPlanks(c, r, seed) {
1216
2008
  const p = r.params;
@@ -1219,9 +2011,16 @@ function paintPlanks(c, r, seed) {
1219
2011
  const gap = param(p, "gap", 0.004);
1220
2012
  const depth = param(p, "depth", 0.004);
1221
2013
  const knots = param(p, "knots", 1.5);
2014
+ const rings = param(p, "rings", 0);
2015
+ const rays = param(p, "rays", 0);
2016
+ const checks = param(p, "checks", 0);
2017
+ const paint = param(p, "paint", 0);
2018
+ const paintRough = param(p, "paintRough", 0.5);
2019
+ const chips = param(p, "chips", 0.3);
1222
2020
  const boards = new Courses(c, { course: width, length, irregular: 0.6, stagger: 0, seed });
1223
2021
  const pu = c.metresU / c.w;
1224
2022
  const pv = c.metresV / c.h;
2023
+ const ringsN = Math.max(1, Math.min(9, Math.floor(width / pv / 4)));
1225
2024
  for (let y = 0;y < c.h; y++) {
1226
2025
  const ym = (y + 0.5) * pv;
1227
2026
  const k = boards.at(ym);
@@ -1249,9 +2048,39 @@ function paintPlanks(c, r, seed) {
1249
2048
  if (rr < 1)
1250
2049
  knot = Math.max(knot, 1 - rr);
1251
2050
  }
1252
- c.height[i] = -depth * (1 - edge) + 0.0004 * ring * edge + 0.0003 * fibre - 0.0008 * knot;
1253
- c.tone[i] = (0.66 + 0.22 * h) * (1 + 0.08 * ring + 0.06 * fibre) * (1 - 0.5 * knot) * (0.5 + 0.5 * edge);
1254
- c.rough[i] = r.roughness + 0.05 * fibre + 0.1 * (1 - edge);
2051
+ let height = -depth * (1 - edge) + 0.0004 * ring * edge + 0.0003 * fibre - 0.0008 * knot;
2052
+ let tone = (0.66 + 0.22 * h) * (1 + 0.08 * ring + 0.06 * fibre) * (1 - 0.5 * knot) * (0.5 + 0.5 * edge);
2053
+ let rough = r.roughness + 0.05 * fibre + 0.1 * (1 - edge);
2054
+ const ring2 = Math.sin((vy * ringsN + warp * 2.5 + h * 6) * Math.PI * 2);
2055
+ const late = smooth(0.1, 0.8, ring2) * (0.75 + 0.25 * fibre);
2056
+ let mix = rings * Math.max(late, knot);
2057
+ if (rays > 0) {
2058
+ const fleck = smooth(0.55, 0.8, noiseAt(c, xm, ym, 0.03, 0.004, seed + 23, 2));
2059
+ tone *= 1 + 0.25 * rays * fleck;
2060
+ mix *= 1 - rays * fleck;
2061
+ }
2062
+ if (checks > 0) {
2063
+ const split = smooth(0.45, 0.6, noiseAt(c, xm, ym, 0.3, 0.003, seed + 29 + id, 3));
2064
+ height += checks * (0.0006 * late - 0.002 * split);
2065
+ tone *= 1 - checks * (0.45 * split + 0.08 * (1 - late));
2066
+ rough += checks * 0.1 * (1 - late);
2067
+ mix = Math.max(mix, checks * split);
2068
+ }
2069
+ if (paint > 0) {
2070
+ const n = noiseAt(c, xm, ym, 0.04, 0.03, seed + 31, 4) * 0.5 + 0.5;
2071
+ const nearEdge = 1 - smooth(gap, gap * 6, d);
2072
+ const chip = smooth(0.5, 0.56, n + chips * 0.5 + nearEdge * 0.15 - 0.45);
2073
+ const film = paint * (1 - chip);
2074
+ height = height * (1 - 0.4 * film) - chip * paint * 0.0002;
2075
+ const under = (0.9 + 0.06 * h) * (1 + 0.05 * ring + 0.04 * fibre) * (0.55 + 0.45 * edge);
2076
+ tone = tone * (1 - film) + under * film;
2077
+ rough = rough * (1 - film) + (paintRough + 0.04 * fibre) * film;
2078
+ mix = mix * (1 - film) + paint * chip;
2079
+ }
2080
+ c.height[i] = height;
2081
+ c.tone[i] = tone;
2082
+ c.rough[i] = Math.min(1, rough);
2083
+ c.mix[i] = mix;
1255
2084
  }
1256
2085
  }
1257
2086
  }
@@ -1400,34 +2229,33 @@ function paintGlass(c, r, seed) {
1400
2229
  }
1401
2230
 
1402
2231
  // src/material/paint/strip.ts
1403
- function paintStrip(c, r, seed) {
1404
- switch (r.kind) {
1405
- case "bond":
1406
- paintBond(c, r, seed);
1407
- break;
1408
- case "planks":
1409
- paintPlanks(c, r, seed);
1410
- break;
1411
- case "plates":
1412
- paintPlates(c, r, seed);
1413
- break;
1414
- case "slate":
1415
- paintSlate(c, r, seed);
1416
- break;
1417
- case "concrete":
1418
- paintConcrete(c, r, seed);
1419
- break;
1420
- case "glass":
1421
- paintGlass(c, r, seed);
1422
- return;
1423
- case "noise":
1424
- paintNoise(c, r, seed);
1425
- break;
1426
- case "flat":
1427
- paintFlat(c, r);
1428
- return;
2232
+ var PAINTERS = {
2233
+ bond: paintBond,
2234
+ planks: paintPlanks,
2235
+ plates: paintPlates,
2236
+ slate: paintSlate,
2237
+ concrete: paintConcrete,
2238
+ metal: paintMetal,
2239
+ weave: paintWeave,
2240
+ leather: paintLeather,
2241
+ noise: paintNoise,
2242
+ glass: paintGlass,
2243
+ flat: (c, r) => paintFlat(c, r)
2244
+ };
2245
+ function paintStrip(c, r, seed, where = "strip") {
2246
+ if (r.texture)
2247
+ paintImageStrip(c, r, r.texture, where);
2248
+ else {
2249
+ if (r.albedo || r.albedo2)
2250
+ c.colours = [r.albedo ?? [1, 1, 1], r.albedo2 ?? r.albedo ?? [1, 1, 1]];
2251
+ PAINTERS[r.kind](c, r, seed);
2252
+ if (r.kind !== "glass" && r.kind !== "flat")
2253
+ grime(c, param(r.params, "grime", 0), seed + 101);
2254
+ }
2255
+ if (r.paint) {
2256
+ r.paint(c, paintKit(c, seed + 211));
2257
+ checkFinite(c, where);
1429
2258
  }
1430
- grime(c, param(r.params, "grime", 0), seed + 101);
1431
2259
  }
1432
2260
 
1433
2261
  // src/material/paint/cell.ts
@@ -1519,7 +2347,7 @@ function composeRegion(sheet, c, x0, y0, pad) {
1519
2347
  const ao = cavityOcclusion(c);
1520
2348
  const maps = [
1521
2349
  [sheet.albedo, packAlbedo(c, ao)],
1522
- [sheet.normal, sobelNormals(c)],
2350
+ [sheet.normal, c.normalRgba ?? sobelNormals(c)],
1523
2351
  [sheet.orm, packOrm(c, ao)]
1524
2352
  ];
1525
2353
  const padX = c.wrapU ? 0 : pad;
@@ -1539,63 +2367,6 @@ function composeRegion(sheet, c, x0, y0, pad) {
1539
2367
  }
1540
2368
  }
1541
2369
 
1542
- // src/material/paint/default-sheet.ts
1543
- var leaf = { kind: "flat", scaleMetres: 1, roughness: 0.6 };
1544
- var DEFAULT_SHEET2 = {
1545
- seed: 1,
1546
- size: 1024,
1547
- strips: {
1548
- plain: { kind: "noise", scaleMetres: 1, roughness: 0.6 },
1549
- brick: { kind: "bond", scaleMetres: 0.45, roughness: 0.85, params: { grime: 0.2 } },
1550
- stone: {
1551
- kind: "bond",
1552
- scaleMetres: 0.9,
1553
- roughness: 0.9,
1554
- params: {
1555
- course: 0.22,
1556
- length: 0.4,
1557
- mortar: 0.016,
1558
- depth: 0.012,
1559
- bevel: 0.025,
1560
- irregular: 0.5,
1561
- relief: 0.004,
1562
- variation: 0.3,
1563
- grime: 0.2
1564
- }
1565
- },
1566
- slate: { kind: "slate", scaleMetres: 0.66, roughness: 0.7 },
1567
- iron: { kind: "plates", scaleMetres: 0.5, roughness: 0.55, metalness: 0 },
1568
- wood: { kind: "planks", scaleMetres: 0.6, roughness: 0.75 },
1569
- glass: { kind: "glass", scaleMetres: 1, roughness: 0.05 },
1570
- concrete: { kind: "concrete", scaleMetres: 1, roughness: 0.9, params: { grime: 0.3 } }
1571
- },
1572
- cells: {
1573
- window_sash: { kind: "noise", scaleMetres: 1, roughness: 0.5 },
1574
- door_panel: { kind: "noise", scaleMetres: 1, roughness: 0.6 },
1575
- clock: { kind: "noise", scaleMetres: 1, roughness: 0.3 },
1576
- smokebox: {
1577
- kind: "plates",
1578
- scaleMetres: 1,
1579
- roughness: 0.6,
1580
- metalness: 0.2,
1581
- params: { rust: 0.3 }
1582
- },
1583
- sign: { kind: "noise", scaleMetres: 1, roughness: 0.5 },
1584
- porthole: { kind: "plates", scaleMetres: 1, roughness: 0.5 },
1585
- foliage_broadleaf: leaf,
1586
- foliage_maple: leaf,
1587
- foliage_maple_autumn: leaf,
1588
- foliage_birch: leaf,
1589
- foliage_acacia: leaf,
1590
- foliage_palm: leaf,
1591
- foliage_spruce: leaf,
1592
- foliage_spruce_snow: leaf,
1593
- foliage_willow: leaf,
1594
- foliage_reeds: leaf,
1595
- foliage_needles: leaf
1596
- }
1597
- };
1598
-
1599
2370
  // src/material/paint/validate.ts
1600
2371
  var KINDS = [
1601
2372
  "bond",
@@ -1605,45 +2376,55 @@ var KINDS = [
1605
2376
  "flat",
1606
2377
  "slate",
1607
2378
  "concrete",
1608
- "glass"
2379
+ "glass",
2380
+ "metal",
2381
+ "weave",
2382
+ "leather"
1609
2383
  ];
1610
2384
  var SIZES = [512, 1024, 2048];
1611
- function fail(cause, fix) {
2385
+ function fail2(cause, fix) {
1612
2386
  throw new Ts23dError2("T3D_RECIPE_INVALID", { cause: `sheet recipe: ${cause}`, fix });
1613
2387
  }
1614
- var unit = (v) => Number.isFinite(v) && v >= 0 && v <= 1;
2388
+ var unit2 = (v) => Number.isFinite(v) && v >= 0 && v <= 1;
1615
2389
  function validateStrip(where, r) {
1616
2390
  if (!KINDS.includes(r.kind))
1617
- fail(`${where}.kind '${r.kind}' is unknown`, `use one of ${KINDS.join(" | ")}`);
2391
+ fail2(`${where}.kind '${r.kind}' is unknown`, `use one of ${KINDS.join(" | ")}`);
1618
2392
  if (!(Number.isFinite(r.scaleMetres) && r.scaleMetres > 0))
1619
- fail(`${where}.scaleMetres is ${r.scaleMetres}`, "set the metres the strip V span represents, > 0 (e.g. 0.6 for brick)");
1620
- if (!unit(r.roughness))
1621
- fail(`${where}.roughness is ${r.roughness}`, "roughness is 0..1");
1622
- if (r.metalness !== undefined && !unit(r.metalness))
1623
- fail(`${where}.metalness is ${r.metalness}`, "metalness is 0..1");
2393
+ fail2(`${where}.scaleMetres is ${r.scaleMetres}`, "set the metres the strip V span represents, > 0 (e.g. 0.6 for brick)");
2394
+ if (!unit2(r.roughness))
2395
+ fail2(`${where}.roughness is ${r.roughness}`, "roughness is 0..1");
2396
+ if (r.metalness !== undefined && !unit2(r.metalness))
2397
+ fail2(`${where}.metalness is ${r.metalness}`, "metalness is 0..1");
2398
+ for (const key of ["albedo", "albedo2"]) {
2399
+ const a = r[key];
2400
+ if (a !== undefined && !(a.length === 3 && a.every(unit2)))
2401
+ fail2(`${where}.${key} is [${a}]`, `${key} is linear RGB, three numbers 0..1 (use hex('#rrggbb'))`);
2402
+ }
2403
+ if (r.paint !== undefined && typeof r.paint !== "function")
2404
+ fail2(`${where}.paint is not a function`, "paint is (surface, kit) => void");
1624
2405
  for (const [k, v] of Object.entries(r.params ?? {}))
1625
2406
  if (!Number.isFinite(v))
1626
- fail(`${where}.params.${k} is ${v}`, "params are finite numbers");
2407
+ fail2(`${where}.params.${k} is ${v}`, "params are finite numbers");
1627
2408
  }
1628
2409
  function validateSheet(recipe) {
1629
2410
  if (!Number.isInteger(recipe.seed))
1630
- fail(`seed ${recipe.seed}`, "seed is an integer");
2411
+ fail2(`seed ${recipe.seed}`, "seed is an integer");
1631
2412
  if (recipe.size !== undefined && !SIZES.includes(recipe.size))
1632
- fail(`size ${recipe.size}`, "size is 512, 1024 or 2048");
2413
+ fail2(`size ${recipe.size}`, "size is 512, 1024 or 2048");
1633
2414
  for (const [id, r] of Object.entries(recipe.strips)) {
1634
- if (!STRIPS2.includes(id))
1635
- fail(`strip '${id}' is not in the layout`, `strips are ${STRIPS2.join(", ")}`);
2415
+ if (stripRow2(id) < 0)
2416
+ fail2(`strip '${id}' is not in the layout`, `strips are ${STRIPS2.join(", ")} or a defineMaterial() id`);
1636
2417
  if (r)
1637
2418
  validateStrip(`strips.${id}`, r);
1638
2419
  }
1639
2420
  for (const [id, e] of Object.entries(recipe.cells ?? {})) {
1640
2421
  if (!CELLS2.includes(id))
1641
- fail(`cell '${id}' is not in the layout`, `cells are ${CELLS2.join(", ")}`);
2422
+ fail2(`cell '${id}' is not in the layout`, `cells are ${CELLS2.join(", ")}`);
1642
2423
  if (!e)
1643
2424
  continue;
1644
2425
  if ("image" in e) {
1645
2426
  if (!(e.image instanceof Uint8Array))
1646
- fail(`cells.${id}.image is not bytes`, "pass PNG file bytes as a Uint8Array");
2427
+ fail2(`cells.${id}.image is not bytes`, "pass PNG file bytes as a Uint8Array");
1647
2428
  } else
1648
2429
  validateStrip(`cells.${id}`, e);
1649
2430
  }
@@ -1653,18 +2434,19 @@ function validateSheet(recipe) {
1653
2434
  var FALLBACK = { kind: "noise", scaleMetres: 1, roughness: 0.6 };
1654
2435
  function paintTrimSheet2(recipe = DEFAULT_SHEET2) {
1655
2436
  validateSheet(recipe);
1656
- const size = recipe.size ?? 1024;
2437
+ const size = recipe.size ?? 2048;
1657
2438
  const sheet = blankSheet(size);
1658
2439
  const rng = createRng2(recipe.seed);
1659
2440
  const seedOf = (name) => rng.fork(name).int(0, 2147483647);
1660
2441
  const pad = Math.max(2, size / 256);
1661
- for (const id of STRIPS2) {
2442
+ const all = [...STRIPS2, ...customIds()];
2443
+ for (const id of all) {
1662
2444
  const r = recipe.strips[id] ?? DEFAULT_SHEET2.strips[id] ?? FALLBACK;
1663
2445
  validateStrip(`strips.${id}`, r);
1664
2446
  const rect = stripRect2(id);
1665
2447
  const h = Math.round((rect.v1 - rect.v0) * size);
1666
2448
  const c = createCanvas(size, h, rect.metres, r.scaleMetres, true);
1667
- paintStrip(c, r, seedOf(id));
2449
+ paintStrip(c, r, seedOf(id), `strips.${id}`);
1668
2450
  composeRegion(sheet, c, 0, Math.round(rect.v0 * size), pad);
1669
2451
  }
1670
2452
  for (const id of CELLS2) {
@@ -1677,39 +2459,6 @@ function paintTrimSheet2(recipe = DEFAULT_SHEET2) {
1677
2459
  }
1678
2460
  return sheet;
1679
2461
  }
1680
- // src/material/palette.ts
1681
- function hex2(value) {
1682
- const n = Number.parseInt(value.replace("#", ""), 16);
1683
- const lin = (c) => {
1684
- const s = c / 255;
1685
- return s <= 0.04045 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;
1686
- };
1687
- return [lin(n >> 16 & 255), lin(n >> 8 & 255), lin(n & 255)];
1688
- }
1689
- function definePalette2(entries) {
1690
- return Object.fromEntries(Object.entries(entries).map(([k, v]) => [k, hex2(v)]));
1691
- }
1692
- var palettes2 = {
1693
- victorianRail: definePalette2({
1694
- paint: "#2f4a3a",
1695
- trim: "#b08d57",
1696
- iron: "#2b2b2e",
1697
- brass: "#c9a24a",
1698
- red: "#7a2a22"
1699
- }),
1700
- artDeco: definePalette2({
1701
- limestone: "#d8cfbd",
1702
- steel: "#9aa3ad",
1703
- glass: "#4a6a80",
1704
- bronze: "#8a6a3c"
1705
- }),
1706
- modernGlass: definePalette2({
1707
- glass: "#6f8fa8",
1708
- aluminium: "#c4c9cf",
1709
- concrete: "#a7a39b"
1710
- }),
1711
- harbour: definePalette2({ hull: "#1f2f45", deck: "#8a6f4e", white: "#e8e6df", red: "#a1302a" })
1712
- };
1713
2462
  // src/material/shader-godot.ts
1714
2463
  var f = (n) => Number.isInteger(n) ? `${n}.0` : `${+n.toFixed(6)}`;
1715
2464
  var rect2 = (r) => `in_rect(UV, vec4(${f(r.u0)}, ${f(r.v0)}, ${f(r.u1)}, ${f(r.v1)}))`;
@@ -1878,18 +2627,18 @@ export function upgradeTrimMaterials(root: Object3D): void {
1878
2627
  }
1879
2628
 
1880
2629
  // src/material/shader.ts
1881
- function fail2(cause, fix) {
2630
+ function fail3(cause, fix) {
1882
2631
  throw new Ts23dError2("T3D_RECIPE_INVALID", { cause: `shader hints: ${cause}`, fix });
1883
2632
  }
1884
2633
  function srgbHex(value) {
1885
2634
  if (!/^#[0-9a-fA-F]{6}$/.test(value))
1886
- fail2(`colour '${value}'`, "use '#rrggbb' sRGB hex");
2635
+ fail3(`colour '${value}'`, "use '#rrggbb' sRGB hex");
1887
2636
  const n = Number.parseInt(value.slice(1), 16);
1888
2637
  return [(n >> 16 & 255) / 255, (n >> 8 & 255) / 255, (n & 255) / 255];
1889
2638
  }
1890
2639
  function resolveHints(h) {
1891
- const unit = (v, what) => Number.isFinite(v) && v >= 0 && v <= 1 ? v : fail2(`${what} is ${v}`, `${what} is 0..1`);
1892
- const pos = (v, what) => Number.isFinite(v) && v > 0 ? v : fail2(`${what} is ${v}`, `${what} is a positive number`);
2640
+ const unit = (v, what) => Number.isFinite(v) && v >= 0 && v <= 1 ? v : fail3(`${what} is ${v}`, `${what} is 0..1`);
2641
+ const pos = (v, what) => Number.isFinite(v) && v > 0 ? v : fail3(`${what} is ${v}`, `${what} is a positive number`);
1893
2642
  const strength = h.wind ? pos(h.wind.strength, "wind.strength") : 0;
1894
2643
  return {
1895
2644
  windStrength: strength,
@@ -1913,7 +2662,7 @@ function threeMaterialSource2(hints = {}) {
1913
2662
  function godotShaderSource2(hints = {}) {
1914
2663
  return godotSource(resolveHints(hints));
1915
2664
  }
1916
- export { TRIM_SHEET_SIZE2, stripRect2, cellRect2, STRIPS2, CELLS2, DECAL_CELLS2, FOLIAGE_CELLS2, isFoliage2, DEFAULT_SHEET2, paintTrimSheet2, hex2, definePalette2, palettes2, threeMaterialSource2, godotShaderSource2 };
2665
+ export { CC0_WOOD_PLANKS_GREY2, CC0_MATERIALS2, MATERIAL_LIBRARY2, hex2, NATURAL2, definePalette2, palettes2, provideTextures2, creditOf2, DEFAULT_SHEET2, STRIP_ROWS2, CUSTOM_STRIP_SLOTS2, TRIM_SHEET_SIZE2, stripRow2, stripRect2, cellRect2, STRIPS2, CLASSIC_STRIPS2, MATERIAL_STRIPS2, CELLS2, DECAL_CELLS2, FOLIAGE_CELLS2, isFoliage2, defineMaterial2, resetMaterials2, materialOf2, textureSources2, stripCredits2, paintTrimSheet2, threeMaterialSource2, godotShaderSource2 };
1917
2666
 
1918
- //# debugId=F437FE42A323CC4464756E2164756E21
1919
- //# sourceMappingURL=chunk-kvcep7ft.js.map
2667
+ //# debugId=06EA6C9948471A9664756E2164756E21
2668
+ //# sourceMappingURL=chunk-y63c6zcv.js.map