@dice-o-rolla/dice-assets 0.3.1 → 0.5.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 (42) hide show
  1. package/README.md +95 -6
  2. package/THIRD_PARTY_NOTICES.md +3 -0
  3. package/assets/runtime/catalog.json +649 -0
  4. package/assets/runtime/previews/diagnostic-d10.svg +1 -0
  5. package/assets/runtime/previews/diagnostic-d100.svg +1 -0
  6. package/assets/runtime/previews/diagnostic-d12.svg +1 -0
  7. package/assets/runtime/previews/diagnostic-d20.svg +1 -0
  8. package/assets/runtime/previews/diagnostic-d4.svg +1 -0
  9. package/assets/runtime/previews/diagnostic-d6.svg +1 -0
  10. package/assets/runtime/previews/diagnostic-d66.svg +1 -0
  11. package/assets/runtime/previews/diagnostic-d8.svg +1 -0
  12. package/assets/runtime/textures/diagnostic-d10.ktx2 +0 -0
  13. package/assets/runtime/textures/diagnostic-d100.ktx2 +0 -0
  14. package/assets/runtime/textures/diagnostic-d12.ktx2 +0 -0
  15. package/assets/runtime/textures/diagnostic-d20.ktx2 +0 -0
  16. package/assets/runtime/textures/diagnostic-d4.ktx2 +0 -0
  17. package/assets/runtime/textures/diagnostic-d6.ktx2 +0 -0
  18. package/assets/runtime/textures/diagnostic-d66.ktx2 +0 -0
  19. package/assets/runtime/textures/diagnostic-d8.ktx2 +0 -0
  20. package/dist/asset-registry.d.ts +2 -1
  21. package/dist/asset-registry.js +37 -0
  22. package/dist/catalog-loader.js +19 -1
  23. package/dist/index.d.ts +2 -1
  24. package/dist/index.js +1 -0
  25. package/dist/textured-skin-set.d.ts +32 -0
  26. package/dist/textured-skin-set.js +115 -0
  27. package/dist/three-material-provider.d.ts +1 -0
  28. package/dist/three-material-provider.js +61 -20
  29. package/dist/tools/build.d.ts +2 -0
  30. package/dist/tools/build.js +232 -0
  31. package/dist/tools/cli.d.ts +2 -0
  32. package/dist/tools/cli.js +63 -0
  33. package/dist/tools/index.d.ts +3 -0
  34. package/dist/tools/index.js +3 -0
  35. package/dist/tools/template.d.ts +9 -0
  36. package/dist/tools/template.js +88 -0
  37. package/dist/tools/types.d.ts +40 -0
  38. package/dist/tools/types.js +10 -0
  39. package/dist/tools/write-directory.d.ts +2 -0
  40. package/dist/tools/write-directory.js +56 -0
  41. package/dist/types.d.ts +13 -0
  42. package/package.json +16 -4
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="2048" height="2048" viewBox="0 0 2048 2048"><defs><pattern id="grid" width="16" height="16" patternUnits="userSpaceOnUse"><path d="M 16 0 H 0 V 16" fill="none" stroke="#142035" stroke-opacity=".2" stroke-width="1"/></pattern></defs><polygon points="1024,1290.0430040425795 1638.3999999999999,1290.0430040425795 1331.2,757.9569959574203" fill="#a8d98c" stroke="#a8d98c" stroke-width="16" stroke-linejoin="round"/><polygon points="1024,1290.0430040425795 1331.2,757.9569959574205 716.8000000000001,757.9569959574205" fill="#8ccce8" stroke="#8ccce8" stroke-width="16" stroke-linejoin="round"/><polygon points="1024,1290.0430040425795 716.8000000000001,757.9569959574205 409.59999999999997,1290.0430040425795" fill="#e5a2cb" stroke="#e5a2cb" stroke-width="16" stroke-linejoin="round"/><polygon points="1024,1290.0430040425795 1331.2,1822.1290121277386 1638.3999999999999,1290.0430040425795" fill="#d3bcf3" stroke="#d3bcf3" stroke-width="16" stroke-linejoin="round"/><polygon points="1024,225.87098787226114 716.8000000000001,757.9569959574205 1331.2,757.9569959574205" fill="#f0da85" stroke="#f0da85" stroke-width="16" stroke-linejoin="round"/><polygon points="1945.6000000000001,757.9569959574203 1331.2,757.9569959574203 1638.3999999999999,1290.0430040425795" fill="#87d9cd" stroke="#87d9cd" stroke-width="16" stroke-linejoin="round"/><polygon points="1945.6000000000001,1822.1290121277386 1638.3999999999999,1290.0430040425795 1331.2,1822.1290121277386" fill="#ef9c99" stroke="#ef9c99" stroke-width="16" stroke-linejoin="round"/><polygon points="102.39999999999992,757.9569959574205 409.59999999999997,1290.0430040425795 716.8000000000001,757.9569959574205" fill="#f6b573" stroke="#f6b573" stroke-width="16" stroke-linejoin="round"/><defs><clipPath id="face-1"><polygon points="1024,1290.0430040425795 1638.3999999999999,1290.0430040425795 1331.2,757.9569959574203"/></clipPath></defs><g clip-path="url(#face-1)"><polygon points="1024,1290.0430040425795 1638.3999999999999,1290.0430040425795 1331.2,757.9569959574203" fill="url(#grid)"/><polygon points="1024,1290.0430040425795 1638.3999999999999,1290.0430040425795 1331.2,757.9569959574203" fill="none" stroke="#162338" stroke-width="3"/><path d="M 1208.32 1290.0430040425795 l 50.50336890020568 -72.89533407379194" stroke="#fff" stroke-width="10"/><circle cx="1208.32" cy="1290.0430040425795" r="6" fill="#132035"/><text x="1331.1999999999998" y="1265.212323665272" font-size="30.15154045815901" text-anchor="middle" fill="#142035">0:2</text><path d="M 1546.2399999999998 1130.4172016170317 l -88.38089557536004 -7.289533407379226" stroke="#fff" stroke-width="10"/><circle cx="1546.2399999999998" cy="1130.4172016170317" r="6" fill="#132035"/><text x="1463.2959999999998" y="1036.4153401886535" font-size="30.15154045815901" text-anchor="middle" fill="#142035">2:4</text><path d="M 1116.16 1130.4172016170317 l 88.38089557536004 -7.289533407379233" stroke="#fff" stroke-width="10"/><circle cx="1116.16" cy="1130.4172016170317" r="6" fill="#132035"/><text x="1199.1039999999998" y="1036.4153401886535" font-size="30.15154045815901" text-anchor="middle" fill="#142035">0:4</text><text x="1331.1999999999998" y="1112.6810013475263" font-size="177.36200269505298" font-family="sans-serif" font-weight="bold" text-anchor="middle" dominant-baseline="central" fill="#101a2d" stroke="#fff" stroke-width="1">1</text><path d="M 1224.782798382968 1236.8344032340633 h 212.83440323406356 l -35.4724005390106 -24.83068037730742 m 35.4724005390106 24.83068037730742 l -35.4724005390106 24.83068037730742" fill="none" stroke="#142035" stroke-width="3"/><text x="1331.1999999999998" y="999.1693196226923" font-size="28.377920431208477" text-anchor="middle">d8 / F1</text></g><defs><clipPath id="face-2"><polygon points="1024,1290.0430040425795 1331.2,757.9569959574205 716.8000000000001,757.9569959574205"/></clipPath></defs><g clip-path="url(#face-2)"><polygon points="1024,1290.0430040425795 1331.2,757.9569959574205 716.8000000000001,757.9569959574205" fill="url(#grid)"/><polygon points="1024,1290.0430040425795 1331.2,757.9569959574205 716.8000000000001,757.9569959574205" fill="none" stroke="#162338" stroke-width="3"/><path d="M 1116.1599999999999 1130.4172016170317 l -37.877526675154286 -80.1848674811711" stroke="#fff" stroke-width="10"/><circle cx="1116.1599999999999" cy="1130.4172016170317" r="6" fill="#132035"/><text x="1156.096" y="1011.5846598113462" font-size="30.15154045815901" text-anchor="middle" fill="#142035">0:4</text><path d="M 901.1200000000001 757.9569959574205 l 50.50336890020572 72.89533407379191" stroke="#fff" stroke-width="10"/><circle cx="901.1200000000001" cy="757.9569959574205" r="6" fill="#132035"/><text x="1024" y="782.7876763347278" font-size="30.15154045815901" text-anchor="middle" fill="#142035">3:4</text><path d="M 931.84 1130.4172016170317 l 37.87752667515433 -80.18486748117107" stroke="#fff" stroke-width="10"/><circle cx="931.84" cy="1130.4172016170317" r="6" fill="#132035"/><text x="891.9040000000001" y="1011.5846598113462" font-size="30.15154045815901" text-anchor="middle" fill="#142035">0:3</text><text x="1024" y="935.3189986524735" font-size="177.36200269505298" font-family="sans-serif" font-weight="bold" text-anchor="middle" dominant-baseline="central" fill="#101a2d" stroke="#fff" stroke-width="1">2</text><path d="M 917.5827983829682 1059.4724005390106 h 212.83440323406356 l -35.4724005390106 -24.83068037730742 m 35.4724005390106 24.83068037730742 l -35.4724005390106 24.83068037730742" fill="none" stroke="#142035" stroke-width="3"/><text x="1024" y="821.8073169276396" font-size="28.377920431208477" text-anchor="middle">d8 / F2</text></g><defs><clipPath id="face-3"><polygon points="1024,1290.0430040425795 716.8000000000001,757.9569959574205 409.59999999999997,1290.0430040425795"/></clipPath></defs><g clip-path="url(#face-3)"><polygon points="1024,1290.0430040425795 716.8000000000001,757.9569959574205 409.59999999999997,1290.0430040425795" fill="url(#grid)"/><polygon points="1024,1290.0430040425795 716.8000000000001,757.9569959574205 409.59999999999997,1290.0430040425795" fill="none" stroke="#162338" stroke-width="3"/><path d="M 931.8399999999999 1130.4172016170317 l -88.38089557536006 -7.289533407379137" stroke="#fff" stroke-width="10"/><circle cx="931.8399999999999" cy="1130.4172016170317" r="6" fill="#132035"/><text x="848.8960000000001" y="1036.4153401886538" font-size="30.15154045815901" text-anchor="middle" fill="#142035">0:3</text><path d="M 624.64 917.5827983829682 l 37.87752667515434 80.18486748117107" stroke="#fff" stroke-width="10"/><circle cx="624.64" cy="917.5827983829682" r="6" fill="#132035"/><text x="584.7040000000001" y="1036.4153401886538" font-size="30.15154045815901" text-anchor="middle" fill="#142035">3:5</text><path d="M 839.68 1290.0430040425795 l -50.50336890020572 -72.89533407379191" stroke="#fff" stroke-width="10"/><circle cx="839.68" cy="1290.0430040425795" r="6" fill="#132035"/><text x="716.8" y="1265.212323665272" font-size="30.15154045815901" text-anchor="middle" fill="#142035">0:5</text><text x="716.8000000000001" y="1112.6810013475265" font-size="177.36200269505298" font-family="sans-serif" font-weight="bold" text-anchor="middle" dominant-baseline="central" fill="#101a2d" stroke="#fff" stroke-width="1">3</text><path d="M 610.3827983829683 1236.8344032340635 h 212.83440323406356 l -35.4724005390106 -24.83068037730742 m 35.4724005390106 24.83068037730742 l -35.4724005390106 24.83068037730742" fill="none" stroke="#142035" stroke-width="3"/><text x="716.8000000000001" y="999.1693196226926" font-size="28.377920431208477" text-anchor="middle">d8 / F3</text></g><defs><clipPath id="face-4"><polygon points="1024,1290.0430040425795 1331.2,1822.1290121277386 1638.3999999999999,1290.0430040425795"/></clipPath></defs><g clip-path="url(#face-4)"><polygon points="1024,1290.0430040425795 1331.2,1822.1290121277386 1638.3999999999999,1290.0430040425795" fill="url(#grid)"/><polygon points="1024,1290.0430040425795 1331.2,1822.1290121277386 1638.3999999999999,1290.0430040425795" fill="none" stroke="#162338" stroke-width="3"/><path d="M 1116.1599999999999 1449.668806468127 l 88.38089557536003 7.289533407379318" stroke="#fff" stroke-width="10"/><circle cx="1116.1599999999999" cy="1449.668806468127" r="6" fill="#132035"/><text x="1199.1039999999998" y="1543.6706678965052" font-size="30.15154045815901" text-anchor="middle" fill="#142035">0:5</text><path d="M 1546.24 1449.668806468127 l -88.38089557536003 7.28953340737931" stroke="#fff" stroke-width="10"/><circle cx="1546.24" cy="1449.668806468127" r="6" fill="#132035"/><text x="1463.2959999999998" y="1543.6706678965052" font-size="30.15154045815901" text-anchor="middle" fill="#142035">2:5</text><path d="M 1208.32 1290.0430040425795 l 50.50336890020568 72.89533407379194" stroke="#fff" stroke-width="10"/><circle cx="1208.32" cy="1290.0430040425795" r="6" fill="#132035"/><text x="1331.1999999999998" y="1314.8736844198868" font-size="30.15154045815901" text-anchor="middle" fill="#142035">0:2</text><text x="1331.1999999999998" y="1467.4050067376327" font-size="177.36200269505298" font-family="sans-serif" font-weight="bold" text-anchor="middle" dominant-baseline="central" fill="#101a2d" stroke="#fff" stroke-width="1">4</text><path d="M 1224.782798382968 1591.5584086241697 h 212.83440323406356 l -35.4724005390106 -24.83068037730742 m 35.4724005390106 24.83068037730742 l -35.4724005390106 24.83068037730742" fill="none" stroke="#142035" stroke-width="3"/><text x="1331.1999999999998" y="1353.8933250127989" font-size="28.377920431208477" text-anchor="middle">d8 / F4</text></g><defs><clipPath id="face-5"><polygon points="1024,225.87098787226114 716.8000000000001,757.9569959574205 1331.2,757.9569959574205"/></clipPath></defs><g clip-path="url(#face-5)"><polygon points="1024,225.87098787226114 716.8000000000001,757.9569959574205 1331.2,757.9569959574205" fill="url(#grid)"/><polygon points="1024,225.87098787226114 716.8000000000001,757.9569959574205 1331.2,757.9569959574205" fill="none" stroke="#162338" stroke-width="3"/><path d="M 931.8399999999999 385.49679029780896 l 37.877526675154314 80.1848674811711" stroke="#fff" stroke-width="10"/><circle cx="931.8399999999999" cy="385.49679029780896" r="6" fill="#132035"/><text x="891.9040000000001" y="504.32933210349455" font-size="30.151540458159012" text-anchor="middle" fill="#142035">1:3</text><path d="M 901.12 757.9569959574205 l 50.50336890020574 -72.89533407379193" stroke="#fff" stroke-width="10"/><circle cx="901.12" cy="757.9569959574205" r="6" fill="#132035"/><text x="1024" y="733.1263155801131" font-size="30.151540458159012" text-anchor="middle" fill="#142035">3:4</text><path d="M 1116.16 385.49679029780896 l -37.877526675154314 80.1848674811711" stroke="#fff" stroke-width="10"/><circle cx="1116.16" cy="385.49679029780896" r="6" fill="#132035"/><text x="1156.096" y="504.32933210349455" font-size="30.151540458159012" text-anchor="middle" fill="#142035">1:4</text><text x="1024" y="580.5949932623674" font-size="177.362002695053" font-family="sans-serif" font-weight="bold" text-anchor="middle" dominant-baseline="central" fill="#101a2d" stroke="#fff" stroke-width="1">5</text><path d="M 917.5827983829682 704.7483951489046 h 212.83440323406361 l -35.4724005390106 -24.830680377307424 m 35.4724005390106 24.830680377307424 l -35.4724005390106 24.830680377307424" fill="none" stroke="#142035" stroke-width="3"/><text x="1024" y="467.0833115375335" font-size="28.37792043120848" text-anchor="middle">d8 / F5</text></g><defs><clipPath id="face-6"><polygon points="1945.6000000000001,757.9569959574203 1331.2,757.9569959574203 1638.3999999999999,1290.0430040425795"/></clipPath></defs><g clip-path="url(#face-6)"><polygon points="1945.6000000000001,757.9569959574203 1331.2,757.9569959574203 1638.3999999999999,1290.0430040425795" fill="url(#grid)"/><polygon points="1945.6000000000001,757.9569959574203 1331.2,757.9569959574203 1638.3999999999999,1290.0430040425795" fill="none" stroke="#162338" stroke-width="3"/><path d="M 1761.2800000000002 757.9569959574203 l -50.50336890020583 72.89533407379184" stroke="#fff" stroke-width="10"/><circle cx="1761.2800000000002" cy="757.9569959574203" r="6" fill="#132035"/><text x="1638.4" y="782.7876763347278" font-size="30.15154045815901" text-anchor="middle" fill="#142035">1:4</text><path d="M 1546.24 1130.4172016170317 l 37.87752667515427 -80.18486748117111" stroke="#fff" stroke-width="10"/><circle cx="1546.24" cy="1130.4172016170317" r="6" fill="#132035"/><text x="1506.3039999999999" y="1011.5846598113462" font-size="30.15154045815901" text-anchor="middle" fill="#142035">2:4</text><path d="M 1853.44 917.5827983829681 l -88.38089557536004 7.289533407379172" stroke="#fff" stroke-width="10"/><circle cx="1853.44" cy="917.5827983829681" r="6" fill="#132035"/><text x="1770.4959999999999" y="1011.5846598113462" font-size="30.15154045815901" text-anchor="middle" fill="#142035">1:2</text><text x="1638.3999999999999" y="935.3189986524734" font-size="177.36200269505298" font-family="sans-serif" font-weight="bold" text-anchor="middle" dominant-baseline="central" fill="#101a2d" stroke="#fff" stroke-width="1">6</text><path d="M 1531.982798382968 1059.4724005390106 h 212.83440323406356 l -35.4724005390106 -24.83068037730742 m 35.4724005390106 24.83068037730742 l -35.4724005390106 24.83068037730742" fill="none" stroke="#142035" stroke-width="3"/><text x="1638.3999999999999" y="821.8073169276395" font-size="28.377920431208477" text-anchor="middle">d8 / F6</text></g><defs><clipPath id="face-7"><polygon points="1945.6000000000001,1822.1290121277386 1638.3999999999999,1290.0430040425795 1331.2,1822.1290121277386"/></clipPath></defs><g clip-path="url(#face-7)"><polygon points="1945.6000000000001,1822.1290121277386 1638.3999999999999,1290.0430040425795 1331.2,1822.1290121277386" fill="url(#grid)"/><polygon points="1945.6000000000001,1822.1290121277386 1638.3999999999999,1290.0430040425795 1331.2,1822.1290121277386" fill="none" stroke="#162338" stroke-width="3"/><path d="M 1853.44 1662.5032097021908 l -88.38089557536004 -7.289533407379125" stroke="#fff" stroke-width="10"/><circle cx="1853.44" cy="1662.5032097021908" r="6" fill="#132035"/><text x="1770.4959999999999" y="1568.5013482738127" font-size="30.151540458159005" text-anchor="middle" fill="#142035">1:2</text><path d="M 1546.2399999999998 1449.668806468127 l 37.877526675154286 80.18486748117108" stroke="#fff" stroke-width="10"/><circle cx="1546.2399999999998" cy="1449.668806468127" r="6" fill="#132035"/><text x="1506.3039999999999" y="1568.5013482738127" font-size="30.151540458159005" text-anchor="middle" fill="#142035">2:5</text><path d="M 1761.2800000000002 1822.1290121277386 l -50.503368900205835 -72.89533407379182" stroke="#fff" stroke-width="10"/><circle cx="1761.2800000000002" cy="1822.1290121277386" r="6" fill="#132035"/><text x="1638.4" y="1797.2983317504313" font-size="30.151540458159005" text-anchor="middle" fill="#142035">1:5</text><text x="1638.3999999999999" y="1644.7670094326857" font-size="177.36200269505295" font-family="sans-serif" font-weight="bold" text-anchor="middle" dominant-baseline="central" fill="#101a2d" stroke="#fff" stroke-width="1">7</text><path d="M 1531.9827983829682 1768.9204113192227 h 212.83440323406353 l -35.47240053901059 -24.830680377307417 m 35.47240053901059 24.830680377307417 l -35.47240053901059 24.830680377307417" fill="none" stroke="#142035" stroke-width="3"/><text x="1638.3999999999999" y="1531.2553277078518" font-size="28.377920431208473" text-anchor="middle">d8 / F7</text></g><defs><clipPath id="face-8"><polygon points="102.39999999999992,757.9569959574205 409.59999999999997,1290.0430040425795 716.8000000000001,757.9569959574205"/></clipPath></defs><g clip-path="url(#face-8)"><polygon points="102.39999999999992,757.9569959574205 409.59999999999997,1290.0430040425795 716.8000000000001,757.9569959574205" fill="url(#grid)"/><polygon points="102.39999999999992,757.9569959574205 409.59999999999997,1290.0430040425795 716.8000000000001,757.9569959574205" fill="none" stroke="#162338" stroke-width="3"/><path d="M 194.55999999999992 917.5827983829682 l 88.38089557536006 7.289533407379178" stroke="#fff" stroke-width="10"/><circle cx="194.55999999999992" cy="917.5827983829682" r="6" fill="#132035"/><text x="277.50399999999996" y="1011.5846598113462" font-size="30.15154045815901" text-anchor="middle" fill="#142035">1:5</text><path d="M 624.64 917.5827983829683 l -88.38089557536006 7.289533407379131" stroke="#fff" stroke-width="10"/><circle cx="624.64" cy="917.5827983829683" r="6" fill="#132035"/><text x="541.696" y="1011.5846598113462" font-size="30.15154045815901" text-anchor="middle" fill="#142035">3:5</text><path d="M 286.71999999999997 757.9569959574205 l 50.50336890020576 72.89533407379189" stroke="#fff" stroke-width="10"/><circle cx="286.71999999999997" cy="757.9569959574205" r="6" fill="#132035"/><text x="409.6" y="782.7876763347278" font-size="30.15154045815901" text-anchor="middle" fill="#142035">1:3</text><text x="409.59999999999997" y="935.3189986524735" font-size="177.36200269505298" font-family="sans-serif" font-weight="bold" text-anchor="middle" dominant-baseline="central" fill="#101a2d" stroke="#fff" stroke-width="1">8</text><path d="M 303.1827983829682 1059.4724005390106 h 212.83440323406356 l -35.4724005390106 -24.83068037730742 m 35.4724005390106 24.83068037730742 l -35.4724005390106 24.83068037730742" fill="none" stroke="#142035" stroke-width="3"/><text x="409.59999999999997" y="821.8073169276396" font-size="28.377920431208477" text-anchor="middle">d8 / F8</text></g></svg>
@@ -1,4 +1,4 @@
1
- import type { AudioBankDefinition, AudioSpriteManifest, DiceAssetCatalogManifest, DiceFaceAtlasDefinition, DiceMaterialDefinition, DicePatternDefinition, DiceSkinDefinition } from './types.js';
1
+ import type { AudioBankDefinition, AudioSpriteManifest, DiceAssetCatalogManifest, DiceFaceAtlasDefinition, DiceMaterialDefinition, DicePatternDefinition, DiceSkinDefinition, DiceSkinSetDefinition } from './types.js';
2
2
  export interface RegisterDiceAssetOptions {
3
3
  readonly replace?: boolean;
4
4
  }
