@jjlmoya/utils-forensic-science 1.1.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 (316) hide show
  1. package/package.json +69 -0
  2. package/scripts/crystal-lattice-catalog-translations.mjs +459 -0
  3. package/scripts/postinstall.mjs +27 -0
  4. package/scripts/sync-crystal-lattice-i18n.mjs +50 -0
  5. package/src/category/i18n/de.ts +55 -0
  6. package/src/category/i18n/en.ts +55 -0
  7. package/src/category/i18n/es.ts +55 -0
  8. package/src/category/i18n/fr.ts +55 -0
  9. package/src/category/i18n/id.ts +55 -0
  10. package/src/category/i18n/it.ts +55 -0
  11. package/src/category/i18n/ja.ts +56 -0
  12. package/src/category/i18n/ko.ts +56 -0
  13. package/src/category/i18n/nl.ts +55 -0
  14. package/src/category/i18n/pl.ts +55 -0
  15. package/src/category/i18n/pt.ts +55 -0
  16. package/src/category/i18n/ru.ts +55 -0
  17. package/src/category/i18n/sv.ts +55 -0
  18. package/src/category/i18n/tr.ts +55 -0
  19. package/src/category/i18n/zh.ts +56 -0
  20. package/src/category/index.ts +48 -0
  21. package/src/category/seo.astro +15 -0
  22. package/src/components/PreviewNavSidebar.astro +116 -0
  23. package/src/components/PreviewToolbar.astro +143 -0
  24. package/src/data.ts +10 -0
  25. package/src/entries.ts +48 -0
  26. package/src/env.d.ts +5 -0
  27. package/src/index.ts +29 -0
  28. package/src/layouts/PreviewLayout.astro +117 -0
  29. package/src/pages/[locale]/[slug].astro +162 -0
  30. package/src/pages/[locale].astro +261 -0
  31. package/src/pages/index.astro +3 -0
  32. package/src/tests/diacritics_density.test.ts +140 -0
  33. package/src/tests/faq_count.test.ts +19 -0
  34. package/src/tests/i18n_coverage.test.ts +36 -0
  35. package/src/tests/inverted_punctuation.test.ts +84 -0
  36. package/src/tests/locale_completeness.test.ts +24 -0
  37. package/src/tests/mocks/astro_mock.js +2 -0
  38. package/src/tests/no_en_dash.test.ts +70 -0
  39. package/src/tests/no_h1_in_components.test.ts +48 -0
  40. package/src/tests/schemas_fulfillment.test.ts +23 -0
  41. package/src/tests/script_density.test.ts +94 -0
  42. package/src/tests/seo_length.test.ts +46 -0
  43. package/src/tests/shared-test-helpers.ts +56 -0
  44. package/src/tests/slug_language_code_format.test.ts +23 -0
  45. package/src/tests/slug_uniqueness.test.ts +81 -0
  46. package/src/tests/title_quality.test.ts +55 -0
  47. package/src/tests/tool_exports.test.ts +34 -0
  48. package/src/tests/tool_validation.test.ts +16 -0
  49. package/src/tests/widmark.test.ts +28 -0
  50. package/src/tool/forensic-age-estimator/bibliography.astro +14 -0
  51. package/src/tool/forensic-age-estimator/bibliography.ts +16 -0
  52. package/src/tool/forensic-age-estimator/component.astro +321 -0
  53. package/src/tool/forensic-age-estimator/dental-skeletal-third-molar-age-estimator.css +634 -0
  54. package/src/tool/forensic-age-estimator/entry.ts +32 -0
  55. package/src/tool/forensic-age-estimator/i18n/de.ts +245 -0
  56. package/src/tool/forensic-age-estimator/i18n/en.ts +245 -0
  57. package/src/tool/forensic-age-estimator/i18n/es.ts +245 -0
  58. package/src/tool/forensic-age-estimator/i18n/fr.ts +245 -0
  59. package/src/tool/forensic-age-estimator/i18n/id.ts +245 -0
  60. package/src/tool/forensic-age-estimator/i18n/it.ts +245 -0
  61. package/src/tool/forensic-age-estimator/i18n/ja.ts +245 -0
  62. package/src/tool/forensic-age-estimator/i18n/ko.ts +245 -0
  63. package/src/tool/forensic-age-estimator/i18n/nl.ts +245 -0
  64. package/src/tool/forensic-age-estimator/i18n/pl.ts +245 -0
  65. package/src/tool/forensic-age-estimator/i18n/pt.ts +245 -0
  66. package/src/tool/forensic-age-estimator/i18n/ru.ts +245 -0
  67. package/src/tool/forensic-age-estimator/i18n/sv.ts +245 -0
  68. package/src/tool/forensic-age-estimator/i18n/tr.ts +245 -0
  69. package/src/tool/forensic-age-estimator/i18n/zh.ts +246 -0
  70. package/src/tool/forensic-age-estimator/index.ts +11 -0
  71. package/src/tool/forensic-age-estimator/logic.ts +81 -0
  72. package/src/tool/forensic-age-estimator/seo.astro +15 -0
  73. package/src/tool/forensic-blood-test-simulator/bibliography.astro +6 -0
  74. package/src/tool/forensic-blood-test-simulator/bibliography.ts +12 -0
  75. package/src/tool/forensic-blood-test-simulator/component.astro +168 -0
  76. package/src/tool/forensic-blood-test-simulator/dom-utils.ts +40 -0
  77. package/src/tool/forensic-blood-test-simulator/entry.ts +32 -0
  78. package/src/tool/forensic-blood-test-simulator/forensic-presumptive-blood-testing-luminol-kastle-meyer-simulator.css +532 -0
  79. package/src/tool/forensic-blood-test-simulator/helpers.ts +16 -0
  80. package/src/tool/forensic-blood-test-simulator/i18n/de.ts +185 -0
  81. package/src/tool/forensic-blood-test-simulator/i18n/en.ts +185 -0
  82. package/src/tool/forensic-blood-test-simulator/i18n/es.ts +185 -0
  83. package/src/tool/forensic-blood-test-simulator/i18n/fr.ts +220 -0
  84. package/src/tool/forensic-blood-test-simulator/i18n/id.ts +220 -0
  85. package/src/tool/forensic-blood-test-simulator/i18n/it.ts +220 -0
  86. package/src/tool/forensic-blood-test-simulator/i18n/ja.ts +220 -0
  87. package/src/tool/forensic-blood-test-simulator/i18n/ko.ts +220 -0
  88. package/src/tool/forensic-blood-test-simulator/i18n/nl.ts +220 -0
  89. package/src/tool/forensic-blood-test-simulator/i18n/pl.ts +220 -0
  90. package/src/tool/forensic-blood-test-simulator/i18n/pt.ts +220 -0
  91. package/src/tool/forensic-blood-test-simulator/i18n/ru.ts +220 -0
  92. package/src/tool/forensic-blood-test-simulator/i18n/sv.ts +220 -0
  93. package/src/tool/forensic-blood-test-simulator/i18n/tr.ts +220 -0
  94. package/src/tool/forensic-blood-test-simulator/i18n/zh.ts +220 -0
  95. package/src/tool/forensic-blood-test-simulator/index.ts +11 -0
  96. package/src/tool/forensic-blood-test-simulator/logic.ts +174 -0
  97. package/src/tool/forensic-blood-test-simulator/script.ts +192 -0
  98. package/src/tool/forensic-blood-test-simulator/seo.astro +15 -0
  99. package/src/tool/forensic-fiber-comparison-microscope/bibliography.astro +6 -0
  100. package/src/tool/forensic-fiber-comparison-microscope/bibliography.ts +16 -0
  101. package/src/tool/forensic-fiber-comparison-microscope/component.astro +146 -0
  102. package/src/tool/forensic-fiber-comparison-microscope/entry.ts +32 -0
  103. package/src/tool/forensic-fiber-comparison-microscope/forensic-fiber-comparison-microscope.css +629 -0
  104. package/src/tool/forensic-fiber-comparison-microscope/i18n/de.ts +250 -0
  105. package/src/tool/forensic-fiber-comparison-microscope/i18n/en.ts +246 -0
  106. package/src/tool/forensic-fiber-comparison-microscope/i18n/es.ts +246 -0
  107. package/src/tool/forensic-fiber-comparison-microscope/i18n/fr.ts +246 -0
  108. package/src/tool/forensic-fiber-comparison-microscope/i18n/id.ts +250 -0
  109. package/src/tool/forensic-fiber-comparison-microscope/i18n/it.ts +246 -0
  110. package/src/tool/forensic-fiber-comparison-microscope/i18n/ja.ts +250 -0
  111. package/src/tool/forensic-fiber-comparison-microscope/i18n/ko.ts +246 -0
  112. package/src/tool/forensic-fiber-comparison-microscope/i18n/nl.ts +246 -0
  113. package/src/tool/forensic-fiber-comparison-microscope/i18n/pl.ts +250 -0
  114. package/src/tool/forensic-fiber-comparison-microscope/i18n/pt.ts +250 -0
  115. package/src/tool/forensic-fiber-comparison-microscope/i18n/ru.ts +250 -0
  116. package/src/tool/forensic-fiber-comparison-microscope/i18n/sv.ts +250 -0
  117. package/src/tool/forensic-fiber-comparison-microscope/i18n/tr.ts +246 -0
  118. package/src/tool/forensic-fiber-comparison-microscope/i18n/zh.ts +250 -0
  119. package/src/tool/forensic-fiber-comparison-microscope/index.ts +11 -0
  120. package/src/tool/forensic-fiber-comparison-microscope/logic.ts +244 -0
  121. package/src/tool/forensic-fiber-comparison-microscope/render.ts +265 -0
  122. package/src/tool/forensic-fiber-comparison-microscope/seo.astro +15 -0
  123. package/src/tool/forensic-fiber-comparison-microscope/view.ts +267 -0
  124. package/src/tool/forensic-glass-becke-line-simulator/bibliography.astro +6 -0
  125. package/src/tool/forensic-glass-becke-line-simulator/bibliography.ts +16 -0
  126. package/src/tool/forensic-glass-becke-line-simulator/component.astro +81 -0
  127. package/src/tool/forensic-glass-becke-line-simulator/entry.ts +32 -0
  128. package/src/tool/forensic-glass-becke-line-simulator/forensic-glass-becke-line-simulator.css +392 -0
  129. package/src/tool/forensic-glass-becke-line-simulator/i18n/de.ts +231 -0
  130. package/src/tool/forensic-glass-becke-line-simulator/i18n/en.ts +231 -0
  131. package/src/tool/forensic-glass-becke-line-simulator/i18n/es.ts +231 -0
  132. package/src/tool/forensic-glass-becke-line-simulator/i18n/fr.ts +231 -0
  133. package/src/tool/forensic-glass-becke-line-simulator/i18n/id.ts +231 -0
  134. package/src/tool/forensic-glass-becke-line-simulator/i18n/it.ts +231 -0
  135. package/src/tool/forensic-glass-becke-line-simulator/i18n/ja.ts +231 -0
  136. package/src/tool/forensic-glass-becke-line-simulator/i18n/ko.ts +231 -0
  137. package/src/tool/forensic-glass-becke-line-simulator/i18n/nl.ts +231 -0
  138. package/src/tool/forensic-glass-becke-line-simulator/i18n/pl.ts +231 -0
  139. package/src/tool/forensic-glass-becke-line-simulator/i18n/pt.ts +231 -0
  140. package/src/tool/forensic-glass-becke-line-simulator/i18n/ru.ts +231 -0
  141. package/src/tool/forensic-glass-becke-line-simulator/i18n/sv.ts +231 -0
  142. package/src/tool/forensic-glass-becke-line-simulator/i18n/tr.ts +231 -0
  143. package/src/tool/forensic-glass-becke-line-simulator/i18n/zh.ts +231 -0
  144. package/src/tool/forensic-glass-becke-line-simulator/index.ts +11 -0
  145. package/src/tool/forensic-glass-becke-line-simulator/logic.ts +100 -0
  146. package/src/tool/forensic-glass-becke-line-simulator/seo.astro +15 -0
  147. package/src/tool/forensic-glass-becke-line-simulator/view.ts +281 -0
  148. package/src/tool/forensic-image-authenticity-analyzer/bibliography.astro +9 -0
  149. package/src/tool/forensic-image-authenticity-analyzer/bibliography.ts +7 -0
  150. package/src/tool/forensic-image-authenticity-analyzer/component.astro +250 -0
  151. package/src/tool/forensic-image-authenticity-analyzer/entry.ts +29 -0
  152. package/src/tool/forensic-image-authenticity-analyzer/forensic-image-metadata-authenticity-analyzer.css +679 -0
  153. package/src/tool/forensic-image-authenticity-analyzer/i18n/de.ts +105 -0
  154. package/src/tool/forensic-image-authenticity-analyzer/i18n/en.ts +105 -0
  155. package/src/tool/forensic-image-authenticity-analyzer/i18n/es.ts +105 -0
  156. package/src/tool/forensic-image-authenticity-analyzer/i18n/fr.ts +105 -0
  157. package/src/tool/forensic-image-authenticity-analyzer/i18n/id.ts +76 -0
  158. package/src/tool/forensic-image-authenticity-analyzer/i18n/it.ts +105 -0
  159. package/src/tool/forensic-image-authenticity-analyzer/i18n/ja.ts +72 -0
  160. package/src/tool/forensic-image-authenticity-analyzer/i18n/ko.ts +72 -0
  161. package/src/tool/forensic-image-authenticity-analyzer/i18n/nl.ts +75 -0
  162. package/src/tool/forensic-image-authenticity-analyzer/i18n/pl.ts +75 -0
  163. package/src/tool/forensic-image-authenticity-analyzer/i18n/pt.ts +105 -0
  164. package/src/tool/forensic-image-authenticity-analyzer/i18n/ru.ts +75 -0
  165. package/src/tool/forensic-image-authenticity-analyzer/i18n/sv.ts +76 -0
  166. package/src/tool/forensic-image-authenticity-analyzer/i18n/tr.ts +76 -0
  167. package/src/tool/forensic-image-authenticity-analyzer/i18n/zh.ts +71 -0
  168. package/src/tool/forensic-image-authenticity-analyzer/index.ts +11 -0
  169. package/src/tool/forensic-image-authenticity-analyzer/logic.ts +283 -0
  170. package/src/tool/forensic-image-authenticity-analyzer/seo.astro +10 -0
  171. package/src/tool/forensic-microcrystal-drug-simulator/bibliography.astro +6 -0
  172. package/src/tool/forensic-microcrystal-drug-simulator/bibliography.ts +12 -0
  173. package/src/tool/forensic-microcrystal-drug-simulator/component.astro +240 -0
  174. package/src/tool/forensic-microcrystal-drug-simulator/entry.ts +32 -0
  175. package/src/tool/forensic-microcrystal-drug-simulator/forensic-microcrystal-drug-simulator.css +430 -0
  176. package/src/tool/forensic-microcrystal-drug-simulator/i18n/de.ts +244 -0
  177. package/src/tool/forensic-microcrystal-drug-simulator/i18n/en.ts +244 -0
  178. package/src/tool/forensic-microcrystal-drug-simulator/i18n/es.ts +244 -0
  179. package/src/tool/forensic-microcrystal-drug-simulator/i18n/fr.ts +244 -0
  180. package/src/tool/forensic-microcrystal-drug-simulator/i18n/id.ts +244 -0
  181. package/src/tool/forensic-microcrystal-drug-simulator/i18n/it.ts +244 -0
  182. package/src/tool/forensic-microcrystal-drug-simulator/i18n/ja.ts +244 -0
  183. package/src/tool/forensic-microcrystal-drug-simulator/i18n/ko.ts +244 -0
  184. package/src/tool/forensic-microcrystal-drug-simulator/i18n/nl.ts +244 -0
  185. package/src/tool/forensic-microcrystal-drug-simulator/i18n/pl.ts +244 -0
  186. package/src/tool/forensic-microcrystal-drug-simulator/i18n/pt.ts +244 -0
  187. package/src/tool/forensic-microcrystal-drug-simulator/i18n/ru.ts +244 -0
  188. package/src/tool/forensic-microcrystal-drug-simulator/i18n/sv.ts +244 -0
  189. package/src/tool/forensic-microcrystal-drug-simulator/i18n/tr.ts +244 -0
  190. package/src/tool/forensic-microcrystal-drug-simulator/i18n/zh.ts +244 -0
  191. package/src/tool/forensic-microcrystal-drug-simulator/index.ts +11 -0
  192. package/src/tool/forensic-microcrystal-drug-simulator/logic.ts +189 -0
  193. package/src/tool/forensic-microcrystal-drug-simulator/seo.astro +15 -0
  194. package/src/tool/forensic-sex-determinator/bibliography.astro +14 -0
  195. package/src/tool/forensic-sex-determinator/bibliography.ts +12 -0
  196. package/src/tool/forensic-sex-determinator/component.astro +463 -0
  197. package/src/tool/forensic-sex-determinator/entry.ts +32 -0
  198. package/src/tool/forensic-sex-determinator/forensic-sex-determinator.css +413 -0
  199. package/src/tool/forensic-sex-determinator/i18n/de.ts +211 -0
  200. package/src/tool/forensic-sex-determinator/i18n/en.ts +211 -0
  201. package/src/tool/forensic-sex-determinator/i18n/es.ts +211 -0
  202. package/src/tool/forensic-sex-determinator/i18n/fr.ts +211 -0
  203. package/src/tool/forensic-sex-determinator/i18n/id.ts +211 -0
  204. package/src/tool/forensic-sex-determinator/i18n/it.ts +211 -0
  205. package/src/tool/forensic-sex-determinator/i18n/ja.ts +211 -0
  206. package/src/tool/forensic-sex-determinator/i18n/ko.ts +211 -0
  207. package/src/tool/forensic-sex-determinator/i18n/nl.ts +211 -0
  208. package/src/tool/forensic-sex-determinator/i18n/pl.ts +211 -0
  209. package/src/tool/forensic-sex-determinator/i18n/pt.ts +211 -0
  210. package/src/tool/forensic-sex-determinator/i18n/ru.ts +211 -0
  211. package/src/tool/forensic-sex-determinator/i18n/sv.ts +211 -0
  212. package/src/tool/forensic-sex-determinator/i18n/tr.ts +211 -0
  213. package/src/tool/forensic-sex-determinator/i18n/zh.ts +211 -0
  214. package/src/tool/forensic-sex-determinator/index.ts +11 -0
  215. package/src/tool/forensic-sex-determinator/logic.ts +89 -0
  216. package/src/tool/forensic-sex-determinator/seo.astro +15 -0
  217. package/src/tool/forensic-stature-estimator/bibliography.astro +14 -0
  218. package/src/tool/forensic-stature-estimator/bibliography.ts +12 -0
  219. package/src/tool/forensic-stature-estimator/component.astro +49 -0
  220. package/src/tool/forensic-stature-estimator/components/EstimationPanel.astro +65 -0
  221. package/src/tool/forensic-stature-estimator/components/OsteometricBoard.astro +39 -0
  222. package/src/tool/forensic-stature-estimator/components/OsteometricSelector.astro +109 -0
  223. package/src/tool/forensic-stature-estimator/dom-utils.ts +71 -0
  224. package/src/tool/forensic-stature-estimator/entry.ts +32 -0
  225. package/src/tool/forensic-stature-estimator/forensic-stature-estimator.css +689 -0
  226. package/src/tool/forensic-stature-estimator/helpers.ts +51 -0
  227. package/src/tool/forensic-stature-estimator/i18n/de.ts +196 -0
  228. package/src/tool/forensic-stature-estimator/i18n/en.ts +196 -0
  229. package/src/tool/forensic-stature-estimator/i18n/es.ts +196 -0
  230. package/src/tool/forensic-stature-estimator/i18n/fr.ts +196 -0
  231. package/src/tool/forensic-stature-estimator/i18n/id.ts +196 -0
  232. package/src/tool/forensic-stature-estimator/i18n/it.ts +196 -0
  233. package/src/tool/forensic-stature-estimator/i18n/ja.ts +196 -0
  234. package/src/tool/forensic-stature-estimator/i18n/ko.ts +196 -0
  235. package/src/tool/forensic-stature-estimator/i18n/nl.ts +196 -0
  236. package/src/tool/forensic-stature-estimator/i18n/pl.ts +196 -0
  237. package/src/tool/forensic-stature-estimator/i18n/pt.ts +196 -0
  238. package/src/tool/forensic-stature-estimator/i18n/ru.ts +196 -0
  239. package/src/tool/forensic-stature-estimator/i18n/sv.ts +196 -0
  240. package/src/tool/forensic-stature-estimator/i18n/tr.ts +196 -0
  241. package/src/tool/forensic-stature-estimator/i18n/zh.ts +196 -0
  242. package/src/tool/forensic-stature-estimator/index.ts +11 -0
  243. package/src/tool/forensic-stature-estimator/logic.ts +119 -0
  244. package/src/tool/forensic-stature-estimator/script.ts +288 -0
  245. package/src/tool/forensic-stature-estimator/seo.astro +15 -0
  246. package/src/tool/forensic-tlc-ink-simulator/bibliography.astro +6 -0
  247. package/src/tool/forensic-tlc-ink-simulator/bibliography.ts +16 -0
  248. package/src/tool/forensic-tlc-ink-simulator/component.astro +245 -0
  249. package/src/tool/forensic-tlc-ink-simulator/entry.ts +32 -0
  250. package/src/tool/forensic-tlc-ink-simulator/forensic-tlc-ink-simulator.css +462 -0
  251. package/src/tool/forensic-tlc-ink-simulator/i18n/de.ts +243 -0
  252. package/src/tool/forensic-tlc-ink-simulator/i18n/en.ts +243 -0
  253. package/src/tool/forensic-tlc-ink-simulator/i18n/es.ts +243 -0
  254. package/src/tool/forensic-tlc-ink-simulator/i18n/fr.ts +243 -0
  255. package/src/tool/forensic-tlc-ink-simulator/i18n/id.ts +243 -0
  256. package/src/tool/forensic-tlc-ink-simulator/i18n/it.ts +243 -0
  257. package/src/tool/forensic-tlc-ink-simulator/i18n/ja.ts +235 -0
  258. package/src/tool/forensic-tlc-ink-simulator/i18n/ko.ts +235 -0
  259. package/src/tool/forensic-tlc-ink-simulator/i18n/nl.ts +243 -0
  260. package/src/tool/forensic-tlc-ink-simulator/i18n/pl.ts +243 -0
  261. package/src/tool/forensic-tlc-ink-simulator/i18n/pt.ts +243 -0
  262. package/src/tool/forensic-tlc-ink-simulator/i18n/ru.ts +243 -0
  263. package/src/tool/forensic-tlc-ink-simulator/i18n/sv.ts +243 -0
  264. package/src/tool/forensic-tlc-ink-simulator/i18n/tr.ts +243 -0
  265. package/src/tool/forensic-tlc-ink-simulator/i18n/zh.ts +235 -0
  266. package/src/tool/forensic-tlc-ink-simulator/index.ts +11 -0
  267. package/src/tool/forensic-tlc-ink-simulator/logic.ts +152 -0
  268. package/src/tool/forensic-tlc-ink-simulator/seo.astro +15 -0
  269. package/src/tool/gsr-dispersion-calculator/bibliography.astro +6 -0
  270. package/src/tool/gsr-dispersion-calculator/bibliography.ts +16 -0
  271. package/src/tool/gsr-dispersion-calculator/component.astro +294 -0
  272. package/src/tool/gsr-dispersion-calculator/entry.ts +32 -0
  273. package/src/tool/gsr-dispersion-calculator/gsr-dispersion-calculator.css +305 -0
  274. package/src/tool/gsr-dispersion-calculator/i18n/de.ts +272 -0
  275. package/src/tool/gsr-dispersion-calculator/i18n/en.ts +272 -0
  276. package/src/tool/gsr-dispersion-calculator/i18n/es.ts +272 -0
  277. package/src/tool/gsr-dispersion-calculator/i18n/fr.ts +272 -0
  278. package/src/tool/gsr-dispersion-calculator/i18n/id.ts +272 -0
  279. package/src/tool/gsr-dispersion-calculator/i18n/it.ts +272 -0
  280. package/src/tool/gsr-dispersion-calculator/i18n/ja.ts +272 -0
  281. package/src/tool/gsr-dispersion-calculator/i18n/ko.ts +272 -0
  282. package/src/tool/gsr-dispersion-calculator/i18n/nl.ts +272 -0
  283. package/src/tool/gsr-dispersion-calculator/i18n/pl.ts +272 -0
  284. package/src/tool/gsr-dispersion-calculator/i18n/pt.ts +272 -0
  285. package/src/tool/gsr-dispersion-calculator/i18n/ru.ts +272 -0
  286. package/src/tool/gsr-dispersion-calculator/i18n/sv.ts +272 -0
  287. package/src/tool/gsr-dispersion-calculator/i18n/tr.ts +272 -0
  288. package/src/tool/gsr-dispersion-calculator/i18n/zh.ts +272 -0
  289. package/src/tool/gsr-dispersion-calculator/index.ts +11 -0
  290. package/src/tool/gsr-dispersion-calculator/logic.ts +148 -0
  291. package/src/tool/gsr-dispersion-calculator/seo.astro +15 -0
  292. package/src/tool/widmark-alcohol-simulator/bibliography.astro +14 -0
  293. package/src/tool/widmark-alcohol-simulator/bibliography.ts +12 -0
  294. package/src/tool/widmark-alcohol-simulator/component.astro +453 -0
  295. package/src/tool/widmark-alcohol-simulator/entry.ts +32 -0
  296. package/src/tool/widmark-alcohol-simulator/i18n/de.ts +193 -0
  297. package/src/tool/widmark-alcohol-simulator/i18n/en.ts +206 -0
  298. package/src/tool/widmark-alcohol-simulator/i18n/es.ts +193 -0
  299. package/src/tool/widmark-alcohol-simulator/i18n/fr.ts +193 -0
  300. package/src/tool/widmark-alcohol-simulator/i18n/id.ts +193 -0
  301. package/src/tool/widmark-alcohol-simulator/i18n/it.ts +193 -0
  302. package/src/tool/widmark-alcohol-simulator/i18n/ja.ts +193 -0
  303. package/src/tool/widmark-alcohol-simulator/i18n/ko.ts +193 -0
  304. package/src/tool/widmark-alcohol-simulator/i18n/nl.ts +193 -0
  305. package/src/tool/widmark-alcohol-simulator/i18n/pl.ts +193 -0
  306. package/src/tool/widmark-alcohol-simulator/i18n/pt.ts +193 -0
  307. package/src/tool/widmark-alcohol-simulator/i18n/ru.ts +193 -0
  308. package/src/tool/widmark-alcohol-simulator/i18n/sv.ts +193 -0
  309. package/src/tool/widmark-alcohol-simulator/i18n/tr.ts +193 -0
  310. package/src/tool/widmark-alcohol-simulator/i18n/zh.ts +193 -0
  311. package/src/tool/widmark-alcohol-simulator/index.ts +11 -0
  312. package/src/tool/widmark-alcohol-simulator/logic.ts +97 -0
  313. package/src/tool/widmark-alcohol-simulator/seo.astro +15 -0
  314. package/src/tool/widmark-alcohol-simulator/widmark-alcohol-simulator.css +386 -0
  315. package/src/tools.ts +27 -0
  316. package/src/types.ts +70 -0
