@jjlmoya/utils-forensic-science 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/package.json +3 -2
  2. package/src/category/i18n/es.ts +9 -9
  3. package/src/category/index.ts +3 -1
  4. package/src/entries.ts +5 -1
  5. package/src/index.ts +1 -0
  6. package/src/tests/locale_completeness.test.ts +2 -2
  7. package/src/tests/tool_validation.test.ts +2 -2
  8. package/src/tool/bloodstain-pattern-origin-analyzer/bibliography.astro +9 -0
  9. package/src/tool/bloodstain-pattern-origin-analyzer/bibliography.ts +7 -0
  10. package/src/tool/bloodstain-pattern-origin-analyzer/bloodstain-pattern-origin-analyzer.css +438 -0
  11. package/src/tool/bloodstain-pattern-origin-analyzer/component.astro +558 -0
  12. package/src/tool/bloodstain-pattern-origin-analyzer/entry.ts +32 -0
  13. package/src/tool/bloodstain-pattern-origin-analyzer/i18n/de.ts +119 -0
  14. package/src/tool/bloodstain-pattern-origin-analyzer/i18n/en.ts +119 -0
  15. package/src/tool/bloodstain-pattern-origin-analyzer/i18n/es.ts +119 -0
  16. package/src/tool/bloodstain-pattern-origin-analyzer/i18n/fr.ts +119 -0
  17. package/src/tool/bloodstain-pattern-origin-analyzer/i18n/id.ts +119 -0
  18. package/src/tool/bloodstain-pattern-origin-analyzer/i18n/it.ts +119 -0
  19. package/src/tool/bloodstain-pattern-origin-analyzer/i18n/ja.ts +119 -0
  20. package/src/tool/bloodstain-pattern-origin-analyzer/i18n/ko.ts +119 -0
  21. package/src/tool/bloodstain-pattern-origin-analyzer/i18n/nl.ts +119 -0
  22. package/src/tool/bloodstain-pattern-origin-analyzer/i18n/pl.ts +119 -0
  23. package/src/tool/bloodstain-pattern-origin-analyzer/i18n/pt.ts +119 -0
  24. package/src/tool/bloodstain-pattern-origin-analyzer/i18n/ru.ts +119 -0
  25. package/src/tool/bloodstain-pattern-origin-analyzer/i18n/sv.ts +119 -0
  26. package/src/tool/bloodstain-pattern-origin-analyzer/i18n/tr.ts +119 -0
  27. package/src/tool/bloodstain-pattern-origin-analyzer/i18n/zh.ts +119 -0
  28. package/src/tool/bloodstain-pattern-origin-analyzer/index.ts +11 -0
  29. package/src/tool/bloodstain-pattern-origin-analyzer/logic.test.ts +23 -0
  30. package/src/tool/bloodstain-pattern-origin-analyzer/logic.ts +137 -0
  31. package/src/tool/bloodstain-pattern-origin-analyzer/seo.astro +10 -0
  32. package/src/tools.ts +3 -1
