@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,25 +1,27 @@
1
- // The orc's armoured bust gear as sculpt groups: layered metal pauldron lames (domes trimmed by a
2
- // tilted plane, each with a rolled rim), bone spikes, a leather baldric that follows the chest
3
- // (a shell of the chest ∩ a diagonal slab) with an iron buckle, and a fur mantle of clumped,
4
- // tapering strands. Head units, mirrored across Y unless an edit opts out.
1
+ // The orc's armoured bust gear as sculpt groups: plate pauldrons built like real armour — a domed
2
+ // shoulder cap and two shingled lames, each a hollow shell (so its edge shows a thickness and a
3
+ // shadowed underside) with a rolled rim, a raised crest and bone spikes along it, rivets — plus a
4
+ // leather baldric that hugs the chest (a shell of the torso ∩ a diagonal slab) with an iron buckle,
5
+ // and a fur mantle of clumped, tapering strands over the trapezius. Head units, mirrored across Y.
5
6
 
6
7
  import type { Vec3 } from '../core/index.ts';
7
8
  import type { SculptEdit } from '../primitives/index.ts';
8
9
  import { alongZ, unit } from './head-geom.ts';
9
10
  import type { HeadParams } from './head-params.ts';
11
+ import { type BustFrame, bustCut, torsoForms } from './head-torso.ts';
10
12
 
11
13
  const RAD = Math.PI / 180;
12
14
  const shade = (c: Vec3, k: number): Vec3 => [c[0] * k, c[1] * k, c[2] * k];
13
15
 
14
16
  /** Half-space kept above the plane through `p` whose normal is +Z rolled `deg` about X. */