@@ -0,0 +1,265 @@
1
+ import type { FiberProfile, SpectrumPoint } from './logic';
2
+
3
+ export interface RenderTheme {
4
+ field: string;
5
+ fiberA: string;
6
+ fiberB: string;
7
+ glow: string;
8
+ text: string;
9
+ grid: string;
10
+ }
11
+
12
+ interface ScopeMetrics {
13
+ width: number;
14
+ height: number;
15
+ radius: number;
16
+ blur: number;
17
+ }
18
+
19
+ interface HalfRenderConfig {
20
+ context: CanvasRenderingContext2D;
21
+ profile: FiberProfile;
22
+ theme: RenderTheme;
23
+ metrics: ScopeMetrics;
24
+ xOffset: number;
25
+ angle: number;
26
+ leftSide: boolean;
27
+ }
28
+
29
+ interface TextureStyle {
30
+ alpha: number;
31
+ stroke: string;
32
+ step: number;
33
+ }
34
+
35
+ interface MicroscopeScene {
36
+ context: CanvasRenderingContext2D;
37
+ canvas: HTMLCanvasElement;
38
+ left: FiberProfile;
39
+ right: FiberProfile;
40
+ focus: number;
41
+ polarizationDegrees: number;
42
+ }
43
+
44
+ interface TextureConfig {
45
+ context: CanvasRenderingContext2D;
46
+ width: number;
47
+ y: number;
48
+ profile: FiberProfile;
49
+ texture: TextureStyle;
50
+ }
51
+
52
+ const DYE_COLORS: Record<string, string> = {
53
+ 'indigo reactive blue': '#1d5f84',
54
+ 'acid crimson': '#b91c4a',
55
+ 'disperse navy': '#243c8f',
56
+ 'acid violet': '#8b5cf6',
57
+ };
58
+
59
+ export function currentTheme(): RenderTheme {
60
+ const dark = document.documentElement.classList.contains('theme-dark') || document.body.classList.contains('theme-dark');
61
+ if (dark) return { field: '#10171c', fiberA: '#7dd3fc', fiberB: '#c4b5fd', glow: '#facc15', text: '#f8fafc', grid: 'rgba(255,255,255,0.12)' };
62
+ return { field: '#f3f7ed', fiberA: '#1d5f84', fiberB: '#7c3aed', glow: '#b45309', text: '#17212b', grid: 'rgba(15,23,42,0.14)' };
63
+ }
64
+
65
+ export function dyeColor(profile: FiberProfile, fallback: string): string {
66
+ return DYE_COLORS[profile.dyeFamily] ?? fallback;
67
+ }
68
+
69
+ export function drawMicroscopeScene(scene: MicroscopeScene): void {
70
+ const { context, canvas, left, right, focus, polarizationDegrees } = scene;
71
+ const theme = currentTheme();
72
+ const metrics = scopeMetrics(canvas, focus);
73
+
74
+ context.clearRect(0, 0, metrics.width, metrics.height);
75
+ context.fillStyle = theme.field;
76
+ context.fillRect(0, 0, metrics.width, metrics.height);
77
+ context.save();
78
+ context.beginPath();
79
+ context.arc(metrics.width / 2, metrics.height / 2, metrics.radius, 0, Math.PI * 2);
80
+ context.clip();
81
+ drawHalf({ context, profile: left, theme, metrics, xOffset: 0, angle: polarizationDegrees, leftSide: true });
82
+ drawHalf({ context, profile: right, theme, metrics, xOffset: metrics.width / 2, angle: polarizationDegrees, leftSide: false });
83
+ drawReticle(context, metrics, theme);
84
+ context.restore();
85
+ context.lineWidth = 18;
86
+ context.strokeStyle = 'rgba(12,18,24,0.72)';
87
+ context.beginPath();
88
+ context.arc(metrics.width / 2, metrics.height / 2, metrics.radius, 0, Math.PI * 2);
89
+ context.stroke();
90
+ }
91
+
92
+ function scopeMetrics(canvas: HTMLCanvasElement, focus: number): ScopeMetrics {
93
+ return {
94
+ width: canvas.width,
95
+ height: canvas.height,
96
+ radius: Math.min(canvas.width, canvas.height) * 0.43,
97
+ blur: Math.abs(focus - 62) / 8,
98
+ };
99
+ }
100
+
101
+ function drawHalf(config: HalfRenderConfig): void {
102
+ const { context, profile, theme, metrics, xOffset, angle, leftSide } = config;
103
+ const brightness = 0.38 + Math.pow(Math.sin((angle * Math.PI) / 180), 2) * profile.birefringence * 9;
104
+ const fiberColor = dyeColor(profile, leftSide ? theme.fiberA : theme.fiberB);
105
+ const wave = fiberWave(profile.material);
106
+ const texture = textureStyle(profile);
107
+ const columnWidth = metrics.width / 2;
108
+
109
+ context.save();
110
+ context.translate(xOffset, 0);
111
+ context.fillStyle = `${fiberColor}18`;
112
+ context.fillRect(0, 0, columnWidth, metrics.height);
113
+ context.filter = `blur(${metrics.blur.toFixed(1)}px)`;
114
+
115
+ for (let i = 0; i < 7; i++) {
116
+ drawFiberStroke({ context, profile, fiberColor, theme, texture, wave, brightness, y: fiberY(i, leftSide), width: columnWidth });
117
+ }
118
+
119
+ context.restore();
120
+ context.globalAlpha = 1;
121
+ context.filter = 'none';
122
+ context.shadowBlur = 0;
123
+ }
124
+
125
+ function fiberWave(material: FiberProfile['material']): number {
126
+ if (material === 'wool') return 74;
127
+ if (material === 'cotton') return 48;
128
+ return 30;
129
+ }
130
+
131
+ function textureStyle(profile: FiberProfile): TextureStyle {
132
+ if (profile.material === 'wool') return { alpha: 0.38, stroke: 'rgba(255,255,255,0.62)', step: 22 };
133
+ if (profile.material === 'cotton') return { alpha: 0.28, stroke: 'rgba(255,255,255,0.62)', step: 30 };
134
+ return { alpha: 0.2, stroke: 'rgba(255,255,255,0.75)', step: 42 };
135
+ }
136
+
137
+ function fiberY(index: number, leftSide: boolean): number {
138
+ return 95 + index * 58 + (leftSide ? index % 2 : (index + 1) % 2) * 18;
139
+ }
140
+
141
+ function drawFiberStroke(config: {
142
+ context: CanvasRenderingContext2D;
143
+ profile: FiberProfile;
144
+ fiberColor: string;
145
+ theme: RenderTheme;
146
+ texture: TextureStyle;
147
+ wave: number;
148
+ brightness: number;
149
+ y: number;
150
+ width: number;
151
+ }): void {
152
+ const { context, profile, fiberColor, theme, texture, wave, brightness, y, width } = config;
153
+ context.beginPath();
154
+ context.moveTo(-40, y);
155
+ context.bezierCurveTo(width * 0.24, y - wave, width * 0.52, y + wave, width + 45, y + (profile.twist === 'S' ? -28 : 28));
156
+ context.lineWidth = Math.max(4, profile.diameterMicrons / 2.7);
157
+ context.strokeStyle = fiberColor;
158
+ context.globalAlpha = Math.min(0.95, brightness + 0.035);
159
+ context.shadowColor = theme.glow;
160
+ context.shadowBlur = profile.natural ? 2 : 10 + brightness * 14;
161
+ context.stroke();
162
+ drawFiberTexture({ context, width, y, profile, texture });
163
+ }
164
+
165
+ function drawFiberTexture(config: TextureConfig): void {
166
+ const { context, width, y, profile, texture } = config;
167
+ context.save();
168
+ context.globalAlpha = texture.alpha;
169
+ context.strokeStyle = texture.stroke;
170
+ context.lineWidth = 1;
171
+
172
+ for (let x = 10; x < width; x += texture.step) {
173
+ context.beginPath();
174
+ if (profile.material === 'wool') {
175
+ context.arc(x, y, 8, -0.9, 0.9);
176
+ } else if (profile.material === 'cotton') {
177
+ context.moveTo(x, y - 16);
178
+ context.lineTo(x + (profile.twist === 'S' ? -12 : 12), y + 16);
179
+ } else {
180
+ context.moveTo(x, y - 10);
181
+ context.lineTo(x + (profile.twist === 'S' ? -18 : 18), y + 10);
182
+ }
183
+ context.stroke();
184
+ }
185
+
186
+ context.restore();
187
+ }
188
+
189
+ function drawReticle(context: CanvasRenderingContext2D, metrics: ScopeMetrics, theme: RenderTheme): void {
190
+ context.strokeStyle = theme.grid;
191
+ context.lineWidth = 1;
192
+ context.beginPath();
193
+ context.moveTo(metrics.width / 2, metrics.height / 2 - metrics.radius);
194
+ context.lineTo(metrics.width / 2, metrics.height / 2 + metrics.radius);
195
+ context.moveTo(metrics.width / 2 - metrics.radius, metrics.height / 2);
196
+ context.lineTo(metrics.width / 2 + metrics.radius, metrics.height / 2);
197
+ context.stroke();
198
+ }
199
+
200
+ export function buildSpectrumChartMarkup(ui: Record<string, string>, left: FiberProfile, right: FiberProfile): string {
201
+ const theme = currentTheme();
202
+ const leftColor = dyeColor(left, theme.fiberA);
203
+ const rightColor = dyeColor(right, theme.fiberB);
204
+ const leftPath = pathFor(left.spectrum);
205
+ const rightPath = pathFor(right.spectrum);
206
+ const leftArea = areaFor(left.spectrum);
207
+ const rightArea = areaFor(right.spectrum);
208
+ const gridLines = [60, 110, 160, 210]
209
+ .map((lineY) => `<line x1="54" y1="${lineY}" x2="688" y2="${lineY}"></line>`)
210
+ .join('');
211
+
212
+ return `${chartDefs(leftColor, rightColor)}
213
+ <rect x="0" y="0" width="720" height="270" rx="8" fill="transparent"></rect>
214
+ <g stroke="${theme.grid}" stroke-width="1">${gridLines}</g>
215
+ ${chartLabels(ui.uvVisTitle, theme.text)}
216
+ <path d="${leftArea}" fill="url(#fiber-left-area)"></path>
217
+ <path d="${rightArea}" fill="url(#fiber-right-area)"></path>
218
+ <path d="${leftPath}" fill="none" stroke="${leftColor}" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"></path>
219
+ <path d="${rightPath}" fill="none" stroke="${rightColor}" stroke-width="5" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="11 9"></path>
220
+ `;
221
+ }
222
+
223
+ function chartDefs(leftColor: string, rightColor: string): string {
224
+ return `
225
+ <defs>
226
+ <linearGradient id="fiber-left-area" x1="0" y1="44" x2="0" y2="222" gradientUnits="userSpaceOnUse">
227
+ <stop offset="0" stop-color="${leftColor}" stop-opacity="0.22"></stop>
228
+ <stop offset="0.62" stop-color="${leftColor}" stop-opacity="0.07"></stop>
229
+ <stop offset="1" stop-color="${leftColor}" stop-opacity="0"></stop>
230
+ </linearGradient>
231
+ <linearGradient id="fiber-right-area" x1="0" y1="44" x2="0" y2="222" gradientUnits="userSpaceOnUse">
232
+ <stop offset="0" stop-color="${rightColor}" stop-opacity="0.18"></stop>
233
+ <stop offset="0.62" stop-color="${rightColor}" stop-opacity="0.06"></stop>
234
+ <stop offset="1" stop-color="${rightColor}" stop-opacity="0"></stop>
235
+ </linearGradient>
236
+ </defs>
237
+ `;
238
+ }
239
+
240
+ function chartLabels(title: string, textColor: string): string {
241
+ return `
242
+ <text x="54" y="34" fill="${textColor}" font-size="19" font-weight="850">${title}</text>
243
+ <text x="54" y="246" fill="${textColor}" font-size="12" font-family="JetBrains Mono, ui-monospace, monospace">380 nm</text>
244
+ <text x="638" y="246" fill="${textColor}" font-size="12" font-family="JetBrains Mono, ui-monospace, monospace">720 nm</text>
245
+ `;
246
+ }
247
+
248
+ function pathFor(points: SpectrumPoint[]): string {
249
+ return points
250
+ .map((point, index) => {
251
+ const x = 54 + ((point.wavelength - 380) / 340) * 634;
252
+ const y = 222 - point.absorbance * 178;
253
+ return `${index === 0 ? 'M' : 'L'} ${x.toFixed(1)} ${y.toFixed(1)}`;
254
+ })
255
+ .join(' ');
256
+ }
257
+
258
+ function areaFor(points: SpectrumPoint[]): string {
259
+ const first = points[0];
260
+ const last = points[points.length - 1];
261
+ if (!first || !last) return '';
262
+ const firstX = 54 + ((first.wavelength - 380) / 340) * 634;
263
+ const lastX = 54 + ((last.wavelength - 380) / 340) * 634;
264
+ return `${pathFor(points)} L ${lastX.toFixed(1)} 222 L ${firstX.toFixed(1)} 222 Z`;
265
+ }
@@ -0,0 +1,15 @@
1
+ ---
2
+ import { SEORenderer } from '@jjlmoya/utils-shared';
3
+ import { forensicFiberComparisonMicroscope } from './index';
4
+ import type { KnownLocale } from '../../types';
5
+
6
+ interface Props {
7
+ locale?: KnownLocale;
8
+ }
9
+
10
+ const { locale = 'en' } = Astro.props;
11
+ const content = await forensicFiberComparisonMicroscope.i18n[locale]?.();
12
+ if (!content) return null;
13
+ ---
14
+
15
+ {content.seo && content.seo.length > 0 && <SEORenderer content={{ locale, sections: content.seo }} />}
@@ -0,0 +1,267 @@
1
+ import { FiberComparisonMicroscope } from './logic';
2
+ import { buildSpectrumChartMarkup, drawMicroscopeScene } from './render';
3
+ import type { DyePreset, FiberMaterial, FiberReference, FiberTwist, QuestionedFiberInput } from './logic';
4
+
5
+ type Ui = Record<string, string>;
6
+ type Result = ReturnType<FiberComparisonMicroscope['compare']>;
7
+
8
+ const STORAGE_KEY = 'forensic-fiber-comparison-microscope-state';
9
+ const FIBER_VALUES: FiberReference[] = ['questionedCotton', 'suspectCotton', 'wool', 'polyester', 'nylon'];
10
+ const MATERIAL_VALUES: FiberMaterial[] = ['cotton', 'wool', 'polyester', 'nylon'];
11
+ const TWIST_VALUES: FiberTwist[] = ['S', 'Z', 'irregular'];
12
+ const DYE_VALUES: DyePreset[] = ['indigo', 'crimson', 'navy', 'violet'];
13
+ const DEFAULT_BUILDER: QuestionedFiberInput = { material: 'cotton', diameterMicrons: 18, twist: 'irregular', birefringence: 0.032, dyePreset: 'indigo' };
14
+
15
+ function byId<T extends HTMLElement>(id: string): T | null {
16
+ return document.getElementById(id) as T | null;
17
+ }
18
+
19
+ function isFiberReference(value: unknown): value is FiberReference {
20
+ return typeof value === 'string' && FIBER_VALUES.includes(value as FiberReference);
21
+ }
22
+
23
+ function normalizeDegrees(value: number): number {
24
+ return Math.max(0, Math.min(180, value));
25
+ }
26
+
27
+ export function initFiberComparisonMicroscope(ui: Ui): void {
28
+ new FiberComparisonView(ui).init();
29
+ }
30
+
31
+ class FiberComparisonView {
32
+ private readonly model = new FiberComparisonMicroscope();
33
+ private readonly canvas = byId<HTMLCanvasElement>('fiber-canvas');
34
+ private readonly context = this.canvas?.getContext('2d') ?? null;
35
+ private readonly chart = byId<SVGSVGElement>('fiber-spectrum-chart');
36
+ private readonly rightSelect = byId<HTMLSelectElement>('fiber-right');
37
+ private readonly focusInput = byId<HTMLInputElement>('fiber-focus');
38
+ private readonly polarizerInput = byId<HTMLInputElement>('fiber-polarizer');
39
+ private readonly diameterInput = byId<HTMLInputElement>('fiber-diameter');
40
+ private readonly birefringenceInput = byId<HTMLInputElement>('fiber-questioned-birefringence');
41
+ private builder: QuestionedFiberInput = { ...DEFAULT_BUILDER };
42
+
43
+ constructor(private readonly ui: Ui) {}
44
+
45
+ init(): void {
46
+ this.restoreState();
47
+ this.bindEvents();
48
+ this.render();
49
+ new MutationObserver(() => this.render()).observe(document.documentElement, { attributes: true, attributeFilter: ['class'] });
50
+ }
51
+
52
+ private bindEvents(): void {
53
+ this.bindInputs();
54
+ this.bindChoiceMenu();
55
+ this.bindBuilderButtons();
56
+ document.addEventListener('click', (event) => this.closeMenusFromOutside(event.target as Node));
57
+ document.addEventListener('keydown', (event) => { if (event.key === 'Escape') this.closeChoiceMenu(); });
58
+ }
59
+
60
+ private bindInputs(): void {
61
+ const inputs = [this.rightSelect, this.focusInput, this.polarizerInput, this.diameterInput, this.birefringenceInput];
62
+ inputs.forEach((input) => {
63
+ input?.addEventListener('input', () => this.render());
64
+ input?.addEventListener('change', () => this.render());
65
+ });
66
+ }
67
+
68
+ private bindChoiceMenu(): void {
69
+ document.querySelectorAll<HTMLButtonElement>('[data-fiber-choice-trigger]').forEach((button) => {
70
+ button.addEventListener('click', () => this.toggleChoiceMenu(button.getAttribute('aria-expanded') !== 'true'));
71
+ });
72
+ document.querySelectorAll<HTMLButtonElement>('[data-fiber-choice]').forEach((button) => {
73
+ button.addEventListener('click', () => this.chooseFiber(button));
74
+ });
75
+ }
76
+
77
+ private bindBuilderButtons(): void {
78
+ document.querySelectorAll<HTMLButtonElement>('[data-fiber-builder]').forEach((button) => {
79
+ button.addEventListener('click', () => {
80
+ this.applyBuilderSelection(button.dataset.fiberBuilder, button.dataset.builderValue);
81
+ this.render();
82
+ });
83
+ });
84
+ }
85
+
86
+ private restoreState(): void {
87
+ try {
88
+ const saved = JSON.parse(localStorage.getItem(STORAGE_KEY) ?? '{}') as Record<string, unknown>;
89
+ this.restoreChoiceState(saved);
90
+ this.restoreBuilderState(saved.builder);
91
+ this.syncBuilderInputs();
92
+ } catch {
93
+ localStorage.removeItem(STORAGE_KEY);
94
+ }
95
+ }
96
+
97
+ private restoreChoiceState(saved: Record<string, unknown>): void {
98
+ if (isFiberReference(saved.right) && this.rightSelect) this.rightSelect.value = saved.right;
99
+ if (typeof saved.focus === 'number' && this.focusInput) this.focusInput.value = String(saved.focus);
100
+ if (typeof saved.polarizer === 'number' && this.polarizerInput) this.polarizerInput.value = String(saved.polarizer);
101
+ }
102
+
103
+ private restoreBuilderState(value: unknown): void {
104
+ if (this.isBuilderState(value)) this.builder = value;
105
+ }
106
+
107
+ private syncBuilderInputs(): void {
108
+ if (this.diameterInput) this.diameterInput.value = String(this.builder.diameterMicrons);
109
+ if (this.birefringenceInput) this.birefringenceInput.value = String(Math.round(this.builder.birefringence * 1000));
110
+ }
111
+
112
+ private isBuilderState(value: unknown): value is QuestionedFiberInput {
113
+ if (!value || typeof value !== 'object') return false;
114
+ const candidate = value as QuestionedFiberInput;
115
+ return MATERIAL_VALUES.includes(candidate.material)
116
+ && TWIST_VALUES.includes(candidate.twist)
117
+ && DYE_VALUES.includes(candidate.dyePreset)
118
+ && typeof candidate.diameterMicrons === 'number'
119
+ && typeof candidate.birefringence === 'number';
120
+ }
121
+
122
+ private saveState(): void {
123
+ localStorage.setItem(STORAGE_KEY, JSON.stringify({
124
+ right: this.rightSelect?.value,
125
+ focus: Number(this.focusInput?.value ?? 62),
126
+ polarizer: Number(this.polarizerInput?.value ?? 45),
127
+ builder: this.builder,
128
+ }));
129
+ }
130
+
131
+ private toggleChoiceMenu(open: boolean): void {
132
+ document.querySelectorAll<HTMLButtonElement>('[data-fiber-choice-trigger]').forEach((button) => {
133
+ button.setAttribute('aria-expanded', open ? 'true' : 'false');
134
+ });
135
+ document.querySelectorAll<HTMLElement>('[data-fiber-choice-menu]').forEach((menu) => {
136
+ menu.classList.toggle('is-open', open);
137
+ });
138
+ }
139
+
140
+ private closeChoiceMenu(): void {
141
+ this.toggleChoiceMenu(false);
142
+ }
143
+
144
+ private closeMenusFromOutside(target: Node): void {
145
+ const element = target instanceof Element ? target : null;
146
+ if (element?.closest('[data-fiber-choice-trigger], [data-fiber-choice-menu]')) return;
147
+ this.closeChoiceMenu();
148
+ }
149
+
150
+ private chooseFiber(button: HTMLButtonElement): void {
151
+ if (!this.rightSelect || !isFiberReference(button.dataset.fiberValue)) return;
152
+ this.rightSelect.value = button.dataset.fiberValue;
153
+ this.closeChoiceMenu();
154
+ this.render();
155
+ }
156
+
157
+ private applyBuilderSelection(key?: string, value?: string): void {
158
+ if (key === 'material' && MATERIAL_VALUES.includes(value as FiberMaterial)) this.builder.material = value as FiberMaterial;
159
+ if (key === 'twist' && TWIST_VALUES.includes(value as FiberTwist)) this.builder.twist = value as FiberTwist;
160
+ if (key === 'dyePreset' && DYE_VALUES.includes(value as DyePreset)) this.builder.dyePreset = value as DyePreset;
161
+ }
162
+
163
+ private render(): void {
164
+ const right = isFiberReference(this.rightSelect?.value) ? this.rightSelect.value : 'suspectCotton';
165
+ const focus = Number(this.focusInput?.value ?? 62);
166
+ const polarizationDegrees = normalizeDegrees(Number(this.polarizerInput?.value ?? 45));
167
+ this.updateBuilderFromInputs();
168
+
169
+ const leftProfile = this.model.createQuestionedFiber(this.builder);
170
+ const result = this.model.compare({ left: leftProfile, right, focus, polarizationDegrees });
171
+
172
+ this.renderVisuals(result, focus, polarizationDegrees);
173
+ this.renderReadout(result, right, leftProfile);
174
+ this.saveState();
175
+ }
176
+
177
+ private updateBuilderFromInputs(): void {
178
+ this.builder.diameterMicrons = Number(this.diameterInput?.value ?? this.builder.diameterMicrons);
179
+ this.builder.birefringence = Number((Number(this.birefringenceInput?.value ?? 32) / 1000).toFixed(3));
180
+ }
181
+
182
+ private renderVisuals(result: Result, focus: number, polarizationDegrees: number): void {
183
+ if (this.canvas && this.context) {
184
+ drawMicroscopeScene({ context: this.context, canvas: this.canvas, left: result.left, right: result.right, focus, polarizationDegrees });
185
+ }
186
+ if (this.chart) this.chart.innerHTML = buildSpectrumChartMarkup(this.ui, result.left, result.right);
187
+ }
188
+
189
+ private renderReadout(result: Result, right: FiberReference, leftProfile: ReturnType<FiberComparisonMicroscope['createQuestionedFiber']>): void {
190
+ this.setText('fiber-left-label', this.ui.customQuestionedLabel ?? 'Questioned case fiber');
191
+ this.setText('fiber-right-label', this.label(right));
192
+ this.setText('fiber-left-choice', this.ui.customQuestionedLabel ?? 'Questioned case fiber');
193
+ this.setText('fiber-left-summary', this.questionedSummary());
194
+ this.setText('fiber-right-choice', this.label(right));
195
+ this.updateChoiceState(right);
196
+ this.updateBuilderState();
197
+ this.setText('fiber-diameter-label', `${leftProfile.diameterMicrons} \u00b5m`);
198
+ this.setText('fiber-birefringence-label', leftProfile.birefringence.toFixed(3));
199
+ this.setText('fiber-focus-label', `${Math.round(result.visualSharpness)}%`);
200
+ this.setText('fiber-polar-label', `${normalizeDegrees(Number(this.polarizerInput?.value ?? 45))}\u00b0`);
201
+ this.setText('fiber-combined-score', `${result.combinedScore}%`);
202
+ this.setText('fiber-verdict-score', `${result.combinedScore}%`);
203
+ this.setText('fiber-morphology-score', `${result.morphologyScore}%`);
204
+ this.setText('fiber-spectrum-score', `${result.spectrumScore}%`);
205
+ this.setText('fiber-birefringence', result.birefringenceContrast.toFixed(3));
206
+ this.setText('fiber-verdict', this.ui[result.verdictKey] ?? result.verdictKey);
207
+ this.updateVerdict(result);
208
+ }
209
+
210
+ private label(id: FiberReference): string {
211
+ return this.ui[id] ?? id;
212
+ }
213
+
214
+ private questionedSummary(): string {
215
+ const material = this.ui[`material${this.builder.material}`] ?? this.builder.material;
216
+ const dye = this.ui[`dye${this.builder.dyePreset}`] ?? this.builder.dyePreset;
217
+ return `${material} / ${this.builder.diameterMicrons} \u00b5m / ${this.builder.twist} / ${dye}`;
218
+ }
219
+
220
+ private setText(id: string, value: string): void {
221
+ const element = byId(id);
222
+ if (element) element.textContent = value;
223
+ }
224
+
225
+ private updateChoiceState(right: FiberReference): void {
226
+ document.querySelectorAll<HTMLButtonElement>('[data-fiber-choice]').forEach((button) => {
227
+ button.setAttribute('aria-selected', button.dataset.fiberValue === right ? 'true' : 'false');
228
+ });
229
+ }
230
+
231
+ private updateBuilderState(): void {
232
+ document.querySelectorAll<HTMLButtonElement>('[data-fiber-builder]').forEach((button) => {
233
+ const key = button.dataset.fiberBuilder;
234
+ const value = button.dataset.builderValue;
235
+ const active = (key === 'material' && value === this.builder.material)
236
+ || (key === 'twist' && value === this.builder.twist)
237
+ || (key === 'dyePreset' && value === this.builder.dyePreset);
238
+ button.dataset.active = active ? 'true' : 'false';
239
+ });
240
+ }
241
+
242
+ private updateVerdict(result: Result): void {
243
+ const panel = byId<HTMLElement>('fiber-verdict-panel');
244
+ const meter = byId<HTMLElement>('fiber-verdict-meter');
245
+ const statusKey = this.statusKey(result.verdictKey);
246
+ const state = this.verdictState(result.verdictKey);
247
+ this.setText('fiber-verdict-status', this.ui[statusKey] ?? statusKey);
248
+ if (meter) meter.style.width = `${result.combinedScore}%`;
249
+ if (!panel) return;
250
+ panel.dataset.verdict = state;
251
+ panel.classList.remove('is-updating');
252
+ void panel.offsetWidth;
253
+ panel.classList.add('is-updating');
254
+ }
255
+
256
+ private statusKey(verdictKey: Result['verdictKey']): string {
257
+ if (verdictKey === 'verdictStrong') return 'statusStrong';
258
+ if (verdictKey === 'verdictPartial') return 'statusPartial';
259
+ return 'statusDifferent';
260
+ }
261
+
262
+ private verdictState(verdictKey: Result['verdictKey']): string {
263
+ if (verdictKey === 'verdictStrong') return 'strong';
264
+ if (verdictKey === 'verdictPartial') return 'partial';
265
+ return 'different';
266
+ }
267
+ }
@@ -0,0 +1,6 @@
1
+ ---
2
+ import { Bibliography as BibliographyRenderer } from '@jjlmoya/utils-shared';
3
+ import { bibliography } from './bibliography';
4
+ ---
5
+
6
+ <BibliographyRenderer links={bibliography} />
@@ -0,0 +1,16 @@
1
+ import type { BibliographyEntry } from '../../types';
2
+
3
+ export const bibliography: BibliographyEntry[] = [
4
+ {
5
+ name: 'ASTM E1967 - Standard Test Method for the Automated Determination of Refractive Index of Glass Samples Using the Oil Immersion Method.',
6
+ url: 'https://store.astm.org/e1967-11a.html',
7
+ },
8
+ {
9
+ name: 'Forensic Examination of Glass and Paint Evidence.',
10
+ url: 'https://www.justice.gov/archives/olp/file/861916/dl?inline=',
11
+ },
12
+ {
13
+ name: 'Forensic Science Communications. Glass Evidence: Interpretation and Significance.',
14
+ url: 'https://archives.fbi.gov/archives/about-us/lab/forensic-science-communications/fsc/july2004/research/2004_03_research01.htm',
15
+ },
16
+ ];
@@ -0,0 +1,81 @@
1
+ ---
2
+ import './forensic-glass-becke-line-simulator.css';
3
+
4
+ interface Props {
5
+ ui: Record<string, string>;
6
+ }
7
+
8
+ const { ui } = Astro.props;
9
+ ---
10
+
11
+ <section class="becke-shell" data-becke-tool>
12
+ <div class="becke-scope">
13
+ <div class="becke-viewer" aria-label={ui.viewerAria}>
14
+ <canvas id="becke-canvas" width="720" height="540"></canvas>
15
+ <div class="becke-reticle" aria-hidden="true"></div>
16
+ <div class="becke-material-chip" id="becke-material-chip">{ui.sodaLime}</div>
17
+ <div class="becke-badge">
18
+ <span>{ui.matchMeter}</span>
19
+ <strong id="becke-match">-</strong>
20
+ </div>
21
+ </div>
22
+
23
+ <aside class="becke-controls">
24
+ <div class="becke-unit-toggle" role="group" aria-label={ui.units}>
25
+ <button type="button" data-becke-unit="metric" data-active="true">{ui.metric}</button>
26
+ <button type="button" data-becke-unit="imperial" data-active="false">{ui.imperial}</button>
27
+ </div>
28
+
29
+ <div class="becke-field becke-choice" data-becke-choice>
30
+ <span>{ui.glassSample}</span>
31
+ <button id="becke-glass-trigger" class="becke-choice-trigger" type="button" aria-haspopup="listbox" aria-expanded="false">
32
+ <span id="becke-glass-label">{ui.sodaLime}</span>
33
+ <span class="becke-choice-icon" aria-hidden="true"></span>
34
+ </button>
35
+ <div class="becke-choice-menu" id="becke-glass-menu" role="listbox" aria-label={ui.glassSample}>
36
+ <button type="button" role="option" data-becke-glass="sodaLime" aria-selected="true">{ui.sodaLime}</button>
37
+ <button type="button" role="option" data-becke-glass="borosilicate" aria-selected="false">{ui.borosilicate}</button>
38
+ <button type="button" role="option" data-becke-glass="leadCrystal" aria-selected="false">{ui.leadCrystal}</button>
39
+ <button type="button" role="option" data-becke-glass="tempered" aria-selected="false">{ui.tempered}</button>
40
+ </div>
41
+ </div>
42
+
43
+ <label class="becke-slider becke-heat">
44
+ <span>{ui.stageTemperature}</span>
45
+ <strong id="becke-temperature-label">-</strong>
46
+ <input id="becke-temperature" type="range" min="15" max="145" value="96" step="0.5" />
47
+ </label>
48
+
49
+ <div class="becke-focus" role="group" aria-label={ui.focusDirection}>
50
+ <button type="button" data-becke-focus="raised" data-active="true">{ui.focusRaised}</button>
51
+ <button type="button" data-becke-focus="lowered" data-active="false">{ui.focusLowered}</button>
52
+ </div>
53
+
54
+ <div class="becke-readouts">
55
+ <div>
56
+ <span>{ui.liquidRi}</span>
57
+ <strong id="becke-liquid-ri">-</strong>
58
+ </div>
59
+ <div>
60
+ <span>{ui.glassRi}</span>
61
+ <strong id="becke-glass-ri">-</strong>
62
+ </div>
63
+ <div>
64
+ <span>{ui.deltaRi}</span>
65
+ <strong id="becke-delta-ri">-</strong>
66
+ </div>
67
+ </div>
68
+
69
+ <p class="becke-interpretation" id="becke-interpretation">-</p>
70
+ </aside>
71
+
72
+ <p class="becke-disclaimer">{ui.disclaimer}</p>
73
+ </div>
74
+ </section>
75
+
76
+ <script id="becke-ui" type="application/json" set:html={JSON.stringify(ui)}></script>
77
+ <script>
78
+ import { initBeckeLineSimulator } from './view';
79
+
80
+ initBeckeLineSimulator(JSON.parse(document.getElementById('becke-ui')?.textContent || '{}'));
81
+ </script>