@@ -0,0 +1,119 @@
1
+ import { bibliography } from '../bibliography';
2
+ import type { ToolLocaleContent } from '../../../types';
3
+
4
+ const slug = 'simulator-ursprung-blodstanksmuster';
5
+ const title = '3D Simulator for berakning av ursprung for blodstanksmuster';
6
+ const description = 'Modellera elliptiska blodfläckar på ett vertikalt plan, uppskatta träffvinklar och visualisera projicerade banor i en interaktiv 3D-scen.';
7
+
8
+ const howTo = [
9
+ { name: 'Mata in eller justera fläckar', text: 'Använd tabellen eller 2D-ytan för att definiera varje fläcks position, bredd, längd och orientering för dess huvudaxel.' },
10
+ { name: 'Kontrollera träffvinklar', text: 'Kalkylatorn uppskattar träffvinkeln från förhållandet mellan bredd och längd med hjälp av invers sinusfunktion.' },
11
+ { name: 'Granska 3D-konvergensen', text: 'Rotera vyn för att jämföra de projicerade banorna och den uppskattade ursprungssfären.' },
12
+ { name: 'Kontrollera osäkerheten', text: 'Använd spridningsvärdet som en praktisk varningsindikator: stor spridning av linjer innebär att det beräknade ursprunget är mindre stabilt.' },
13
+ ];
14
+
15
+ const faq = [
16
+ { question: 'Kan ett webbverktyg avgöra den verkliga källan till en blödning?', answer: 'Nej. Det kan visa och utvärdera geometriska samband, men slutsatser i ett fall kräver validerade metoder, dokumentation av platsen, kalibrering av skala och kvalificerad blodstänksmönsteranalys.' },
17
+ { question: 'Varför beräknas träffvinkeln från bredd och längd?', answer: 'För en ideal elliptisk fläck approximeras sinus för träffvinkeln genom att dela bredden med längden. Verkliga fläckar kan förvrängas av ytans struktur, rinningar, överföring eller mätfel.' },
18
+ { question: 'Vad representerar den uppskattade 3D-ursprungssfären?', answer: 'Det är den minsta kvadrat-konvergensberäkningen mellan de projicerade banlinjerna, inte en garanterad punktkälla.' },
19
+ ];
20
+
21
+ export const content: ToolLocaleContent = {
22
+ slug,
23
+ title,
24
+ description,
25
+ ui: {
26
+ metric: 'Metrisk',
27
+ imperial: 'Imperial',
28
+ unitsLabel: 'Enheter',
29
+ addStain: 'Lägg till fläck',
30
+ reset: 'Återställ',
31
+ surface: 'Träffyta',
32
+ viewport3dLabel: '3D-vy för banor',
33
+ stainLabel: 'Fläck',
34
+ table: 'Mätningar',
35
+ origin: 'Uppskattat ursprung',
36
+ spread: 'Linjespridning',
37
+ confidence: 'Konfidens',
38
+ impact: 'Träffvinkel',
39
+ high: 'Hög',
40
+ medium: 'Medium',
41
+ low: 'Låg',
42
+ x: 'X',
43
+ y: 'Y',
44
+ width: 'Bredd',
45
+ length: 'Längd',
46
+ rotation: 'Rotation',
47
+ remove: 'Ta bort',
48
+ cm: 'cm',
49
+ inch: 'tum',
50
+ degree: 'grader',
51
+ rotateHint: 'Dra i 3D-vyn för att rotera rekonstruktionen.',
52
+ disclaimer: 'Endast för utbildningsrekonstruktion. Tolka med validerade platsmätningar och dokumenterad osäkerhet.',
53
+ },
54
+ seo: [
55
+ { type: 'title', text: 'Hur en 3D Simulator for berakning av ursprung for blodstanksmuster fungerar', level: 2 },
56
+ { type: 'paragraph', html: 'En simulator för ursprung av blodstänksmönster hjälper användare att utforska en av de centrala geometriska frågorna inom blodstänksanalys: <strong>var i rummet kan blodkällan ha varit när dropparna träffade en yta?</strong> Verktyget modellerar fläckar som ellipser på ett vertikalt plan, beräknar en träffvinkel från varje förhållande mellan bredd och längd, projicerar banor bakåt i en 3D-scen och uppskattar ett praktiskt konvergensområde.' },
57
+ { type: 'diagnostic', variant: 'warning', title: 'Adliell varning', html: 'Resultatet är ett rekonstruktionshjälpmedel, inte en slutgiltig slutsats i ett fall. Verkliga fläckar kan påverkas av ytans struktur, satellitstänk, koagulering, rinningar, avtorkning, överföring, perspektivfel och ofullständig dokumentation av skala.' },
58
+ { type: 'stats', columns: 3, items: [
59
+ { value: 'asin(B/L)', label: 'Formel för träffvinkel' },
60
+ { value: '3+', label: 'Rekommenderade oberoende fläckar' },
61
+ { value: '3D', label: 'Konvergensområde för banor' },
62
+ ] },
63
+ { type: 'title', text: 'Formel för träffvinkel', level: 3 },
64
+ { type: 'paragraph', html: 'För en enkel elliptisk fläck approximeras träffvinkeln vanligtvis som <strong>arcsin(bredd / längd)</strong>. En fläck som mäts till 1 cm bredd och 2 cm längd ger därför en träffvinkel på 30 grader. Det förhållandet är användbart eftersom det omvandlar ett platt märke till en banhöjd, men det förutsätter att den uppmätta fläcken är representativ och inte förvrängd.' },
65
+ { type: 'code', ariaLabel: 'Beräkning av träffvinkel', code: 'träffvinkel = arcsin(fläckens bredd / fläckens längd)\nexempel: arcsin(1 cm / 2 cm) = 30 grader' },
66
+ { type: 'paragraph', html: 'Detta är en vanlig sökintention bakom frågor som <em>kalkylator för träffvinkel på blodfläckar</em>, <em>formel för träffvinkel för blodstänk</em> och <em>hur man beräknar blodstänksbana</em>. Det viktiga är att formeln bara besvarar en del av rekonstruktionen. Den uppskattar banans höjd i förhållande till träffytan; den identifierar inte i sig källan, mekanismen, vapnet eller händelseförloppet.' },
67
+ { type: 'title', text: 'Hur man mäter fläckar innan data matas in', level: 3 },
68
+ { type: 'paragraph', html: 'De mest användbara indata kommer från kalibrerade, vinkelräta fotografier eller direkta platsmätningar. Mät fläckens elliptiska huvudkropp, inte satellitstänk, svansar, rinningar eller sekundära fläckar. Bredden ska mätas längs den korta axeln och längden längs den långa axeln. Rotationen bör följa den långa axelns riktning på träffytan. Små fel i längd, bredd eller orientering kan flytta den projicerade banan avsevärt när den utvidgas i 3D-rymden.' },
69
+ { type: 'table', headers: ['Mätkvalitet', 'Sannolik effekt', 'Praktisk åtgärd'], rows: [
70
+ ['Den långa axeln är tydlig', 'Rotationen är mer tillförlitlig', 'Använd den synliga långa axeln och dokumentera orienteringsmetoden.'],
71
+ ['Bredden eller längden är förvrängd', 'Träffvinkeln kan vara partisk', 'Markera osäkerheten och jämför med angränsande fläckar.'],
72
+ ['Få fläckar konvergerar', 'Ursprungsuppskattningen blir instabil', 'Lägg till oberoende fläckar innan du tolkar 3D-ursprunget.'],
73
+ ['Skala saknas', 'Alla avstånd blir endast illustrativa', 'Rapportera inte verkliga ursprungskoordinater utan kalibrering.'],
74
+ ] },
75
+ { type: 'title', text: 'Att läsa 3D-konvergensen', level: 3 },
76
+ { type: 'paragraph', html: 'Ursprungssfären i detta verktyg beräknas som den punkt som ligger närmast alla projicerade banlinjer. När linjer passerar nära varandra är spridningsvärdet litet och konfidensen förbättras. När linjer divergerar visas sfären fortfarande, men den bör tolkas som en svag minsta kvadrat-kompromiss snarare än en exakt källa.' },
77
+ { type: 'comparative', columns: 2, items: [
78
+ { title: 'Konvergensområde', description: 'En tvådimensionell uppskattning på träffytan, som ofta används för att se var de långa axlarnas riktningar möts när man tittar framifrån.', points: ['Användbar för en första bedömning', 'Representerar inte vertikal höjd i sig'] },
79
+ { title: 'Ursprungsområde eller Ursprungsvolym', description: 'En tredimensionell uppskattning som kombinerar riktningen på planet med träffvinkeln för att projicera banor i rymden.', highlight: true, points: ['Förklarar möjlig höjd på källan', 'Känslig för osäkerhet i mätning och vinkel'] },
80
+ ] },
81
+ { type: 'paragraph', html: 'Användare söker ofta efter en <em>kalkylator för ursprungspunkt för blodstänk</em> och förväntar sig en enda exakt koordinat. I praktiken kräver en god tolkning försiktighet. Ursprunget förstås bäst som en region som stöds av flera banor. Om de ritade linjerna bildar ett tätt knippe är modellen mer koherent. Om de passerar genom en stor volym talar rekonstruktionen om för dig att mätningarna, fläckvalet eller antagandena behöver ses över.' },
82
+ { type: 'title', text: 'Vanliga orsaker till att banorna inte möts', level: 3 },
83
+ { type: 'proscons', title: 'Starka indata jämfört med svaga indata', items: [
84
+ { pro: 'Flera välformade elliptiska fläckar med tydliga långaxlar', con: 'Endast en eller två fläckar, eller fläckar som valts ut för att de verkar passa en teori' },
85
+ { pro: 'Kalibrerade mätningar tagna vinkelrätt mot träffytan', con: 'Snea foton, okänd skala eller kopierade skärmdumpar' },
86
+ { pro: 'Oberoende fläckar från samma händelsemekanism', con: 'Blandade passiva droppar, överföringsfläckar, kaststänk, rinningar eller sekundära träffmönster' },
87
+ ] },
88
+ { type: 'paragraph', html: 'En dålig konvergens betyder inte automatiskt att verktyget har fel. Det kan innebära att fläckarna inte tillhör samma mönster, att fotografiet är perspektivförvrängt, att den långa axeln fellästes, att fläcken inte är en ren ellips eller att den fysiska händelsen är mer komplex än en enkel punktkällmodell. Det är värdefull information. Ett rekonstruktionsverktyg bör visa oenighet snarare än att dölja den bakom en tillförlitlig siffra.' },
89
+ { type: 'title', text: 'Vad detta verktyg är bra för', level: 3 },
90
+ { type: 'list', items: [
91
+ '<strong>Använd flera fläckar:</strong> Tre eller fler oberoende fläckar ger en mer meningsfull konvergensuppskattning.',
92
+ '<strong>Bevaka spridningen:</strong> En hög spridning varnar för att linjerna inte stämmer överens i 3D.',
93
+ '<strong>Behåll skalan:</strong> Centimeter- eller tumvärden måste komma från kalibrerade fotografier eller platsmätningar.',
94
+ '<strong>Skilj utbildning från formell rapportering:</strong> Denna visualisering är bäst för utbildning, planering och förklaring före formell granskning.',
95
+ ] },
96
+ { type: 'card', title: 'Bästa arbetsflöde', html: 'Börja med att rita upp en liten uppsättning fläckar av hög kvalitet. Kontrollera om de 2D-riktningarna för långaxeln verkar logiska. Granska sedan 3D-banorna, notera spridningen och ändra en mätning i taget för att se vilken fläck som styr rekonstruktionen. Den känslighetskontrollen är ofta mer användbar än den råa ursprungskoordinaten.' },
97
+ { type: 'title', text: 'När man inte bör lita på en ursprungsuppskattning för blodstänk', level: 3 },
98
+ { type: 'paragraph', html: 'Lita inte på en enkel ursprungsuppskattning när fläckarna är kraftigt förvrängda, träffytan är böjd eller oregelbunden, mönstret inkluderar överföringsfläckar eller utandad kasta-av, ytan flyttades eller platsen saknar en tillförlitlig skala. Samma försiktighet gäller när fläckar kommer från olika mekanismer eller olika tidpunkter i händelseförloppet. En ren 3D-visualisering kan fortfarande vara vilseledande om indatamönstret inte är lämpligt för geometrisk rekonstruktion.' },
99
+ { type: 'glossary', items: [
100
+ { term: 'Träffvinkel', definition: 'Den uppskattade vinkeln mellan droppens bana och träffytan, vanligtvis beräknad från förhållandet mellan bredd och längd för en elliptisk fläck.' },
101
+ { term: 'Banlinje', definition: 'En projicerad linje som visar en möjlig bakåtriktad färdbana från en fläck in i det tredimensionella rummet.' },
102
+ { term: 'Konvergensområde', definition: 'Det ungefärliga området där fläckarnas riktningar skär varandra när de projiceras på träffplanet.' },
103
+ { term: 'Ursprungsområde', definition: 'Det ungefärliga tredimensionella området från vilket blodet kan ha kommit, baserat på flera projicerade banor.' },
104
+ ] },
105
+ { type: 'summary', title: 'Bästa användning', items: [
106
+ 'Använd analysatorn för att förstå banans geometri och testa mätkänsligheten.',
107
+ 'Jämför ursprungssfären med spridningsvärdet innan du litar på en rekonstruktion.',
108
+ 'Använd inte visualiseringen som ett självständigt adliellt utlåtande.',
109
+ ] },
110
+ ],
111
+ faq,
112
+ bibliography,
113
+ howTo,
114
+ schemas: [
115
+ { '@context': 'https://schema.org', '@type': 'SoftwareApplication', name: title, description, applicationCategory: 'ForensicApplication', operatingSystem: 'Any' },
116
+ { '@context': 'https://schema.org', '@type': 'FAQPage', mainEntity: faq.map((item) => ({ '@type': 'Question', name: item.question, acceptedAnswer: { '@type': 'Answer', text: item.answer } })) },
117
+ { '@context': 'https://schema.org', '@type': 'HowTo', name: title, step: howTo.map((step) => ({ '@type': 'HowToStep', name: step.name, text: step.text })) },
118
+ ],
119
+ };
@@ -0,0 +1,119 @@
1
+ import { bibliography } from '../bibliography';
2
+ import type { ToolLocaleContent } from '../../../types';
3
+
4
+ const slug = 'kan-lekesi-modeli-koken-analizoru';
5
+ const title = '3D Kan Lekesi Modeli Koken Analizörü';
6
+ const description = 'Dikey bir düzlemde eliptik kan lekelerini modelleyin, çarpma açılarını tahmin edin ve etkileşimli bir 3D sahnede yansıtılan yörüngeleri görselleştirin.';
7
+
8
+ const howTo = [
9
+ { name: 'Lekeleri girin veya ayarlayın', text: 'Her bir lekenin konumunu, genişliğini, uzunluğunu ve uzun eksen yönünü tanımlamak için tabloyu veya 2D yüzeyi kullanın.' },
10
+ { name: 'Çarpma açılarını inceleyin', text: 'Hesaplayıcı, arksinüs ilişkisini kullanarak genişlik/uzunluk oranından çarpma açısını tahmin eder.' },
11
+ { name: '3D yakınsamayı denetleyin', text: 'Yansıtılan yörüngeleri ve tahmini köken küresini karşılaştırmak için görüş alanını döndürün.' },
12
+ { name: 'Belirsizliği kontrol edin', text: 'Yayılma değerini pratik bir uyarı göstergesi olarak kullanın: geniş çizgi ayrımı, çizilen kökenin daha az kararlı olduğu anlamına gelir.' },
13
+ ];
14
+
15
+ const faq = [
16
+ { question: 'Bir tarayıcı aracı kan dökülmesinin gerçek kaynağını belirleyebilir mi?', answer: 'Hayır. Geometrik ilişkileri öğretebilir ve ön değerlendirme yapabilir, ancak vaka sonuçları doğrulanmış yöntemler, olay yeri dokümantasyonu, ölçek kalibrasyonu ve nitelikli kan lekesi modeli analizi gerektirir.' },
17
+ { question: 'Çarpma açısı neden genişlik ve uzunluktan hesaplanır?', answer: 'İdeal bir eliptik leke için çarpma açısının sinüsü, genişliğin uzunluğa bölünmesiyle tahmin edilir. Gerçek lekeler yüzey dokusu, akış, transfer veya ölçüm hatası nedeniyle bozulabilir.' },
18
+ { question: '3D köken küresi neyi temsil ediyor?', answer: 'Yansıtılan yörünge çizgileri arasındaki en küçük kareler yakınsama tahminidir, garantili bir nokta kaynak değildir.' },
19
+ ];
20
+
21
+ export const content: ToolLocaleContent = {
22
+ slug,
23
+ title,
24
+ description,
25
+ ui: {
26
+ metric: 'Metrik',
27
+ imperial: 'İmparatorluk',
28
+ unitsLabel: 'Birimler',
29
+ addStain: 'Leke ekle',
30
+ reset: 'Sıfırla',
31
+ surface: 'Çarpma yüzeyi',
32
+ viewport3dLabel: '3D yörünge görünümü',
33
+ stainLabel: 'Leke',
34
+ table: 'Ölçümler',
35
+ origin: 'Tahmini köken',
36
+ spread: 'Çizgi yayılması',
37
+ confidence: 'Güven düzeyi',
38
+ impact: 'Çarpma açısı',
39
+ high: 'Yüksek',
40
+ medium: 'Orta',
41
+ low: 'Düşük',
42
+ x: 'X',
43
+ y: 'Y',
44
+ width: 'Genişlik',
45
+ length: 'Uzunluk',
46
+ rotation: 'Dönme',
47
+ remove: 'Kaldır',
48
+ cm: 'cm',
49
+ inch: 'inç',
50
+ degree: 'derece',
51
+ rotateHint: 'Rekonstrüksiyonu döndürmek için 3D görünümü sürükleyin.',
52
+ disclaimer: 'Yalnızca eğitim amaçlı rekonstrüksiyon. Doğrulanmış olay yeri ölçümleri ve belgelenmiş belirsizlikle yorumlayın.',
53
+ },
54
+ seo: [
55
+ { type: 'title', text: '3D Kan Lekesi Modeli Koken Analizörü Nasıl Çalışır', level: 2 },
56
+ { type: 'paragraph', html: 'Bir kan lekesi modeli köken analizörü, kullanıcıların kan lekesi modeli analizindeki temel geometrik sorulardan birini keşfetmesine yardımcı olur: <strong>damlacıklar bir yüzeye çarptığında kan kaynağı uzayda nerede bulunuyor olabilirdi?</strong> Bu araç, lekeleri dikey bir düzlemde elips olarak modeller, her bir genişlik/uzunluk oranından bir çarpma açısı hesaplar, yörüngeleri geriye doğru bir 3D sahneye yansıtır ve pratik bir yakınsama bölgesi tahmin eder.' },
57
+ { type: 'diagnostic', variant: 'warning', title: 'Adli tıp uyarısı', html: 'Elde edilen sonuç bir rekonstrüksiyon yardımıdır, vaka kararı değildir. Gerçek lekeler hedef dokusu, uydu lekeler, pıhtılaşma, akış, silinme, transfer, perspektif hatası ve eksik ölçek belgelendirmesinden etkilenebilir.' },
58
+ { type: 'stats', columns: 3, items: [
59
+ { value: 'asin(G/U)', label: 'Çarpma açısı formülü' },
60
+ { value: '3+', label: 'Önerilen bağımsız leke sayısı' },
61
+ { value: '3D', label: 'Yörünge yakınsama alanı' },
62
+ ] },
63
+ { type: 'title', text: 'Çarpma Açısı Formülü', level: 3 },
64
+ { type: 'paragraph', html: 'Basit bir eliptik leke için çarpma açısı genellikle <strong>arcsin(genişlik / uzunluk)</strong> olarak tahmin edilir. Bu nedenle, 1 cm genişliğinde ve 2 cm uzunluğunda ölçülen bir leke 30 derecelik bir çarpma açısı üretir. Bu ilişki yararlıdır çünkü düz bir izi yörünge yüksekliğine dönüştürür; ancak ölçülen lekenin temsilci olduğunu ve bozulmadığını varsayar.' },
65
+ { type: 'code', ariaLabel: 'Çarpma açısı hesabı', code: 'çarpma açısı = arcsin(leke genişliği / leke uzunluğu)\nörnek: arcsin(1 cm / 2 cm) = 30 derece' },
66
+ { type: 'paragraph', html: 'Bu, <em>kan lekesi çarpma açısı hesaplayıcı</em>, <em>kan sıçraması çarpma açısı formülü</em> ve <em>kan lekesi yörüngesi nasıl hesaplanır</em> gibi sorguların arkasındaki yaygın bir arama amacıdır. Önemli olan nokta, formülün rekonstrüksiyonun yalnızca bir bölümüne yanıt vermesidir. Hedef yüzeye göre hareket yüksekliğini tahmin eder; kendi başına kaynağı, mekanizmayı, silahı veya olay sırasını tanımlamaz.' },
67
+ { type: 'title', text: 'Verileri Girmeden Önce Lekeler Nasıl Ölçülür', level: 3 },
68
+ { type: 'paragraph', html: 'En yararlı girdiler kalibre edilmiş, dik çekilmiş fotoğraflardan veya doğrudan olay yeri ölçümlerinden gelir. Lekelerin ana eliptik gövdesini ölçün; uyduları, kuyrukları, akış izlerini veya ikincil lekeleri ölçmeyin. Genişlik kısa eksen boyunca, uzunluk ise uzun eksen boyunca alınmalıdır. Dönme, hedef düzlemdeki uzun eksen yönünü takip etmelidir. Uzunluk, genişlik veya yönelimdeki küçük hatalar, yansıtılan yörüngeyi 3D uzayda uzatıldığında büyük miktarda kaydırabilir.' },
69
+ { type: 'table', headers: ['Ölçüm kalitesi', 'Olası etki', 'Pratik yanıt'], rows: [
70
+ ['Uzun eksen nettir', 'Dönme daha güvenilirdir', 'Görünür uzun ekseni kullanın ve yönelim yöntemini belgeleyin.'],
71
+ ['Genişlik veya uzunluk bozuktur', 'Çarpma açısı sapmalı olabilir', 'Belirsizliği işaretleyin ve komşu lekelerle karşılaştırın.'],
72
+ ['Az sayıda leke yakınsıyor', 'Köken tahmini kararsız hale gelir', '3D kökeni yorumlamadan önce bağımsız lekeler ekleyin.'],
73
+ ['Ölçek eksik', 'Tüm mesafeler yalnızca örnekleme haline gelir', 'Kalibrasyon olmadan gerçek dünya köken koordinatlarını raporlamayın.'],
74
+ ] },
75
+ { type: 'title', text: '3D Yakınsamayı Okumak', level: 3 },
76
+ { type: 'paragraph', html: 'Bu araçtaki köken küresi, yansıtılan tüm yörünge çizgilerine en yakın duran nokta olarak hesaplanır. Çizgiler birbirine yakın geçtiğinde, yayılma değeri küçülür ve güven düzeyi artar. Çizgiler saptığında, küre yine de görünür ancak kesin bir kaynak yerine zayıf bir en küçük kareler uzlaşması olarak okunmalıdır.' },
77
+ { type: 'comparative', columns: 2, items: [
78
+ { title: 'Yakınsama alanı', description: 'Hedef düzlemde iki boyutlu bir tahmin, genellikle önden bakıldığında uzun eksen yönlerinin nerede birleştiğini görmek için kullanılır.', points: ['İlk değerlendirme taraması için yararlıdır', 'Tek başına dikey yüksekliği temsil etmez'] },
79
+ { title: 'Köken alanı veya hacmi', description: 'Yörüngeleri uzaya yansıtmak için düzlemdeki yönü çarpma açısıyla birleştiren üç boyutlu bir tahmin.', highlight: true, points: ['Olası kaynak yüksekliğini açıklar', 'Ölçüm ve açı belirsizliğine duyarlıdır'] },
80
+ ] },
81
+ { type: 'paragraph', html: 'Kullanıcılar genellikle tek bir kesin koordinat bekleyerek <em>kan sıçraması köken noktası hesaplayıcı</em> araması yaparlar. Pratikte, iyi bir yorumlama daha ihtiyatlıdır. Köken, birden fazla yörünge tarafından desteklenen bir bölge olarak daha iyi anlaşılır. Çizilen çizgiler sıkı bir demet oluşturuyorsa model daha tutarlıdır. Geniş bir hacimden geçiyorlarsa, rekonstrüksiyon size ölçümlerin, leke seçiminin veya varsayımların gözden geçirilmesi gerektiğini söylüyor demektir.' },
82
+ { type: 'title', text: 'Yörüngelerin Birleşmeme Nedenleri', level: 3 },
83
+ { type: 'proscons', title: 'Güçlü girdiler vs zayıf girdiler', items: [
84
+ { pro: 'Net uzun eksenleri olan birkaç iyi biçimlendirilmiş eliptik leke', con: 'Sadece bir veya iki leke veya bir teoriye uyduğu için seçilen lekeler' },
85
+ { pro: 'Çarpma yüzeyine dik olarak alınan kalibre edilmiş ölçümler', con: 'Eğik fotoğraflar, bilinmeyen ölçek veya kopyalanmış ekran görüntüleri' },
86
+ { pro: 'Aynı olay mekanizmasından gelen bağımsız lekeler', con: 'Karışık pasif damlalar, transfer lekeleri, savrulma izleri, akış veya ikincil çarpma modelleri' },
87
+ ] },
88
+ { type: 'paragraph', html: 'Kötü bir yakınsama, aracın otomatik olarak yanlış olduğu anlamına gelmez. Lekelerin aynı modele ait olmadığı, fotoğrafın perspektif bozulmasına uğradığı, uzun eksenin yanlış okunduğu, lekenin temiz bir elips olmadığı veya fiziksel olayın basit bir nokta kaynak modelinden daha karmaşık olduğu anlamına gelebilir. Bu değerli bir bilgidir. Bir rekonstrüksiyon aracı, anlaşmazlığı güvenli görünen bir sayının arkasına saklamak yerine ortaya çıkarmalıdır.' },
89
+ { type: 'title', text: 'Bu Araç Ne İçin İşe Yarar', level: 3 },
90
+ { type: 'list', items: [
91
+ '<strong>Birden fazla leke kullanın:</strong> üç veya daha fazla bağımsız leke daha anlamlı bir yakınsama tahmini sağlar.',
92
+ '<strong>Yayılmayı izleyin:</strong> yüksek yayılma değeri, çizgilerin 3D de yakından uyuşmadığı konusunda uyarır.',
93
+ '<strong>Ölçeği koruyun:</strong> santimetre veya inç değerleri kalibre edilmiş fotoğraflardan veya olay yeri ölçümlerinden gelmelidir.',
94
+ '<strong>Eğitimi raporlamadan ayırın:</strong> bu görselleştirme, resmi incelemeden önce eğitim, planlama ve açıklama için en iyisidir.',
95
+ ] },
96
+ { type: 'card', title: 'En iyi çalışma akışı', html: 'Küçük bir yüksek kaliteli leke seti çizerek başlayın. 2D uzun eksen yönlerinin görsel olarak mantıklı olup olmadığını kontrol edin. Ardından 3D yörüngeleri inceleyin, yayılmayı not edin ve rekonstrüksiyonu hangi lekenin yönlendirdiğini görmek için her seferinde bir ölçümü değiştirin. Bu duyarlılık kontrolü genellikle ham köken koordinatından daha yararlıdır.' },
97
+ { type: 'title', text: 'Ne Zaman Bir Kan Lekesi Köken Tahminine Güvenilmez', level: 3 },
98
+ { type: 'paragraph', html: 'Lekeler aşırı derecede bozulduğunda, hedef yüzey kavisli veya düzensiz olduğunda, model transfer veya fırlatılan nefes kanı içerdiğinde, yüzey hareket ettirildiğinde veya olay yerinde güvenilir bir ölçek bulunmadığında basit bir köken tahminine güvenmeyin. Aynı ihtiyat, lekeler farklı mekanizmalardan veya olayın farklı anlarından geldiğinde de geçerlidir. Giriş modeli geometrik rekonstrüksiyon için uygun değilse, temiz görünen bir 3D görselleştirme yine de yanıltıcı olabilir.' },
99
+ { type: 'glossary', items: [
100
+ { term: 'Çarpma açısı', definition: 'Damlacık yolu ile hedef yüzey arasındaki tahmini açı, genellikle eliptik bir lekenin genişlik/uzunluk oranından hesaplanır.' },
101
+ { term: 'Yörünge çizgisi', definition: 'Bir lekeden üç boyutlu uzaya doğru olası bir geriye dönük hareket yolunu gösteren yansıtılmış çizgi.' },
102
+ { term: 'Yakınsama alanı', definition: 'Leke yönlerinin hedef düzlemde bakıldığında kesiştiği yaklaşık bölge.' },
103
+ { term: 'Köken alanı', definition: 'Birden fazla yansıtılan yörüngeye dayanarak kanın kaynaklanmış olabileceği yaklaşık üç boyutlu bölge.' },
104
+ ] },
105
+ { type: 'summary', title: 'En iyi kullanım', items: [
106
+ 'Yörünge geometrisini anlamak ve ölçüm duyarlılığını test etmek için analizörü kullanın.',
107
+ 'Bir rekonstrüksiyona güvenmeden önce köken küresini yayılma değeriyle karşılaştırın.',
108
+ 'Görselleştirmeyi tek başına bir adli tıp görüşü olarak kullanmayın.',
109
+ ] },
110
+ ],
111
+ faq,
112
+ bibliography,
113
+ howTo,
114
+ schemas: [
115
+ { '@context': 'https://schema.org', '@type': 'SoftwareApplication', name: title, description, applicationCategory: 'ForensicApplication', operatingSystem: 'Any' },
116
+ { '@context': 'https://schema.org', '@type': 'FAQPage', mainEntity: faq.map((item) => ({ '@type': 'Question', name: item.question, acceptedAnswer: { '@type': 'Answer', text: item.answer } })) },
117
+ { '@context': 'https://schema.org', '@type': 'HowTo', name: title, step: howTo.map((step) => ({ '@type': 'HowToStep', name: step.name, text: step.text })) },
118
+ ],
119
+ };
@@ -0,0 +1,119 @@
1
+ import { bibliography } from '../bibliography';
2
+ import type { ToolLocaleContent } from '../../../types';
3
+
4
+ const slug = 'bloodstain-pattern-origin-analyzer';
5
+ const title = '3D血迹喷溅图案起点分析仪';
6
+ const description = '在垂直平面上模拟椭圆形血迹,估算撞击角度,并在交互式3D场景中可视化投影轨迹。';
7
+
8
+ const howTo = [
9
+ { name: '输入或调整血迹', text: '使用表格或2D平面来定义每个血迹的位置、宽度、长度和长轴方向。' },
10
+ { name: '查看撞击角度', text: '计算器利用反正弦关系,通过宽度与长度的比值估算撞击角度。' },
11
+ { name: '检查3D收敛情况', text: '旋转视口以比对投影轨迹和估算的起点球体。' },
12
+ { name: '检查不确定性', text: '将散布值作为实用的警示指标:线条间距较宽意味着绘制的起点稳定性较低。' },
13
+ ];
14
+
15
+ const faq = [
16
+ { question: '浏览器工具能确定实际流血事件的真实源头吗?', answer: '不能。它可以用于几何关系的教学和初步评估,但实际案件的结论需要经过验证的方法、现场记录、比例尺校准以及有资质的血迹图案分析。' },
17
+ { question: '为什么要根据宽度和长度计算撞击角度?', answer: '对于理想的椭圆形血迹,撞击角度的正弦值可通过宽度除以长度来近似。实际血迹可能会因表面粗糙度、流动、转移或测量误差而发生变形。' },
18
+ { question: '3D起点球体代表什么?', answer: '它是投影轨迹线之间的最小二乘法收敛估算值,而非保证存在的点源(精确起点)。' },
19
+ ];
20
+
21
+ export const content: ToolLocaleContent = {
22
+ slug,
23
+ title,
24
+ description,
25
+ ui: {
26
+ metric: '公制',
27
+ imperial: '英制',
28
+ unitsLabel: '单位',
29
+ addStain: '添加血迹',
30
+ reset: '重置',
31
+ surface: '承受体表面',
32
+ viewport3dLabel: '3D轨迹视口',
33
+ stainLabel: '血迹',
34
+ table: '测量数据',
35
+ origin: '估算起点',
36
+ spread: '线条散布',
37
+ confidence: '置信度',
38
+ impact: '撞击角度',
39
+ high: '高',
40
+ medium: '中',
41
+ low: '低',
42
+ x: 'X',
43
+ y: 'Y',
44
+ width: '宽度',
45
+ length: '长度',
46
+ rotation: '旋转角度',
47
+ remove: '移除',
48
+ cm: 'cm',
49
+ inch: '英寸',
50
+ degree: '度',
51
+ rotateHint: '拖动3D视图以旋转重建画面。',
52
+ disclaimer: '仅用于教学重建。请结合经验证的现场测量数据和记录的不确定性进行解读。',
53
+ },
54
+ seo: [
55
+ { type: 'title', text: '3D血迹喷溅图案起点分析仪的工作原理', level: 2 },
56
+ { type: 'paragraph', html: '血迹喷溅图案起点分析仪能帮助用户探索血迹图案分析中的核心几何问题之一:<strong>当血滴撞击表面时,空间中的血源可能在什么位置?</strong> 该工具将垂直面上的血迹模拟为椭圆,通过每个血迹的宽长比计算撞击角度,将轨迹向后投影到3D场景中,并估算出一个实用的收敛区域。' },
57
+ { type: 'diagnostic', variant: 'warning', title: '法医警示', html: '输出结果仅为重建辅助手段,而非案件最终结论。实际血迹可能受到承受体粗糙度、卫星状喷溅、凝固、流动、擦拭、转移、透视误差以及比例尺记录不完整的影响。' },
58
+ { type: 'stats', columns: 3, items: [
59
+ { value: 'asin(W/L)', label: '撞击角度公式' },
60
+ { value: '3个以上', label: '建议的独立血迹数' },
61
+ { value: '3D', label: '轨迹收敛空间' },
62
+ ] },
63
+ { type: 'title', text: '撞击角度公式', level: 3 },
64
+ { type: 'paragraph', html: '对于简单的椭圆形血迹,撞击角度通常近似为 <strong>arcsin(宽度 / 长度)</strong>。例如,测得宽度为1厘米、长度为2厘米的血迹,对应的撞击角度为30度。这种关系非常实用,因为它能将扁平的痕迹转换为轨迹仰角,但它假设测得的血迹具有代表性且未发生变形。' },
65
+ { type: 'code', ariaLabel: '撞击角度计算', code: '撞击角度 = arcsin(血迹宽度 / 血迹长度)\n例如: arcsin(1 cm / 2 cm) = 30度' },
66
+ { type: 'paragraph', html: '这是诸如<em>血迹撞击角度计算器</em>、<em>喷溅血迹撞击角度公式</em>和<em>如何计算血迹轨迹</em>等搜索意图背后的常见需求。重点在于,该公式仅能解决重建工作的一部分。它估算的是相对于承受体表面的飞行仰角,其本身并不能确定血源、形成机制、凶器或事件发生顺序。' },
67
+ { type: 'title', text: '在输入数据前如何测量血迹', level: 3 },
68
+ { type: 'paragraph', html: '最有效的输入数据来源于经过校准的垂直拍摄照片或直接的现场测量。应测量血迹的主要椭圆体,而不是卫星状喷溅、拖尾、流淌痕迹或二次血迹。宽度应沿短轴测量,长度应沿长轴测量。旋转角度应符合承受体平面上的长轴方向。一旦延伸到3D空间中,长度、宽度或方向的微小误差都可能导致投影轨迹发生巨大偏移。' },
69
+ { type: 'table', headers: ['测量质量', '可能的影响', '实际应对措施'], rows: [
70
+ ['长轴清晰', '旋转角度更可靠', '使用可见的长轴,并记录确定方向的方法。'],
71
+ ['宽度或长度变形', '撞击角度可能存在偏差', '标记不确定性,并与相邻血迹进行比对。'],
72
+ ['收敛血迹过少', '起点估算变得不稳定', '在解读3D起点之前,先增加独立的血迹。'],
73
+ ['缺失比例尺', '所有距离均仅具示意性质', '在没有校准的情况下,切勿报告实际的起点坐标。'],
74
+ ] },
75
+ { type: 'title', text: '解读3D收敛情况', level: 3 },
76
+ { type: 'paragraph', html: '该工具中的起点球体是计算出的与所有投影轨迹线距离最近的点。当线条彼此接近时,散布值较小,置信度提高。当线条发散时,球体仍会显示,但应将其理解为较弱的最小二乘法折中值,而非精确的源头。' },
77
+ { type: 'comparative', columns: 2, items: [
78
+ { title: '收敛区域', description: '承受体平面上的二维估算,通常用于在正视图中观察长轴方向相交的位置。', points: ['适用于初步筛选', '其本身不代表垂直高度'] },
79
+ { title: '起点区域或体积', description: '将平面方向与撞击角度结合,将轨迹投影到空间中的三维估算。', highlight: true, points: ['说明可能的血源高度', '对测量和角度的不确定性较敏感'] },
80
+ ] },
81
+ { type: 'paragraph', html: '用户经常搜索<em>血迹喷溅起点计算器</em>,期望得到一个单一的精确坐标。在实际应用中,合理的解读应更加谨慎。起点更适合被理解为由多条轨迹支持的区域。如果绘制的线条形成紧密的束状,则模型的一致性更高。如果它们穿过一个宽阔的空间体积,则重建在提醒您需要重新审核测量数据、血迹选择或假设。' },
82
+ { type: 'title', text: '轨迹无法交汇的常见原因', level: 3 },
83
+ { type: 'proscons', title: '可靠输入与薄弱输入对比', items: [
84
+ { pro: '数个长轴清晰、形状规则的椭圆形血迹', con: '仅有一两个血迹,或者专门挑选出符合某种假设的血迹' },
85
+ { pro: '垂直于撞击表面拍摄的校准测量数据', con: '倾斜照片、未知比例尺或复制的屏幕截图' },
86
+ { pro: '来自同一事件机制的的独立血迹', con: '混杂了静止滴落血迹、转移血迹、甩溅痕迹、流淌痕迹或二次撞击图案' },
87
+ ] },
88
+ { type: 'paragraph', html: '收敛效果差并不一定意味着工具出错。这可能意味着血迹不属于同一喷溅图案、照片存在透视变形、长轴方向读取错误、血迹不是纯粹的椭圆,或者物理事件比简单的点源模型更为复杂。这是非常有价值的信息。重建工具应该展现这种分歧,而不是将其隐藏在看似确定的数字背后。' },
89
+ { type: 'title', text: '本工具的适用场景', level: 3 },
90
+ { type: 'list', items: [
91
+ '<strong>使用多个血迹:</strong> 三个或更多独立的血迹能提供更有意义的收敛估算。',
92
+ '<strong>注意散布值:</strong> 高散布值警告您线条在3D空间中契合度较低。',
93
+ '<strong>保留比例尺:</strong> 厘米或英寸的数值必须来源于经校准的照片或现场测量。',
94
+ '<strong>将教学与正式报告分开:</strong> 该可视化分析最适合用于正式评估前的教学、规划和阐释方案。',
95
+ ] },
96
+ { type: 'card', title: '最佳工作流程', html: '首先绘制一小组高质量血迹。检查2D长轴方向在视觉上是否合理。然后检查3D轨迹,注意散布值,并每次更改一项测量数据,以观察哪个血迹在主导重建。这种敏感性检查通常比原始的起点坐标更有用。' },
97
+ { type: 'title', text: '何时不应依赖血迹起点估算', level: 3 },
98
+ { type: 'paragraph', html: '当血迹严重变形、承受体表面弯曲或不规则、图案中包含转移血迹或喷射出的呼出血液、表面已被移动,或现场缺乏可靠的比例尺时,切勿依赖简单的起点估算。如果血迹来自不同的形成机制或事件的不同时间点,也应同样保持谨慎。如果输入的图案不适合进行几何重建,即使一见之下干净漂亮的3D可视化图像也可能会产生误导。' },
99
+ { type: 'glossary', items: [
100
+ { term: '撞击角度', definition: '血滴路径与承受体表面之间的估算角度,通常根据椭圆形血迹的宽长比计算。' },
101
+ { term: '轨迹线', definition: '一条投影线,显示从血迹向三维空间反向延伸的可能飞行路径。' },
102
+ { term: '收敛区域', description: '在承受体平面上观察时,血迹方向交叉的大致区域。' },
103
+ { term: '起点区域', definition: '根据多条投影轨迹确定的血液可能来源于的大致三维空间区域。' },
104
+ ] },
105
+ { type: 'summary', title: '最佳应用', items: [
106
+ '使用分析仪来理解轨迹几何学并测试测量的敏感性。',
107
+ '在信任重建结果之前, 将起点球体与散布值进行对比。',
108
+ '切勿将该可视化图像作为独立的法医鉴定意见。',
109
+ ] },
110
+ ],
111
+ faq,
112
+ bibliography,
113
+ howTo,
114
+ schemas: [
115
+ { '@context': 'https://schema.org', '@type': 'SoftwareApplication', name: title, description, applicationCategory: 'ForensicApplication', operatingSystem: 'Any' },
116
+ { '@context': 'https://schema.org', '@type': 'FAQPage', mainEntity: faq.map((item) => ({ '@type': 'Question', name: item.question, acceptedAnswer: { '@type': 'Answer', text: item.answer } })) },
117
+ { '@context': 'https://schema.org', '@type': 'HowTo', name: title, step: howTo.map((step) => ({ '@type': 'HowToStep', name: step.name, text: step.text })) },
118
+ ],
119
+ };
@@ -0,0 +1,11 @@
1
+ import { bloodstainPatternOriginAnalyzer } from './entry';
2
+ import type { ToolDefinition } from '../../types';
3
+
4
+ export * from './entry';
5
+
6
+ export const BLOODSTAIN_PATTERN_ORIGIN_ANALYZER_TOOL: ToolDefinition = {
7
+ entry: bloodstainPatternOriginAnalyzer,
8
+ Component: () => import('./component.astro'),
9
+ SEOComponent: () => import('./seo.astro'),
10
+ BibliographyComponent: () => import('./bibliography.astro'),
11
+ };
@@ -0,0 +1,23 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { BloodstainCalculator, analyzeBloodstains } from './logic';
3
+
4
+ describe('bloodstain pattern origin analyzer', () => {
5
+ it('calculates a 30 degree impact angle from a 1 cm by 2 cm stain', () => {
6
+ const calculator = new BloodstainCalculator();
7
+
8
+ expect(calculator.calculateImpactAngleDeg(1, 2)).toBeCloseTo(30, 8);
9
+ });
10
+
11
+ it('resolves a compact origin region from three convergent trajectories', () => {
12
+ const result = analyzeBloodstains([
13
+ { id: 'A', xCm: 0, yCm: 0, widthCm: 1.2, lengthCm: 2, rotationDeg: 0 },
14
+ { id: 'B', xCm: 24, yCm: 0, widthCm: 1.08, lengthCm: 2, rotationDeg: 30.3 },
15
+ { id: 'C', xCm: -24, yCm: 0, widthCm: 1.08, lengthCm: 2, rotationDeg: -30.3 },
16
+ ]);
17
+
18
+ expect(result.point.x).toBeCloseTo(0, 0);
19
+ expect(result.point.y).toBeCloseTo(37, 0);
20
+ expect(result.point.z).toBeCloseTo(28, 0);
21
+ expect(result.spreadCm).toBeLessThan(2);
22
+ });
23
+ });
@@ -0,0 +1,137 @@
1
+ export interface BloodstainInput {
2
+ id: string;
3
+ xCm: number;
4
+ yCm: number;
5
+ widthCm: number;
6
+ lengthCm: number;
7
+ rotationDeg: number;
8
+ }
9
+
10
+ export interface TrajectoryLine {
11
+ stain: BloodstainInput;
12
+ impactAngleDeg: number;
13
+ originPoint: Vector3;
14
+ direction: Vector3;
15
+ }
16
+
17
+ export interface Vector3 {
18
+ x: number;
19
+ y: number;
20
+ z: number;
21
+ }
22
+
23
+ export interface OriginEstimate {
24
+ point: Vector3;
25
+ spreadCm: number;
26
+ confidence: 'low' | 'medium' | 'high';
27
+ lines: TrajectoryLine[];
28
+ }
29
+
30
+ const DEG_TO_RAD = Math.PI / 180;
31
+ const RAD_TO_DEG = 180 / Math.PI;
32
+
33
+ function clamp(value: number, min: number, max: number): number {
34
+ return Math.max(min, Math.min(max, value));
35
+ }
36
+
37
+ function normalize(vector: Vector3): Vector3 {
38
+ const length = Math.hypot(vector.x, vector.y, vector.z) || 1;
39
+ return { x: vector.x / length, y: vector.y / length, z: vector.z / length };
40
+ }
41
+
42
+ function dot(a: Vector3, b: Vector3): number {
43
+ return a.x * b.x + a.y * b.y + a.z * b.z;
44
+ }
45
+
46
+ function subtract(a: Vector3, b: Vector3): Vector3 {
47
+ return { x: a.x - b.x, y: a.y - b.y, z: a.z - b.z };
48
+ }
49
+
50
+ function add(a: Vector3, b: Vector3): Vector3 {
51
+ return { x: a.x + b.x, y: a.y + b.y, z: a.z + b.z };
52
+ }
53
+
54
+ function scale(vector: Vector3, factor: number): Vector3 {
55
+ return { x: vector.x * factor, y: vector.y * factor, z: vector.z * factor };
56
+ }
57
+
58
+ function distance(a: Vector3, b: Vector3): number {
59
+ return Math.hypot(a.x - b.x, a.y - b.y, a.z - b.z);
60
+ }
61
+
62
+ export class BloodstainCalculator {
63
+ calculateImpactAngleDeg(widthCm: number, lengthCm: number): number {
64
+ if (widthCm <= 0 || lengthCm <= 0) return 0;
65
+ const ratio = clamp(widthCm / lengthCm, 0, 1);
66
+ return Math.asin(ratio) * RAD_TO_DEG;
67
+ }
68
+
69
+ createTrajectory(stain: BloodstainInput): TrajectoryLine {
70
+ const impactAngleDeg = this.calculateImpactAngleDeg(stain.widthCm, stain.lengthCm);
71
+ const angle = stain.rotationDeg * DEG_TO_RAD;
72
+ const elevation = Math.max(impactAngleDeg, 2) * DEG_TO_RAD;
73
+ const planar = Math.cos(elevation);
74
+
75
+ return {
76
+ stain,
77
+ impactAngleDeg,
78
+ originPoint: { x: stain.xCm, y: stain.yCm, z: 0 },
79
+ direction: normalize({
80
+ x: -Math.sin(angle) * planar,
81
+ y: Math.cos(angle) * planar,
82
+ z: Math.sin(elevation),
83
+ }),
84
+ };
85
+ }
86
+
87
+ createTrajectories(stains: BloodstainInput[]): TrajectoryLine[] {
88
+ return stains.map((stain) => this.createTrajectory(stain));
89
+ }
90
+ }
91
+
92
+ export class ConvergenceResolver {
93
+ resolve(lines: TrajectoryLine[]): OriginEstimate {
94
+ if (lines.length === 0) {
95
+ return { point: { x: 0, y: 0, z: 0 }, spreadCm: 0, confidence: 'low', lines };
96
+ }
97
+
98
+ let estimate = lines.reduce<Vector3>((sum, line) => add(sum, line.originPoint), { x: 0, y: 0, z: 80 });
99
+ estimate = scale(estimate, 1 / lines.length);
100
+
101
+ for (let iteration = 0; iteration < 16; iteration++) {
102
+ let next = { x: 0, y: 0, z: 0 };
103
+ for (const line of lines) {
104
+ next = add(next, this.closestPointOnLine(estimate, line));
105
+ }
106
+ estimate = scale(next, 1 / lines.length);
107
+ }
108
+
109
+ const distances = lines.map((line) => distance(estimate, this.closestPointOnLine(estimate, line)));
110
+ const spreadCm = distances.reduce((sum, item) => sum + item, 0) / Math.max(distances.length, 1);
111
+
112
+ return {
113
+ point: estimate,
114
+ spreadCm,
115
+ confidence: this.confidenceFromSpread(spreadCm),
116
+ lines,
117
+ };
118
+ }
119
+
120
+ private confidenceFromSpread(spreadCm: number): OriginEstimate['confidence'] {
121
+ if (spreadCm < 8) return 'high';
122
+ if (spreadCm < 20) return 'medium';
123
+ return 'low';
124
+ }
125
+
126
+ private closestPointOnLine(point: Vector3, line: TrajectoryLine): Vector3 {
127
+ const offset = subtract(point, line.originPoint);
128
+ const t = dot(offset, line.direction);
129
+ return add(line.originPoint, scale(line.direction, Math.max(0, t)));
130
+ }
131
+ }
132
+
133
+ export function analyzeBloodstains(stains: BloodstainInput[]): OriginEstimate {
134
+ const calculator = new BloodstainCalculator();
135
+ const resolver = new ConvergenceResolver();
136
+ return resolver.resolve(calculator.createTrajectories(stains));
137
+ }
@@ -0,0 +1,10 @@
1
+ ---
2
+ import { SEORenderer } from '@jjlmoya/utils-shared';
3
+ import { bloodstainPatternOriginAnalyzer } from './index';
4
+ import type { KnownLocale } from '../../types';
5
+ interface Props { locale?: KnownLocale; }
6
+ const { locale = 'en' } = Astro.props;
7
+ const content = await bloodstainPatternOriginAnalyzer.i18n[locale]?.();
8
+ if (!content) return null;
9
+ ---
10
+ {content.seo.length > 0 && <SEORenderer content={{ locale, sections: content.seo }} />}
package/src/tools.ts CHANGED
@@ -11,6 +11,7 @@ import { FORENSIC_TLC_INK_SIMULATOR_TOOL } from './tool/forensic-tlc-ink-simulat
11
11
  import { FORENSIC_MICROCRYSTAL_DRUG_SIMULATOR_TOOL } from './tool/forensic-microcrystal-drug-simulator/index';