15
- const above = (p: Vec3, deg: number, op: 'intersect' | 'subtract' = 'intersect'): SculptEdit => {
17
+ const above = (p: Vec3, deg: number): SculptEdit => {
16
18
  const n: Vec3 = [0, -Math.sin(deg * RAD), Math.cos(deg * RAD)];
17
19
  return {
18
20
  shape: 'roundBox',
19
21
  at: [p[0] + n[0], p[1] + n[1], p[2] + n[2]],
20
22
  size: [4, 4, 2],
21
23
  rotate: [deg, 0, 0],
22
- op,
24
+ op: 'intersect',
23
25
  };
24
26
  };
25
27
 
@@ -32,84 +34,119 @@ const slab = (p: Vec3, deg: number, t: number): SculptEdit => ({
32
34
  op: 'intersect',
33
35
  });
34
36
 
35
- /** Chest ellipsoid of the bust (shared with head-bust.ts so the strap hugs it). */
36
- export const chestOf = (cx: number, s: number, b: number): { at: Vec3; r: Vec3 } => ({
37
- at: [cx - 0.04, 0, b + 0.2],
38
- r: [0.36, s * 0.75, 0.42],
37
+ const ell = (at: Vec3, r: Vec3, g: number, roll: number): SculptEdit => ({
38
+ shape: 'ellipsoid',
39
+ at,
40
+ size: [r[0] + g, r[1] + g, r[2] + g],
41
+ rotate: [roll, 0, 0],
39
42
  });
40
43
 
41
- export function armour(p: HeadParams, cx: number, s: number, b: number, nz: number): SculptEdit[] {
44
+ /** One plate: a shell of thickness `t` trimmed above a tilted plane, with a rolled rim. */
45
+ function plate(
46
+ at: Vec3,
47
+ r: Vec3,
48
+ roll: number,
49
+ cut: Vec3,
50
+ tilt: number,
51
+ c: Vec3,
52
+ t: number,
53
+ ): SculptEdit[] {
54
+ return [
55
+ {
56
+ group: [
57
+ ell(at, r, 0, roll),
58
+ // Hollow only while the shell is thinner than the plate itself (fine LODs).
59
+ ...(t < Math.min(...r) * 0.6 ? [{ ...ell(at, r, -t, roll), op: 'subtract' as const }] : []),
60
+ above(cut, tilt),
61
+ ],
62
+ color: c,
63
+ blend: 0.006,
64
+ },
65
+ {
66
+ group: [
67
+ ell(at, r, 0.022, roll),
68
+ ...(t < Math.min(...r) * 0.6
69
+ ? [{ ...ell(at, r, -t * 0.8, roll), op: 'subtract' as const }]
70
+ : []),
71
+ slab(cut, tilt, 0.05),
72
+ ],
73
+ // Worn edge: the rim catches the light (a painter's edge highlight).
74
+ color: shade(c, 1.3),
75
+ blend: 0.006,
76
+ },
77
+ ];
78
+ }
79
+
80
+ /** `cell`: the bust's sculpt cell; plates thicken to ≈ 2.5 cells so far LODs stay closed. */
81
+ export function armour(p: HeadParams, f: BustFrame, cell = 0): SculptEdit[] {
82
+ const t = Math.max(0.085, cell * 2.5);
42
83
  const c = p.colors;
84
+ const { cx, s, b, nz } = f;
43
85
  const metal = c.metal;
44
86
  const out: SculptEdit[] = [];
45
- // Pauldrons: three lames, top one biggest, stepping down and out; rims a darker iron.
46
- for (let k = 0; k < 3; k++) {
47
- const at: Vec3 = [cx - 0.03, s * (0.78 + k * 0.1), b + 0.53 - k * 0.12];
48
- const r: Vec3 = [0.31 - k * 0.02, 0.27 - k * 0.03, 0.19 - k * 0.02];
49
- const tilt = -(28 + k * 14);
50
- const cut: Vec3 = [at[0], at[1], at[2] - r[2] * 0.35];
51
- out.push(
52
- {
53
- group: [{ shape: 'ellipsoid', at, size: r }, above(cut, tilt)],
54
- color: shade(metal, 1 - k * 0.12),
55
- blend: 0.008,
56
- },
57
- {
58
- group: [
59
- { shape: 'ellipsoid', at, size: [r[0] + 0.018, r[1] + 0.018, r[2] + 0.018] },
60
- slab(cut, tilt, 0.035),
61
- ],
62
- color: shade(metal, 0.55),
63
- blend: 0.004,
64
- },
65
- );
66
- // Rivets along the top of each lame's rim.
87
+ // Pauldron: a big shoulder cap, then two lames shingled out and down over the arm.
88
+ const cap: Vec3 = [cx - 0.04, s * 0.8, b + 0.66];
89
+ for (let k = 1; k >= 0; k--) {
90
+ const at: Vec3 = [cap[0], cap[1] + k * 0.07 * s, cap[2] - k * 0.17];
91
+ const r: Vec3 = k === 0 ? [0.27, 0.29, 0.25] : [0.23, 0.25, 0.19];
92
+ const tilt = -(38 + k * 14);
93
+ const cut: Vec3 = [at[0], at[1], at[2] - r[2] * 0.45];
94
+ out.push(...plate(at, r, -25 - k * 10, cut, tilt, shade(metal, 1 - k * 0.15), t));
95
+ // Rivets along each plate's rim.
67
96
  for (let j = 0; j < 4; j++) {
68
- const t = (j - 1.5) * 0.11;
97
+ const x = at[0] + (j - 1.5) * 0.12;
69
98
  out.push({
70
99
  shape: 'sphere',
71
- at: [at[0] + t, at[1] + Math.sin(-tilt * RAD) * r[1] * 0.55, cut[2] + 0.03],
72
- size: 0.016,
73
- color: shade(metal, 1.4),
100
+ at: [x, at[1] + r[1] * 0.62, cut[2] + 0.02 - r[1] * 0.62 * Math.tan(-tilt * RAD) * 0.4],
101
+ size: 0.017,
102
+ color: shade(metal, 1.35),
74
103
  blend: 0.004,
75
104
  });
76
105
  }
77
106
  }
78
- // Bone spikes along the top lame's ridge.
107
+ // Crest along the cap's crown, bone spikes rising out of it.
108
+ const crest: Vec3 = [cap[0], cap[1] - 0.03, cap[2] + 0.25];
109
+ out.push({
110
+ shape: 'ellipsoid',
111
+ at: crest,
112
+ size: [0.25, 0.028, 0.07],
113
+ rotate: [-22, 0, 0],
114
+ color: shade(metal, 0.75),
115
+ blend: 0.02,
116
+ });
79
117
  for (let k = 0; k < 3; k++)
80
118
  out.push({
81
119
  shape: 'cone',
82
- at: [cx - 0.17 + k * 0.14, s * 0.84, b + 0.68 - Math.abs(k - 1) * 0.03],
83
- size: [0.065, 0.006, 0.26 - Math.abs(k - 1) * 0.08],
84
- rotate: alongZ(unit([(k - 1) * 0.35, 0.45, 1])),
120
+ at: [crest[0] - 0.14 + k * 0.14, crest[1] + 0.01, crest[2] + 0.02 - Math.abs(k - 1) * 0.03],
121
+ size: [0.075, 0.012, 0.36 - Math.abs(k - 1) * 0.12],
122
+ rotate: alongZ(unit([(k - 1) * 0.3, 0.55, 1])),
85
123
  color: c.teeth,
86
- blend: 0.01,
124
+ blend: 0.015,
87
125
  });
88
- // Baldric: a shell of the chest cut to a diagonal band, over the left shoulder; iron buckle.
89
- const chest = chestOf(cx, s, b);
90
- const grow = (g: number): Vec3 => [chest.r[0] + g, chest.r[1] + g, chest.r[2] + g];
91
- const band: Vec3 = [cx, 0, b + 0.28];
126
+ // Baldric: a shell of the chest cut to a diagonal band over the left shoulder; iron buckle.
127
+ const band: Vec3 = [cx, 0, b + 0.36];
92
128
  out.push(
93
129
  {
94
- group: [{ shape: 'ellipsoid', at: chest.at, size: grow(0.03) }, slab(band, 52, 0.17)],
130
+ group: [{ group: torsoForms(f, 0.03) }, slab(band, 50, 0.15), bustCut(f)],
95
131
  color: c.leather,
96
132
  blend: 0.006,
97
133
  mirror: false,
98
134
  },
99
135
  {
100
136
  group: [
101
- { shape: 'ellipsoid', at: chest.at, size: grow(0.055) },
137
+ { group: torsoForms(f, 0.055) },
138
+ bustCut(f),
102
139
  {
103
140
  shape: 'roundBox',
104
- at: [cx + 0.3, 0.02, b + 0.3],
105
- size: [0.3, 0.15, 0.15],
106
- rotate: [52, 0, 0],
141
+ at: [cx + 0.3, 0.02, b + 0.38],
142
+ size: [0.3, 0.14, 0.14],
143
+ rotate: [50, 0, 0],
107
144
  op: 'intersect',
108
145
  },
109
146
  {
110
147
  shape: 'ellipsoid',
111
- at: [cx + 0.42, 0.02, b + 0.3],
112
- size: [0.1, 0.045, 0.045],
148
+ at: [cx + 0.42, 0.02, b + 0.38],
149
+ size: [0.1, 0.04, 0.04],
113
150
  op: 'subtract',
114
151
  },
115
152
  ],
@@ -118,33 +155,26 @@ export function armour(p: HeadParams, cx: number, s: number, b: number, nz: numb
118
155
  mirror: false,
119
156
  },
120
157
  );
121
- // Fur mantle: a thick base roll, then clumped strands fanning out and down from it.
158
+ // Fur mantle over the trapezius: clumped strands fanning back and out from behind the neck.
122
159
  const fur = c.fur ?? c.leather;
123
- const R = p.neck.radius + 0.1;
124
- out.push({
125
- shape: 'torus',
126
- at: [cx - 0.08, 0, nz + 0.02],
127
- size: [R, 0.07],
128
- color: shade(fur, 0.75),
129
- blend: 0.04,
130
- });
131
- for (let row = 0; row < 2; row++)
132
- for (let i = 0; i < 10; i++) {
133
- const a = (-85 + i * 20 + row * 10) * RAD;
134
- const rr = R + row * 0.05;
160
+ const R = f.n + 0.08;
161
+ for (let row = 0; row < 3; row++)
162
+ for (let i = 0; i < 8; i++) {
163
+ const a = (-80 + i * 22 + row * 11) * RAD;
164
+ const rr = R + row * 0.07;
135
165
  const root: Vec3 = [
136
- cx - 0.08 + Math.cos(a) * rr,
137
- Math.abs(Math.sin(a)) * rr + 0.015,
138
- nz + 0.07 - row * 0.06,
166
+ cx - 0.1 - Math.abs(Math.cos(a)) * rr * 0.9,
167
+ Math.abs(Math.sin(a)) * rr + 0.02,
168
+ nz + 0.16 - row * 0.05,
139
169
  ];
140
- const dir = unit([Math.cos(a), Math.abs(Math.sin(a)), -0.5 - ((i + row) % 2) * 0.35]);
170
+ const dir = unit([-0.7, Math.abs(Math.sin(a)) * 0.8, -0.55 - ((i + row) % 2) * 0.3]);
141
171
  out.push({
142
172
  shape: 'cone',
143
173
  at: root,
144
- size: [0.07 - row * 0.01, 0.012, 0.13 + ((i + row) % 3) * 0.03],
174
+ size: [0.075 - row * 0.008, 0.012, 0.16 + ((i + row) % 3) * 0.04],
145
175
  rotate: alongZ(dir),
146
- color: (i + row) % 2 ? fur : shade(fur, 0.7),
147
- blend: 0.03,
176
+ color: (i + row) % 2 ? fur : shade(fur, 0.72),
177
+ blend: 0.035,
148
178
  });
149
179
  }
150
180
  return out;
@@ -1,38 +1,22 @@
1
- // The bust under the head: chest, trapezius and deltoid caps, cut flat at the base and at the arms
2
- // (the classic sculpted bust), plus the gear that sells the race at a glance — layered spiked
3
- // pauldrons and a fur mantle (orc), a draped hood (goblin), a tunic with cloak and clasp (human).
4
- // Head units, mirrored across Y; the neck itself belongs to the head.
1
+ // The bust under the head: the anatomy, bust cut and plinth (head-torso.ts) plus the gear that
2
+ // sells the race at a glance — spiked plate pauldrons and a fur mantle (orc, head-armour.ts), a
3
+ // leather vest, scarf and cowl (goblin, head-garb.ts), a draped hood, or a tunic with mantle and
4
+ // brooch (human). Head units, mirrored across Y; the neck itself belongs to the head.
5
5
 
6
6
  import type { Vec3 } from '../core/index.ts';
7
- import type { SculptEdit } from '../primitives/index.ts';
8
- import { armour, chestOf } from './head-armour.ts';
7
+ import { type SculptEdit, sculptField } from '../primitives/index.ts';
8
+ import { armour } from './head-armour.ts';
9
+ import { vest } from './head-garb.ts';
9
10
  import type { HeadLayout } from './head-layout.ts';
10
11
  import type { HeadParams } from './head-params.ts';
11
-
12
- /** Bare-chested torso forms under the armour: pectorals with a sternal groove, deltoid caps. */
13
- function torso(p: HeadParams, cx: number, b: number): SculptEdit[] {
14
- const c = p.colors;
15
- return [
16
- {
17
- shape: 'ellipsoid',
18
- at: [cx + 0.17, 0.16, b + 0.32],
19
- size: [0.16, 0.19, 0.12],
20
- rotate: [0, 0, 12],
21
- color: c.skin,
22
- blend: 0.07,
23
- },
24
- {
25
- shape: 'capsule',
26
- at: [cx + 0.33, 0, b + 0.42],
27
- to: [cx + 0.34, 0, b + 0.1],
28
- size: 0.012,
29
- op: 'subtract',
30
- blend: 0.04,
31
- color: c.shade,
32
- mirror: false,
33
- },
34
- ];
35
- }
12
+ import {
13
+ type BustFrame,
14
+ bodyEdits,
15
+ bustCut,
16
+ frameOf,
17
+ plinthEdits,
18
+ torsoForms,
19
+ } from './head-torso.ts';
36
20
 
37
21
  function hood(p: HeadParams, cx: number, nz: number): SculptEdit[] {
38
22
  const c = p.colors;
@@ -80,14 +64,16 @@ function hood(p: HeadParams, cx: number, nz: number): SculptEdit[] {
80
64
  ];
81
65
  }
82
66
 
83
- function tunic(p: HeadParams, cx: number, s: number, b: number, nz: number): SculptEdit[] {
67
+ function tunic(p: HeadParams, f: BustFrame): SculptEdit[] {
84
68
  const c = p.colors;
85
- const chest = chestOf(cx, s, b);
86
- const shell = (g: number): SculptEdit => ({
87
- shape: 'ellipsoid',
88
- at: chest.at,
89
- size: [chest.r[0] + g, chest.r[1] + g, chest.r[2] + g],
90
- });
69
+ const { cx, s, b, nz } = f;
70
+ const shell = (g: number): SculptEdit => ({ group: torsoForms(f, g) });
71
+ const torso = sculptField(torsoForms(f), { color: c.cloth, mirror: 'y' }).field;
72
+ const front = (z: number): number => {
73
+ let x = cx + 0.7;
74
+ for (let i = 0; i < 300 && torso([x, 0, z]) > 0; i++) x -= 0.004;
75
+ return x;
76
+ };
91
77
  const n = p.neck.radius;
92
78
  const axis = (z: number): Vec3 => [cx - 0.08, 0, z];
93
79
  const mantle = (g: number): SculptEdit => ({
@@ -117,6 +103,7 @@ function tunic(p: HeadParams, cx: number, s: number, b: number, nz: number): Scu
117
103
  mantle(0),
118
104
  { shape: 'roundBox', at: [0, 0, b + 0.3 + 1], size: [4, 4, 2], op: 'intersect' },
119
105
  opening,
106
+ bustCut(f),
120
107
  ],
121
108
  color: c.cape ?? c.leather,
122
109
  blend: 0.01,
@@ -154,68 +141,44 @@ function tunic(p: HeadParams, cx: number, s: number, b: number, nz: number): Scu
154
141
  blend: 0.004,
155
142
  mirror: false,
156
143
  },
157
- ...[0.42, 0.3, 0.18].map(
144
+ // Brass buttons seated on the chest surface (probed along −X at the midline).
145
+ ...[0.5, 0.38, 0.26].map(
158
146
  (z): SculptEdit => ({
159
- group: [
160
- shell(0.03),
161
- { shape: 'sphere', at: [cx + 0.3, 0, b + z], size: 0.03, op: 'intersect' },
162
- ],
147
+ shape: 'sphere',
148
+ at: [front(b + z) + 0.014, 0, b + z],
149
+ size: 0.028,
163
150
  color: c.metal,
164
- blend: 0.003,
151
+ blend: 0.004,
165
152
  mirror: false,
166
153
  }),
167
154
  ),
168
155
  ];
169
156
  }
170
157
 
171
- export function bustEdits(p: HeadParams, l: HeadLayout): SculptEdit[] {
158
+ /** `cell`: the bust's sculpt cell (head units), floors the plinth disc so far LODs keep it. */
159
+ export function bustEdits(p: HeadParams, l: HeadLayout, cell = 0): SculptEdit[] {
172
160
  const c = p.colors;
173
- const cx = l.cranium.at[0] + p.jaw.muzzle * 0.3;
174
- const s = p.bust.shoulders;
175
- const b = l.base;
176
- const nz = b + 0.6; // neck root
177
- const body = p.bust.style === 'pauldrons' ? c.skin : c.cloth;
161
+ const f = frameOf(p, l);
162
+ const style = p.bust.style;
163
+ const bare = style === 'pauldrons' || style === 'vest';
178
164
  const e: SculptEdit[] = [
179
- { shape: 'ellipsoid', at: chestOf(cx, s, b).at, size: chestOf(cx, s, b).r, color: body },
180
- {
181
- shape: 'capsule',
182
- at: [cx - 0.16, 0.1, nz + 0.16],
183
- to: [cx - 0.1, s * 0.78, b + 0.44],
184
- size: 0.15,
185
- color: body,
186
- blend: 0.15,
187
- },
188
- { shape: 'sphere', at: [cx - 0.04, s * 0.84, b + 0.3], size: 0.2, color: body, blend: 0.1 },
189
- // Trapezius: from high behind the neck down to the shoulder.
190
- {
191
- shape: 'capsule',
192
- at: [cx - 0.2, 0.08, nz + 0.3],
193
- to: [cx - 0.1, s * 0.62, b + 0.5],
194
- size: 0.1 + p.neck.radius * 0.2,
195
- color: body,
196
- blend: 0.12,
197
- },
165
+ ...bodyEdits(p, f, bare ? c.skin : c.cloth),
166
+ // Neck root in skin, so the head's neck seats into the bust without a cloth ring (flattened,
167
+ // so it never pokes through the chest below the collar).
198
168
  {
199
- shape: 'capsule',
200
- at: [cx - 0.08, 0, nz - 0.05],
201
- to: [cx - 0.1, 0, nz + 0.12],
202
- size: p.neck.radius * 1.05,
169
+ shape: 'ellipsoid',
170
+ at: [f.cx - 0.09, 0, f.nz + 0.06],
171
+ size: [f.n * 1.02, f.n * 1.02, 0.12],
203
172
  color: c.skin,
204
- blend: 0.03,
173
+ blend: 0.04,
205
174
  },
206
175
  ];
207
- // Arms cut flat at the shoulders (before the gear, so pauldrons overhang the cut).
208
- e.push({
209
- shape: 'roundBox',
210
- at: [0, s + 1.02, b + 0.5],
211
- size: [4, 2, 4],
212
- op: 'subtract',
213
- color: body,
214
- });
215
- if (p.bust.style === 'pauldrons') e.push(...torso(p, cx, b), ...armour(p, cx, s, b, nz));
216
- else if (p.bust.style === 'hood') e.push(...hood(p, cx, nz));
217
- else e.push(...tunic(p, cx, s, b, nz));
176
+ if (style === 'pauldrons') e.push(...armour(p, f, cell));
177
+ else if (style === 'hood') e.push(...hood(p, f.cx, f.nz));
178
+ else if (style === 'vest') e.push(...vest(p, f));
179
+ else e.push(...tunic(p, f));
180
+ e.push(...plinthEdits(p, f, cell * 1.5));
218
181
  // Base cut flat at z = base.
219
- e.push({ shape: 'roundBox', at: [0, 0, b - 1], size: [4, 4, 2], op: 'subtract' });
182
+ e.push({ shape: 'roundBox', at: [0, 0, f.floor - 1], size: [4, 4, 2], op: 'subtract' });
220
183
  return e;
221
184
  }