@@ -20,6 +20,7 @@ export declare class DiceAssetRegistry {
20
20
  readonly materials: AssetRegistry<DiceMaterialDefinition>;
21
21
  readonly patterns: AssetRegistry<DicePatternDefinition>;
22
22
  readonly skins: AssetRegistry<DiceSkinDefinition>;
23
+ readonly skinSets: AssetRegistry<DiceSkinSetDefinition>;
23
24
  readonly faces: AssetRegistry<DiceFaceAtlasDefinition>;
24
25
  get revision(): number;
25
26
  registerCatalog(catalog: DiceAssetCatalogManifest): void;
@@ -60,6 +60,7 @@ export class DiceAssetRegistry {
60
60
  materials = new AssetRegistry(validateMaterial);
61
61
  patterns = new AssetRegistry(validatePattern);
62
62
  skins = new AssetRegistry(validateSkin);
63
+ skinSets = new AssetRegistry(validateSkinSet);
63
64
  faces = new AssetRegistry(validateFaceAtlas);
64
65
  get revision() {
65
66
  return (this.audio.revision +
@@ -67,6 +68,7 @@ export class DiceAssetRegistry {
67
68
  this.materials.revision +
68
69
  this.patterns.revision +
69
70
  this.skins.revision +
71
+ this.skinSets.revision +
70
72
  this.faces.revision);
71
73
  }
72
74
  registerCatalog(catalog) {
@@ -84,9 +86,17 @@ export class DiceAssetRegistry {
84
86
  this.faces.register(asset);
85
87
  for (const asset of catalog.skins ?? [])
86
88
  this.skins.register(asset);
89
+ for (const asset of catalog.skinSets ?? [])
90
+ this.skinSets.register(asset);
87
91
  this.validateReferences();
88
92
  }
89
93
  validateReferences() {
94
+ for (const set of this.skinSets.list()) {
95
+ for (const [dieType, skinId] of Object.entries(set.skins)) {
96
+ if (!this.skins.has(skinId))
97
+ throw new Error(`Skin set "${set.id}" ${dieType} references missing skin "${skinId}"`);
98
+ }
99
+ }
90
100
  for (const bank of this.audioBanks.list()) {
91
101
  const sprite = this.audio.get(bank.spriteId);
92
102
  if (sprite === undefined)
@@ -177,6 +187,25 @@ function validateMaterial(material) {
177
187
  }
178
188
  }
179
189
  function validatePattern(pattern) {
190
+ if (pattern.unwrap !== undefined) {
191
+ assertId(pattern.unwrap.geometryId);
192
+ if (pattern.repeat !== undefined)
193
+ throw new RangeError('unwrapped patterns cannot repeat');
194
+ if (Object.keys(pattern.unwrap.faces).length === 0)
195
+ throw new RangeError('unwrap must contain faces');
196
+ for (const [face, uvs] of Object.entries(pattern.unwrap.faces)) {
197
+ if (!/^[1-9]\d*$/.test(face) ||
198
+ !Array.isArray(uvs) ||
199
+ uvs.length < 3 ||
200
+ !uvs.every((uv) => Array.isArray(uv) &&
201
+ uv.length === 2 &&
202
+ uv.every((value) => Number.isFinite(value) && value >= 0 && value <= 1))) {
203
+ throw new RangeError(`Pattern "${pattern.id}" face ${face}: invalid surface UVs`);
204
+ }
205
+ }
206
+ if (pattern.unwrap.preview !== undefined)
207
+ validateReference(pattern.unwrap.preview, 'unwrap preview');
208
+ }
180
209
  validateTexture(pattern.baseColor, 'baseColor');
181
210
  if (pattern.normal !== undefined)
182
211
  validateTexture(pattern.normal, 'normal');
@@ -201,3 +230,11 @@ function validateFaceAtlas(atlas) {
201
230
  throw new RangeError('face atlas dimensions must be positive integers');
202
231
  }
203
232
  }
233
+ function validateSkinSet(set) {
234
+ if (Object.keys(set.skins).length === 0)
235
+ throw new RangeError('skin set must contain skins');
236
+ for (const [dieType, skinId] of Object.entries(set.skins)) {
237
+ assertId(dieType);
238
+ assertId(skinId);
239
+ }
240
+ }
@@ -35,6 +35,16 @@ export function resolveCatalogReferences(catalog, catalogUri) {
35
35
  patterns: catalog.patterns.map((pattern) => ({
36
36
  ...pattern,
37
37
  baseColor: resolveReference(pattern.baseColor),
38
+ ...(pattern.unwrap === undefined
39
+ ? {}
40
+ : {
41
+ unwrap: {
42
+ ...pattern.unwrap,
43
+ ...(pattern.unwrap.preview === undefined
44
+ ? {}
45
+ : { preview: resolveReference(pattern.unwrap.preview) }),
46
+ },
47
+ }),
38
48
  ...(pattern.normal === undefined ? {} : { normal: resolveReference(pattern.normal) }),
39
49
  ...(pattern.orm === undefined ? {} : { orm: resolveReference(pattern.orm) }),
40
50
  })),
@@ -69,7 +79,15 @@ function isCatalogManifest(source) {
69
79
  return false;
70
80
  if (source.schemaVersion !== 1)
71
81
  return false;
72
- for (const key of ['audioSprites', 'audioBanks', 'materials', 'patterns', 'skins', 'faces']) {
82
+ for (const key of [
83
+ 'audioSprites',
84
+ 'audioBanks',
85
+ 'materials',
86
+ 'patterns',
87
+ 'skins',
88
+ 'faces',
89
+ 'skinSets',
90
+ ]) {
73
91
  if (source[key] !== undefined && !Array.isArray(source[key]))
74
92
  return false;
75
93
  }
package/dist/index.d.ts CHANGED
@@ -3,4 +3,5 @@ export { resolveImpactGain, WebAudioSpritePlayer, type AudioBufferSourceNodeLike
3
3
  export { ThreeAssetMaterialProvider, type ThreeAssetMaterialProviderOptions, } from './three-material-provider.js';
4
4
  export { DiceAssetCatalogLoader, parseCatalog, resolveCatalogReferences, type DiceAssetCatalogLoaderOptions, } from './catalog-loader.js';
5
5
  export { ImpactSoundGate, type SoundCollisionEvent, type SoundImpactEvent, } from './impact-sound-gate.js';
6
- export type { AudioBankDefinition, AudioBankKind, AudioSpriteClip, AudioSpriteManifest, DiceAssetCatalogManifest, DiceAssetMetadataValue, DiceAssetReference, DiceFaceAtlasDefinition, DiceFaceRegion, DiceMaterialDefinition, DicePatternDefinition, DiceSkinCompositeMode, DiceSkinDefinition, RuntimeTextureReference, } from './types.js';
6
+ export type { AudioBankDefinition, AudioBankKind, AudioSpriteClip, AudioSpriteManifest, DiceAssetCatalogManifest, DiceAssetMetadataValue, DiceAssetReference, DiceFaceAtlasDefinition, DiceFaceRegion, DiceMaterialDefinition, DicePatternDefinition, DiceSkinCompositeMode, DiceSkinDefinition, DiceSkinSetDefinition, DiceSurfaceUnwrap, RuntimeTextureReference, } from './types.js';
7
+ export { prepareTexturedSkinSet, type PreparedTexturedSkinSet, type PrepareTexturedSkinSetOptions, type TexturedSkinSetTarget, type TexturedSkinSetProvider, type TexturedSkinSetSelectionContext, } from './textured-skin-set.js';
package/dist/index.js CHANGED
@@ -3,3 +3,4 @@ export { resolveImpactGain, WebAudioSpritePlayer, } from './web-audio-player.js'
3
3
  export { ThreeAssetMaterialProvider, } from './three-material-provider.js';
4
4
  export { DiceAssetCatalogLoader, parseCatalog, resolveCatalogReferences, } from './catalog-loader.js';
5
5
  export { ImpactSoundGate, } from './impact-sound-gate.js';
6
+ export { prepareTexturedSkinSet, } from './textured-skin-set.js';
@@ -0,0 +1,32 @@
1
+ import type { VisualPresetDescriptor } from '@dice-o-rolla/dice-renderer';
2
+ import type { DiceAssetRegistry } from './asset-registry.js';
3
+ export interface TexturedSkinSetTarget {
4
+ registerVisualPreset(preset: VisualPresetDescriptor): VisualPresetDescriptor;
5
+ unregisterVisualPreset(id: string): boolean;
6
+ }
7
+ export interface TexturedSkinSetProvider {
8
+ readonly registry: DiceAssetRegistry;
9
+ prepareSkin(id: string): Promise<void>;
10
+ }
11
+ export interface PrepareTexturedSkinSetOptions {
12
+ readonly registry: DiceAssetRegistry;
13
+ readonly provider: TexturedSkinSetProvider;
14
+ readonly skinSetId: string;
15
+ readonly presetPrefix?: string;
16
+ readonly soundPackId?: string;
17
+ }
18
+ export interface TexturedSkinSetSelectionContext {
19
+ readonly physicalDieType: string;
20
+ readonly component?: {
21
+ readonly role: string;
22
+ readonly groupType: string;
23
+ };
24
+ }
25
+ export interface PreparedTexturedSkinSet {
26
+ readonly presets: readonly VisualPresetDescriptor[];
27
+ /** Register without changing defaults. Repeated registration through this API is idempotent. */
28
+ register(engine: TexturedSkinSetTarget): void;
29
+ readonly visualPresetSelector: (context: TexturedSkinSetSelectionContext) => string | undefined;
30
+ }
31
+ /** Validate and load first; engine registration is an explicit, separate operation. */
32
+ export declare function prepareTexturedSkinSet(options: PrepareTexturedSkinSetOptions): Promise<PreparedTexturedSkinSet>;
@@ -0,0 +1,115 @@
1
+ import { getDieGeometry } from '@dice-o-rolla/dice-geometry';
2
+ import { validateSurfaceUvs } from '@dice-o-rolla/dice-renderer-three';
3
+ const registrations = new WeakMap();
4
+ const geometries = {
5
+ d4: 'd4',
6
+ d6: 'd6',
7
+ d8: 'd8',
8
+ d10: 'd10',
9
+ d12: 'd12',
10
+ d20: 'd20',
11
+ d100: 'd10',
12
+ d66: 'd6',
13
+ };
14
+ const identifier = /^[a-z0-9][a-z0-9._:-]*$/i;
15
+ /** Validate and load first; engine registration is an explicit, separate operation. */
16
+ export async function prepareTexturedSkinSet(options) {
17
+ const { registry, provider, skinSetId } = options;
18
+ if (provider.registry !== registry)
19
+ throw new Error('The material provider must use the supplied asset registry');
20
+ registry.validateReferences();
21
+ const set = registry.skinSets.get(skinSetId);
22
+ if (set === undefined)
23
+ throw new Error(`Unknown skin set: ${skinSetId}`);
24
+ const prefix = options.presetPrefix ?? `skin-set:${skinSetId}`;
25
+ if (!identifier.test(prefix) ||
26
+ (options.soundPackId !== undefined && !identifier.test(options.soundPackId)))
27
+ throw new RangeError('Preset prefix and sound pack id must be portable identifiers');
28
+ const entries = Object.entries(set.skins).map(([type, skinId]) => {
29
+ const geometryId = Object.hasOwn(geometries, type) ? geometries[type] : undefined;
30
+ if (geometryId === undefined)
31
+ throw new Error(`Skin set "${skinSetId}" has unsupported type "${type}"`);
32
+ const skin = registry.skins.get(skinId);
33
+ const pattern = registry.patterns.get(skin.patternId);
34
+ if (pattern.unwrap?.geometryId !== geometryId)
35
+ throw new Error(`Skin "${skinId}" for ${type} requires an unwrap for ${geometryId}`);
36
+ try {
37
+ validateSurfaceUvs(getDieGeometry(geometryId), pattern.unwrap.faces);
38
+ }
39
+ catch (error) {
40
+ throw new Error(`Skin "${skinId}": ${error instanceof Error ? error.message : String(error)}`, { cause: error });
41
+ }
42
+ const preset = Object.freeze({
43
+ id: `${prefix}:${type}`,
44
+ dieType: geometryId,
45
+ geometryId,
46
+ skinId,
47
+ ...(options.soundPackId === undefined ? {} : { soundPackId: options.soundPackId }),
48
+ });
49
+ return {
50
+ type,
51
+ skinId,
52
+ preset,
53
+ fingerprint: canonical({
54
+ preset,
55
+ skin,
56
+ pattern,
57
+ material: registry.materials.get(skin.materialId),
58
+ atlas: skin.faceAtlasId === undefined ? null : registry.faces.get(skin.faceAtlasId),
59
+ }),
60
+ };
61
+ });
62
+ await Promise.all([...new Set(entries.map((entry) => entry.skinId))].map((id) => provider.prepareSkin(id)));
63
+ const byType = new Map(entries.map((entry) => [entry.type, entry.preset.id]));
64
+ return Object.freeze({
65
+ presets: Object.freeze(entries.map((entry) => entry.preset)),
66
+ register(engine) {
67
+ const known = registrations.get(engine) ?? new Map();
68
+ for (const entry of entries) {
69
+ const previous = known.get(entry.preset.id);
70
+ if (previous !== undefined && previous !== entry.fingerprint)
71
+ throw new Error(`Conflicting preset: ${entry.preset.id}`);
72
+ }
73
+ const added = [];
74
+ try {
75
+ for (const entry of entries) {
76
+ if (known.has(entry.preset.id))
77
+ continue;
78
+ engine.registerVisualPreset(entry.preset);
79
+ added.push(entry.preset.id);
80
+ known.set(entry.preset.id, entry.fingerprint);
81
+ }
82
+ }
83
+ catch (error) {
84
+ const rollbackErrors = [];
85
+ for (const id of added.toReversed()) {
86
+ try {
87
+ engine.unregisterVisualPreset(id);
88
+ known.delete(id);
89
+ }
90
+ catch (rollbackError) {
91
+ rollbackErrors.push(rollbackError);
92
+ }
93
+ }
94
+ registrations.set(engine, known);
95
+ if (rollbackErrors.length > 0)
96
+ throw new AggregateError([error, ...rollbackErrors], 'Preset registration and rollback failed', { cause: error });
97
+ throw error;
98
+ }
99
+ registrations.set(engine, known);
100
+ },
101
+ visualPresetSelector(context) {
102
+ return byType.get(context.component?.role === 'tens' ? context.component.groupType : context.physicalDieType);
103
+ },
104
+ });
105
+ }
106
+ function canonical(value) {
107
+ if (Array.isArray(value))
108
+ return `[${value.map(canonical).join(',')}]`;
109
+ if (value !== null && typeof value === 'object')
110
+ return `{${Object.entries(value)
111
+ .toSorted(([a], [b]) => a.localeCompare(b))
112
+ .map(([key, child]) => `${JSON.stringify(key)}:${canonical(child)}`)
113
+ .join(',')}}`;
114
+ return JSON.stringify(value) ?? 'null';
115
+ }
@@ -10,6 +10,7 @@ export declare class ThreeAssetMaterialProvider implements ThreeFaceMaterialProv
10
10
  #private;
11
11
  readonly registry: DiceAssetRegistry;
12
12
  constructor(registry: DiceAssetRegistry, options: ThreeAssetMaterialProviderOptions);
13
+ getSurfaceUvs(...[definition, preset]: Parameters<NonNullable<ThreeFaceMaterialProvider['getSurfaceUvs']>>): Readonly<Record<number, readonly (readonly [u: number, v: number])[]>> | undefined;
13
14
  prepareSkin(skinId: string): Promise<void>;
14
15
  createFace(context: FaceMaterialContext): FaceMaterialResource;
15
16
  dispose(): void;
@@ -1,5 +1,5 @@
1
- import { ThreeMaterialFactory, } from '@dice-o-rolla/dice-renderer-three';
2
- import { Color, MeshPhysicalMaterial, RepeatWrapping, SRGBColorSpace, } from 'three';
1
+ import { validateSurfaceUvs, ThreeMaterialFactory, } from '@dice-o-rolla/dice-renderer-three';
2
+ import { Color, ClampToEdgeWrapping, MeshPhysicalMaterial, RepeatWrapping, SRGBColorSpace, } from 'three';
3
3
  import { KTX2Loader } from 'three/examples/jsm/loaders/KTX2Loader.js';
4
4
  /** KTX2-backed PBR adapter; lower-level renderer packages remain asset-schema agnostic. */
5
5
  export class ThreeAssetMaterialProvider {
@@ -8,13 +8,33 @@ export class ThreeAssetMaterialProvider {
8
8
  #textures = new Map();
9
9
  #skins = new Map();
10
10
  #fallback = new ThreeMaterialFactory();
11
+ #disposed = false;
11
12
  constructor(registry, options) {
12
13
  this.registry = registry;
13
14
  this.#loader = new KTX2Loader()
14
15
  .setTranscoderPath(options.transcoderPath)
15
16
  .detectSupport(options.renderer);
16
17
  }
18
+ getSurfaceUvs(...[definition, preset]) {
19
+ const skin = preset?.skinId === undefined ? undefined : this.registry.skins.get(preset.skinId);
20
+ if (skin === undefined)
21
+ return undefined;
22
+ const unwrap = this.registry.patterns.get(skin.patternId)?.unwrap;
23
+ if (unwrap === undefined)
24
+ return undefined;
25
+ if (unwrap.geometryId !== definition.id)
26
+ throw new RangeError(`Skin "${skin.id}" requires geometry "${unwrap.geometryId}", received "${definition.id}"`);
27
+ try {
28
+ validateSurfaceUvs(definition, unwrap.faces);
29
+ }
30
+ catch (error) {
31
+ throw new RangeError(`Skin "${skin.id}": ${error instanceof Error ? error.message : String(error)}`);
32
+ }
33
+ return unwrap.faces;
34
+ }
17
35
  async prepareSkin(skinId) {
36
+ if (this.#disposed)
37
+ throw new Error('Asset material provider is disposed');
18
38
  if (this.#skins.has(skinId))
19
39
  return;
20
40
  this.registry.validateReferences();
@@ -24,16 +44,15 @@ export class ThreeAssetMaterialProvider {
24
44
  const pattern = this.registry.patterns.get(skin.patternId);
25
45
  const atlas = skin.faceAtlasId === undefined ? undefined : this.registry.faces.get(skin.faceAtlasId);
26
46
  const [baseColor, normal, orm, faces] = await Promise.all([
27
- this.#load(pattern.baseColor),
28
- pattern.normal === undefined ? undefined : this.#load(pattern.normal),
29
- pattern.orm === undefined ? undefined : this.#load(pattern.orm),
47
+ this.#load(pattern.baseColor, pattern.unwrap !== undefined),
48
+ pattern.normal === undefined
49
+ ? undefined
50
+ : this.#load(pattern.normal, pattern.unwrap !== undefined),
51
+ pattern.orm === undefined ? undefined : this.#load(pattern.orm, pattern.unwrap !== undefined),
30
52
  atlas === undefined ? undefined : this.#load(atlas.texture),
31
53
  ]);
32
- baseColor.wrapS = baseColor.wrapT = RepeatWrapping;
33
- if (normal !== undefined)
34
- normal.wrapS = normal.wrapT = RepeatWrapping;
35
- if (orm !== undefined)
36
- orm.wrapS = orm.wrapT = RepeatWrapping;
54
+ if (this.#disposed)
55
+ throw new Error('Asset material provider is disposed');
37
56
  this.#skins.set(skinId, {
38
57
  skin,
39
58
  baseColor,
@@ -70,7 +89,7 @@ export class ThreeAssetMaterialProvider {
70
89
  if (definition.normalScale !== undefined)
71
90
  material.normalScale.setScalar(definition.normalScale);
72
91
  if (loaded.faces !== undefined && region !== undefined && atlas !== undefined) {
73
- configureCompositingShader(material, loaded.faces, region, atlas.width, atlas.height, loaded.skin);
92
+ configureCompositingShader(material, loaded.faces, region, atlas.width, atlas.height, loaded.skin, loaded.baseColor.channel === 1);
74
93
  }
75
94
  return {
76
95
  material,
@@ -80,23 +99,41 @@ export class ThreeAssetMaterialProvider {
80
99
  };
81
100
  }
82
101
  dispose() {
83
- for (const pending of this.#textures.values())
84
- void pending.then((texture) => texture.dispose());
102
+ if (this.#disposed)
103
+ return;
104
+ this.#disposed = true;
105
+ const pending = [...this.#textures.values()];
106
+ for (const texture of pending)
107
+ void texture.then((value) => value.dispose(), () => undefined);
85
108
  this.#textures.clear();
86
109
  this.#skins.clear();
87
- this.#loader.dispose();
110
+ // Let active transcodes settle before terminating their workers.
111
+ void Promise.allSettled(pending).then(() => this.#loader.dispose());
88
112
  }
89
- #load(reference) {
90
- const existing = this.#textures.get(reference.uri);
113
+ #load(reference, surface = false) {
114
+ const key = `${reference.uri}:${reference.colorSpace}:${surface}`;
115
+ const existing = this.#textures.get(key);
91
116
  if (existing !== undefined)
92
117
  return existing;
93
- const pending = this.#loader.loadAsync(reference.uri).then((texture) => {
118
+ const pending = this.#loader
119
+ .loadAsync(reference.uri)
120
+ .then((texture) => {
121
+ if (this.#disposed) {
122
+ texture.dispose();
123
+ throw new Error('Asset material provider is disposed');
124
+ }
94
125
  texture.colorSpace = reference.colorSpace === 'srgb' ? SRGBColorSpace : '';
126
+ texture.channel = surface ? 1 : 0;
127
+ texture.wrapS = texture.wrapT = surface ? ClampToEdgeWrapping : RepeatWrapping;
95
128
  texture.generateMipmaps = false;
96
129
  texture.needsUpdate = true;
97
130
  return texture;
131
+ })
132
+ .catch((error) => {
133
+ this.#textures.delete(key);
134
+ throw new Error(`Texture "${reference.uri}": ${error instanceof Error ? error.message : 'load failed'}`, { cause: error });
98
135
  });
99
- this.#textures.set(reference.uri, pending);
136
+ this.#textures.set(key, pending);
100
137
  return pending;
101
138
  }
102
139
  #createFallback(context) {
@@ -110,7 +147,7 @@ export class ThreeAssetMaterialProvider {
110
147
  };
111
148
  }
112
149
  }
113
- function configureCompositingShader(material, atlas, region, width, height, skin) {
150
+ function configureCompositingShader(material, atlas, region, width, height, skin, surface) {
114
151
  const labelColor = new Color(skin.labelColor ?? '#111111');
115
152
  material.onBeforeCompile = (shader) => {
116
153
  shader.uniforms.faceAtlas = { value: atlas };
@@ -124,9 +161,13 @@ function configureCompositingShader(material, atlas, region, width, height, skin
124
161
  shader.uniforms.compositeMode = {
125
162
  value: skin.composite === 'overlay' ? 2 : skin.composite === 'multiply' ? 1 : 0,
126
163
  };
164
+ shader.vertexShader = shader.vertexShader
165
+ .replace('#include <common>', '#include <common>\nvarying vec2 diceLabelUv;')
166
+ .replace('#include <uv_vertex>', '#include <uv_vertex>\ndiceLabelUv = uv;');
127
167
  shader.fragmentShader = shader.fragmentShader
168
+ .replace('#include <common>', '#include <common>\nvarying vec2 diceLabelUv;')
128
169
  .replace('#include <map_pars_fragment>', `#include <map_pars_fragment>\nuniform sampler2D faceAtlas;\nuniform vec4 faceRect;\nuniform vec3 labelColor;\nuniform float skinHue;\nuniform float skinSaturation;\nuniform vec2 patternScale;\nuniform int compositeMode;`)
129
- .replace('#include <map_fragment>', `#ifdef USE_MAP\nvec4 skinPattern = texture2D(map, fract(vMapUv * patternScale));\nvec3 skinTint = diffuseColor.rgb;\nif (compositeMode == 0) diffuseColor.rgb = mix(skinTint, skinPattern.rgb, skinPattern.a);\nelse if (compositeMode == 1) diffuseColor.rgb = skinTint * skinPattern.rgb;\nelse { vec3 low = 2.0 * skinTint * skinPattern.rgb; vec3 high = 1.0 - 2.0 * (1.0 - skinTint) * (1.0 - skinPattern.rgb); diffuseColor.rgb = mix(low, high, step(vec3(0.5), skinTint)); }\ndiffuseColor.a *= skinPattern.a;\n#endif\nvec3 skinGray = vec3(dot(diffuseColor.rgb, vec3(0.2126, 0.7152, 0.0722)));\ndiffuseColor.rgb = mix(skinGray, diffuseColor.rgb, skinSaturation);\nfloat skinAngle = skinHue * 6.28318530718;\nvec3 skinAxis = normalize(vec3(1.0));\ndiffuseColor.rgb = diffuseColor.rgb * cos(skinAngle) + cross(skinAxis, diffuseColor.rgb) * sin(skinAngle) + skinAxis * dot(skinAxis, diffuseColor.rgb) * (1.0 - cos(skinAngle));\nvec2 faceUv = faceRect.xy + vMapUv * faceRect.zw;\nfloat faceMask = texture2D(faceAtlas, faceUv).a;\ndiffuseColor.rgb = mix(diffuseColor.rgb, labelColor, faceMask);`);
170
+ .replace('#include <map_fragment>', `#ifdef USE_MAP\nvec4 skinPattern = texture2D(map, ${surface ? 'vMapUv' : 'fract(vMapUv * patternScale)'});\nvec3 skinTint = diffuseColor.rgb;\nif (compositeMode == 0) diffuseColor.rgb = mix(skinTint, skinPattern.rgb, skinPattern.a);\nelse if (compositeMode == 1) diffuseColor.rgb = skinTint * skinPattern.rgb;\nelse { vec3 low = 2.0 * skinTint * skinPattern.rgb; vec3 high = 1.0 - 2.0 * (1.0 - skinTint) * (1.0 - skinPattern.rgb); diffuseColor.rgb = mix(low, high, step(vec3(0.5), skinTint)); }\ndiffuseColor.a *= skinPattern.a;\n#endif\nvec3 skinGray = vec3(dot(diffuseColor.rgb, vec3(0.2126, 0.7152, 0.0722)));\ndiffuseColor.rgb = mix(skinGray, diffuseColor.rgb, skinSaturation);\nfloat skinAngle = skinHue * 6.28318530718;\nvec3 skinAxis = normalize(vec3(1.0));\ndiffuseColor.rgb = diffuseColor.rgb * cos(skinAngle) + cross(skinAxis, diffuseColor.rgb) * sin(skinAngle) + skinAxis * dot(skinAxis, diffuseColor.rgb) * (1.0 - cos(skinAngle));\nvec2 faceUv = faceRect.xy + diceLabelUv * faceRect.zw;\nfloat faceMask = texture2D(faceAtlas, faceUv).a;\ndiffuseColor.rgb = mix(diffuseColor.rgb, labelColor, faceMask);`);
130
171
  };
131
172
  material.customProgramCacheKey = () => `dice-skin:${skin.id}`;
132
173
  }
@@ -0,0 +1,2 @@
1
+ import type { BuildTexturedSkinSetOptions, BuiltTexturedSkinSet } from './types.js';
2
+ export declare function buildTexturedSkinSet(options: BuildTexturedSkinSetOptions): Promise<BuiltTexturedSkinSet>;