12
12
  import { FORENSIC_GLASS_BECKE_LINE_SIMULATOR_TOOL } from './tool/forensic-glass-becke-line-simulator/index';
13
13
  import { FORENSIC_FIBER_COMPARISON_MICROSCOPE_TOOL } from './tool/forensic-fiber-comparison-microscope/index';
14
+ import { BLOODSTAIN_PATTERN_ORIGIN_ANALYZER_TOOL } from './tool/bloodstain-pattern-origin-analyzer/index';
14
15
 
15
16
  export const ALL_TOOLS: ToolDefinition[] = [
16
17
  FORENSIC_AGE_ESTIMATOR_TOOL,
@@ -23,5 +24,6 @@ export const ALL_TOOLS: ToolDefinition[] = [
23
24
  FORENSIC_TLC_INK_SIMULATOR_TOOL,
24
25
  FORENSIC_MICROCRYSTAL_DRUG_SIMULATOR_TOOL,
25
26
  FORENSIC_GLASS_BECKE_LINE_SIMULATOR_TOOL,
26
- FORENSIC_FIBER_COMPARISON_MICROSCOPE_TOOL
27
+ FORENSIC_FIBER_COMPARISON_MICROSCOPE_TOOL,
28
+ BLOODSTAIN_PATTERN_ORIGIN_ANALYZER_TOOL
27
29
  ];