@jjlmoya/utils-forensic-science 1.5.0 → 1.7.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.
- package/package.json +4 -2
- package/src/category/index.ts +3 -1
- package/src/entries.ts +5 -1
- package/src/index.ts +1 -0
- package/src/layouts/PreviewLayout.astro +7 -2
- package/src/tests/locale_completeness.test.ts +2 -2
- package/src/tests/pagespeed_best_practices.test.ts +198 -0
- package/src/tests/tool_validation.test.ts +2 -2
- package/src/tool/bloodstain-pattern-origin-analyzer/component.astro +39 -39
- package/src/tool/forensic-blood-test-simulator/dom-utils.ts +3 -3
- package/src/tool/forensic-fiber-comparison-microscope/component.astro +2 -2
- package/src/tool/forensic-fiber-comparison-microscope/view.ts +3 -2
- package/src/tool/forensic-fingerprint-minutiae-identifier/component.astro +8 -4
- package/src/tool/forensic-sex-determinator/component.astro +1 -1
- package/src/tool/forensic-stature-estimator/components/OsteometricSelector.astro +1 -1
- package/src/tool/forensic-toolmark-striation-matcher/bibliography.astro +6 -0
- package/src/tool/forensic-toolmark-striation-matcher/bibliography.ts +16 -0
- package/src/tool/forensic-toolmark-striation-matcher/component.astro +121 -0
- package/src/tool/forensic-toolmark-striation-matcher/entry.ts +32 -0
- package/src/tool/forensic-toolmark-striation-matcher/forensic-toolmark-striation-matcher.css +580 -0
- package/src/tool/forensic-toolmark-striation-matcher/i18n/de.ts +301 -0
- package/src/tool/forensic-toolmark-striation-matcher/i18n/en.ts +301 -0
- package/src/tool/forensic-toolmark-striation-matcher/i18n/es.ts +301 -0
- package/src/tool/forensic-toolmark-striation-matcher/i18n/fr.ts +301 -0
- package/src/tool/forensic-toolmark-striation-matcher/i18n/id.ts +301 -0
- package/src/tool/forensic-toolmark-striation-matcher/i18n/it.ts +301 -0
- package/src/tool/forensic-toolmark-striation-matcher/i18n/ja.ts +301 -0
- package/src/tool/forensic-toolmark-striation-matcher/i18n/ko.ts +301 -0
- package/src/tool/forensic-toolmark-striation-matcher/i18n/nl.ts +301 -0
- package/src/tool/forensic-toolmark-striation-matcher/i18n/pl.ts +301 -0
- package/src/tool/forensic-toolmark-striation-matcher/i18n/pt.ts +301 -0
- package/src/tool/forensic-toolmark-striation-matcher/i18n/ru.ts +301 -0
- package/src/tool/forensic-toolmark-striation-matcher/i18n/sv.ts +301 -0
- package/src/tool/forensic-toolmark-striation-matcher/i18n/tr.ts +301 -0
- package/src/tool/forensic-toolmark-striation-matcher/i18n/zh.ts +301 -0
- package/src/tool/forensic-toolmark-striation-matcher/index.ts +11 -0
- package/src/tool/forensic-toolmark-striation-matcher/logic.ts +71 -0
- package/src/tool/forensic-toolmark-striation-matcher/renderer.ts +237 -0
- package/src/tool/forensic-toolmark-striation-matcher/seo.astro +15 -0
- package/src/tool/forensic-toolmark-striation-matcher/view.ts +291 -0
- package/src/tool/widmark-alcohol-simulator/component.astro +6 -6
- package/src/tools.ts +3 -1
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import { StriationMatcher } from './logic';
|
|
2
|
+
import { renderToolmarkCanvas } from './renderer';
|
|
3
|
+
import type { ToolProfile } from './logic';
|
|
4
|
+
import type { UploadedSample } from './renderer';
|
|
5
|
+
|
|
6
|
+
type Ui = Record<string, string>;
|
|
7
|
+
type UnitSystem = 'metric' | 'imperial';
|
|
8
|
+
type NumberInput = HTMLInputElement | null;
|
|
9
|
+
|
|
10
|
+
const STORAGE_KEY = 'forensic-toolmark-striation-matcher-state';
|
|
11
|
+
const PROFILES: ToolProfile[] = ['screwdriver', 'prybar', 'boltCutter'];
|
|
12
|
+
const DEFAULTS = { offset: 34, rotation: 2.5, zoom: 100, contrast: 108, brightness: 100, split: 50 };
|
|
13
|
+
|
|
14
|
+
function byId<T extends HTMLElement>(id: string): T | null {
|
|
15
|
+
return document.getElementById(id) as T | null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function isToolProfile(value: unknown): value is ToolProfile {
|
|
19
|
+
return typeof value === 'string' && PROFILES.includes(value as ToolProfile);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function isDarkTheme(): boolean {
|
|
23
|
+
return document.documentElement.classList.contains('theme-dark') || document.body.classList.contains('theme-dark');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function readNumber(input: NumberInput, fallback: number): number {
|
|
27
|
+
return Number(input?.value ?? fallback);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function setInput(input: NumberInput, value: number): void {
|
|
31
|
+
if (input) input.value = String(value);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function bindInput(input: NumberInput, render: () => void): void {
|
|
35
|
+
input?.addEventListener('input', render);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function initToolmarkStriationMatcher(ui: Ui): void {
|
|
39
|
+
new ToolmarkView(ui).init();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
class ToolmarkView {
|
|
43
|
+
private readonly matcher = new StriationMatcher();
|
|
44
|
+
private readonly canvas = byId<HTMLCanvasElement>('toolmark-canvas');
|
|
45
|
+
private readonly root = document.querySelector<HTMLElement>('[data-toolmark-tool]');
|
|
46
|
+
private readonly splitInput = byId<HTMLInputElement>('toolmark-split');
|
|
47
|
+
private readonly offsetInput = byId<HTMLInputElement>('toolmark-offset');
|
|
48
|
+
private readonly rotationInput = byId<HTMLInputElement>('toolmark-rotation');
|
|
49
|
+
private readonly zoomInput = byId<HTMLInputElement>('toolmark-zoom');
|
|
50
|
+
private readonly contrastInput = byId<HTMLInputElement>('toolmark-contrast');
|
|
51
|
+
private readonly brightnessInput = byId<HTMLInputElement>('toolmark-brightness');
|
|
52
|
+
private profile: ToolProfile = 'screwdriver';
|
|
53
|
+
private unitSystem: UnitSystem = 'metric';
|
|
54
|
+
private knownSample: UploadedSample | null = null;
|
|
55
|
+
private questionedSample: UploadedSample | null = null;
|
|
56
|
+
private gridEnabled = true;
|
|
57
|
+
|
|
58
|
+
constructor(private readonly ui: Ui) {}
|
|
59
|
+
|
|
60
|
+
init(): void {
|
|
61
|
+
this.restoreState();
|
|
62
|
+
this.bindEvents();
|
|
63
|
+
this.updateButtons();
|
|
64
|
+
this.render();
|
|
65
|
+
new MutationObserver(() => this.render()).observe(document.documentElement, { attributes: true, attributeFilter: ['class'] });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
private bindEvents(): void {
|
|
69
|
+
[this.splitInput, this.offsetInput, this.rotationInput, this.zoomInput, this.contrastInput, this.brightnessInput].forEach((input) => bindInput(input, () => this.render()));
|
|
70
|
+
byId<HTMLInputElement>('toolmark-known-file')?.addEventListener('change', (event) => this.loadSample(event, 'known'));
|
|
71
|
+
byId<HTMLInputElement>('toolmark-questioned-file')?.addEventListener('change', (event) => this.loadSample(event, 'questioned'));
|
|
72
|
+
byId<HTMLButtonElement>('toolmark-reset')?.addEventListener('click', () => this.resetAlignment());
|
|
73
|
+
byId<HTMLButtonElement>('toolmark-grid')?.addEventListener('click', () => this.toggleGrid());
|
|
74
|
+
byId<HTMLButtonElement>('toolmark-export')?.addEventListener('click', () => this.exportCanvas());
|
|
75
|
+
this.bindProfileButtons();
|
|
76
|
+
this.bindUnitButtons();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
private bindProfileButtons(): void {
|
|
80
|
+
document.querySelectorAll<HTMLButtonElement>('[data-toolmark-profile]').forEach((button) => {
|
|
81
|
+
button.addEventListener('click', () => this.chooseProfile(button.dataset.toolmarkProfile));
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private bindUnitButtons(): void {
|
|
86
|
+
document.querySelectorAll<HTMLButtonElement>('[data-toolmark-unit]').forEach((button) => {
|
|
87
|
+
button.addEventListener('click', () => this.chooseUnit(button.dataset.toolmarkUnit));
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
private chooseProfile(value: unknown): void {
|
|
92
|
+
if (!isToolProfile(value)) return;
|
|
93
|
+
this.profile = value;
|
|
94
|
+
this.updateButtons();
|
|
95
|
+
this.render();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
private chooseUnit(value: unknown): void {
|
|
99
|
+
this.unitSystem = value === 'imperial' ? 'imperial' : 'metric';
|
|
100
|
+
this.updateButtons();
|
|
101
|
+
this.render();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
private resetAlignment(): void {
|
|
105
|
+
setInput(this.offsetInput, 0);
|
|
106
|
+
setInput(this.rotationInput, 0);
|
|
107
|
+
setInput(this.zoomInput, DEFAULTS.zoom);
|
|
108
|
+
setInput(this.contrastInput, DEFAULTS.contrast);
|
|
109
|
+
setInput(this.brightnessInput, DEFAULTS.brightness);
|
|
110
|
+
setInput(this.splitInput, DEFAULTS.split);
|
|
111
|
+
this.render();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
private toggleGrid(): void {
|
|
115
|
+
this.gridEnabled = !this.gridEnabled;
|
|
116
|
+
this.updateButtons();
|
|
117
|
+
this.render();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private restoreState(): void {
|
|
121
|
+
const saved = this.readSavedState();
|
|
122
|
+
if (isToolProfile(saved.profile)) this.profile = saved.profile;
|
|
123
|
+
if (saved.unitSystem === 'metric' || saved.unitSystem === 'imperial') this.unitSystem = saved.unitSystem;
|
|
124
|
+
this.restoreNumberInputs(saved);
|
|
125
|
+
if (typeof saved.gridEnabled === 'boolean') this.gridEnabled = saved.gridEnabled;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
private readSavedState(): Record<string, unknown> {
|
|
129
|
+
try {
|
|
130
|
+
return JSON.parse(localStorage.getItem(STORAGE_KEY) ?? '{}') as Record<string, unknown>;
|
|
131
|
+
} catch {
|
|
132
|
+
localStorage.removeItem(STORAGE_KEY);
|
|
133
|
+
return {};
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private restoreNumberInputs(saved: Record<string, unknown>): void {
|
|
138
|
+
this.restoreNumber(saved.offset, this.offsetInput);
|
|
139
|
+
this.restoreNumber(saved.rotation, this.rotationInput);
|
|
140
|
+
this.restoreNumber(saved.zoom, this.zoomInput);
|
|
141
|
+
this.restoreNumber(saved.contrast, this.contrastInput);
|
|
142
|
+
this.restoreNumber(saved.brightness, this.brightnessInput);
|
|
143
|
+
this.restoreNumber(saved.split, this.splitInput);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
private restoreNumber(value: unknown, input: NumberInput): void {
|
|
147
|
+
if (typeof value === 'number') setInput(input, value);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
private saveState(): void {
|
|
151
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(this.currentState()));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
private currentState(): Record<string, unknown> {
|
|
155
|
+
return {
|
|
156
|
+
profile: this.profile,
|
|
157
|
+
unitSystem: this.unitSystem,
|
|
158
|
+
offset: readNumber(this.offsetInput, DEFAULTS.offset),
|
|
159
|
+
rotation: readNumber(this.rotationInput, DEFAULTS.rotation),
|
|
160
|
+
zoom: readNumber(this.zoomInput, DEFAULTS.zoom),
|
|
161
|
+
contrast: readNumber(this.contrastInput, DEFAULTS.contrast),
|
|
162
|
+
brightness: readNumber(this.brightnessInput, DEFAULTS.brightness),
|
|
163
|
+
split: readNumber(this.splitInput, DEFAULTS.split),
|
|
164
|
+
gridEnabled: this.gridEnabled,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
private updateButtons(): void {
|
|
169
|
+
this.updateProfileButtons();
|
|
170
|
+
document.querySelectorAll<HTMLButtonElement>('[data-toolmark-unit]').forEach((button) => {
|
|
171
|
+
button.dataset.active = button.dataset.toolmarkUnit === this.unitSystem ? 'true' : 'false';
|
|
172
|
+
});
|
|
173
|
+
const gridButton = byId<HTMLButtonElement>('toolmark-grid');
|
|
174
|
+
if (gridButton) gridButton.dataset.active = this.gridEnabled ? 'true' : 'false';
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
private updateProfileButtons(): void {
|
|
178
|
+
document.querySelectorAll<HTMLButtonElement>('[data-toolmark-profile]').forEach((button) => {
|
|
179
|
+
button.dataset.active = button.dataset.toolmarkProfile === this.profile ? 'true' : 'false';
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
private render(): void {
|
|
184
|
+
const result = this.matcher.compare({ profile: this.profile, offsetMicrons: this.offset(), rotationDegrees: this.rotation() });
|
|
185
|
+
this.renderCanvas(result);
|
|
186
|
+
this.root?.style.setProperty('--toolmark-split', `${readNumber(this.splitInput, DEFAULTS.split)}%`);
|
|
187
|
+
this.updateLabels(result);
|
|
188
|
+
this.saveState();
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
private renderCanvas(result: ReturnType<StriationMatcher['compare']>): void {
|
|
192
|
+
if (!this.canvas) return;
|
|
193
|
+
renderToolmarkCanvas(this.canvas, {
|
|
194
|
+
result,
|
|
195
|
+
knownSample: this.knownSample,
|
|
196
|
+
questionedSample: this.questionedSample,
|
|
197
|
+
splitPercent: readNumber(this.splitInput, DEFAULTS.split),
|
|
198
|
+
zoomPercent: readNumber(this.zoomInput, DEFAULTS.zoom),
|
|
199
|
+
contrastPercent: readNumber(this.contrastInput, DEFAULTS.contrast),
|
|
200
|
+
brightnessPercent: readNumber(this.brightnessInput, DEFAULTS.brightness),
|
|
201
|
+
gridEnabled: this.gridEnabled,
|
|
202
|
+
dark: isDarkTheme(),
|
|
203
|
+
reliefGraphLabel: this.ui.reliefGraph,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
private updateLabels(result: ReturnType<StriationMatcher['compare']>): void {
|
|
208
|
+
const visualMode = this.hasUploadedImages();
|
|
209
|
+
this.setText('toolmark-correlation', visualMode ? this.ui.visualMode : `${result.correlation}%`);
|
|
210
|
+
this.setText('toolmark-verdict', this.verdictLabel(result.correlation, visualMode));
|
|
211
|
+
this.setText('toolmark-offset-label', this.offsetLabel(result));
|
|
212
|
+
this.setText('toolmark-rotation-label', `${result.rotationDegrees.toFixed(1)}${this.ui.degrees}`);
|
|
213
|
+
this.setText('toolmark-zoom-label', `${readNumber(this.zoomInput, DEFAULTS.zoom)}%`);
|
|
214
|
+
this.setText('toolmark-interpretation', visualMode ? this.ui.visualInterpretation : '');
|
|
215
|
+
this.setText('toolmark-phase-label', `${result.phaseScore}%`);
|
|
216
|
+
this.setText('toolmark-rotation-fit-label', `${result.rotationScore}%`);
|
|
217
|
+
this.setText('toolmark-known-name', this.knownSample?.name ?? this.ui.noFile);
|
|
218
|
+
this.setText('toolmark-questioned-name', this.questionedSample?.name ?? this.ui.noFile);
|
|
219
|
+
this.updateMeters(result);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
private verdictLabel(correlation: number, visualMode: boolean): string {
|
|
223
|
+
if (visualMode) return this.ui.visualVerdict;
|
|
224
|
+
if (correlation >= 82) return this.ui.verdictStrong;
|
|
225
|
+
if (correlation >= 58) return this.ui.verdictPartial;
|
|
226
|
+
return this.ui.verdictWeak;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
private offsetLabel(result: ReturnType<StriationMatcher['compare']>): string {
|
|
230
|
+
if (this.unitSystem === 'imperial') return `${result.offsetThousandths.toFixed(2)} ${this.ui.thousandths}`;
|
|
231
|
+
return `${result.offsetMicrons.toFixed(0)} ${this.ui.microns}`;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
private updateMeters(result: ReturnType<StriationMatcher['compare']>): void {
|
|
235
|
+
const phase = byId<HTMLMeterElement>('toolmark-phase');
|
|
236
|
+
const rotation = byId<HTMLMeterElement>('toolmark-rotation-fit');
|
|
237
|
+
if (phase) phase.value = result.phaseScore;
|
|
238
|
+
if (rotation) rotation.value = result.rotationScore;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
private setText(id: string, value: string): void {
|
|
242
|
+
const element = byId(id);
|
|
243
|
+
if (element) element.textContent = value;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
private hasUploadedImages(): boolean {
|
|
247
|
+
return Boolean(this.knownSample || this.questionedSample);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
private loadSample(event: Event, kind: 'known' | 'questioned'): void {
|
|
251
|
+
const input = event.currentTarget as HTMLInputElement;
|
|
252
|
+
const file = input.files?.[0];
|
|
253
|
+
if (!file || !file.type.startsWith('image/')) return;
|
|
254
|
+
this.readImage(file, (sample) => this.setSample(kind, sample));
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
private readImage(file: File, callback: (sample: UploadedSample) => void): void {
|
|
258
|
+
const reader = new FileReader();
|
|
259
|
+
reader.addEventListener('load', () => this.createImage(String(reader.result ?? ''), file.name, callback));
|
|
260
|
+
reader.readAsDataURL(file);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
private createImage(src: string, name: string, callback: (sample: UploadedSample) => void): void {
|
|
264
|
+
const image = new Image();
|
|
265
|
+
image.addEventListener('load', () => callback({ image, name }));
|
|
266
|
+
image.src = src;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
private setSample(kind: 'known' | 'questioned', sample: UploadedSample): void {
|
|
270
|
+
if (kind === 'known') this.knownSample = sample;
|
|
271
|
+
else this.questionedSample = sample;
|
|
272
|
+
this.render();
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
private exportCanvas(): void {
|
|
276
|
+
if (!this.canvas) return;
|
|
277
|
+
const link = document.createElement('a');
|
|
278
|
+
link.download = this.ui.exportFilename;
|
|
279
|
+
link.href = this.canvas.toDataURL('image/png');
|
|
280
|
+
link.click();
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
private offset(): number {
|
|
284
|
+
return readNumber(this.offsetInput, DEFAULTS.offset);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
private rotation(): number {
|
|
288
|
+
return readNumber(this.rotationInput, DEFAULTS.rotation);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
}
|
|
@@ -16,7 +16,7 @@ const { ui } = Astro.props;
|
|
|
16
16
|
|
|
17
17
|
<div class="widmark-field">
|
|
18
18
|
<span>{ui.weight}</span>
|
|
19
|
-
<input type="number" id="widmark-weight" class="widmark-input" min="30" max="250" value="80" />
|
|
19
|
+
<input type="number" id="widmark-weight" class="widmark-input" min="30" max="250" value="80" aria-label={ui.weight} />
|
|
20
20
|
</div>
|
|
21
21
|
|
|
22
22
|
<div class="widmark-field">
|
|
@@ -35,7 +35,7 @@ const { ui } = Astro.props;
|
|
|
35
35
|
|
|
36
36
|
<div class="widmark-field">
|
|
37
37
|
<span>{ui.hydration}</span>
|
|
38
|
-
<select id="widmark-hydration" class="widmark-select">
|
|
38
|
+
<select id="widmark-hydration" class="widmark-select" aria-label={ui.hydration}>
|
|
39
39
|
<option value="low">{ui.hydrationLow}</option>
|
|
40
40
|
<option value="normal" selected>{ui.hydrationNormal}</option>
|
|
41
41
|
<option value="high">{ui.hydrationHigh}</option>
|
|
@@ -44,7 +44,7 @@ const { ui } = Astro.props;
|
|
|
44
44
|
|
|
45
45
|
<div class="widmark-field">
|
|
46
46
|
<span>{ui.stomachState}</span>
|
|
47
|
-
<select id="widmark-stomach" class="widmark-select">
|
|
47
|
+
<select id="widmark-stomach" class="widmark-select" aria-label={ui.stomachState}>
|
|
48
48
|
<option value="empty">{ui.stomachEmpty}</option>
|
|
49
49
|
<option value="light" selected>{ui.stomachLight}</option>
|
|
50
50
|
<option value="full">{ui.stomachFull}</option>
|
|
@@ -64,15 +64,15 @@ const { ui } = Astro.props;
|
|
|
64
64
|
<div class="widmark-drink-form-grid">
|
|
65
65
|
<div class="widmark-field">
|
|
66
66
|
<span>{ui.drinkVolume}</span>
|
|
67
|
-
<input type="number" id="widmark-drink-vol" class="widmark-input" value="330" min="10" max="2000" />
|
|
67
|
+
<input type="number" id="widmark-drink-vol" class="widmark-input" value="330" min="10" max="2000" aria-label={ui.drinkVolume} />
|
|
68
68
|
</div>
|
|
69
69
|
<div class="widmark-field">
|
|
70
70
|
<span>{ui.drinkAbv}</span>
|
|
71
|
-
<input type="number" id="widmark-drink-abv" class="widmark-input" value="5" min="0.1" max="100" step="0.1" />
|
|
71
|
+
<input type="number" id="widmark-drink-abv" class="widmark-input" value="5" min="0.1" max="100" step="0.1" aria-label={ui.drinkAbv} />
|
|
72
72
|
</div>
|
|
73
73
|
<div class="widmark-field">
|
|
74
74
|
<span>{ui.drinkTime}</span>
|
|
75
|
-
<input type="number" id="widmark-drink-time" class="widmark-input" value="0" min="0" max="11.9" step="0.1" />
|
|
75
|
+
<input type="number" id="widmark-drink-time" class="widmark-input" value="0" min="0" max="11.9" step="0.1" aria-label={ui.drinkTime} />
|
|
76
76
|
</div>
|
|
77
77
|
<button type="button" id="widmark-add-drink" class="widmark-btn-primary">
|
|
78
78
|
<span>{ui.addDrink}</span>
|
package/src/tools.ts
CHANGED
|
@@ -14,6 +14,7 @@ import { FORENSIC_FIBER_COMPARISON_MICROSCOPE_TOOL } from './tool/forensic-fiber
|
|
|
14
14
|
import { BLOODSTAIN_PATTERN_ORIGIN_ANALYZER_TOOL } from './tool/bloodstain-pattern-origin-analyzer/index';
|
|
15
15
|
import { FORENSIC_FINGERPRINT_MINUTIAE_IDENTIFIER_TOOL } from './tool/forensic-fingerprint-minutiae-identifier/index';
|
|
16
16
|
import { FIRE_PATTERN_ORIGIN_ANALYZER_TOOL } from './tool/fire-pattern-origin-analyzer/index';
|
|
17
|
+
import { FORENSIC_TOOLMARK_STRIATION_MATCHER_TOOL } from './tool/forensic-toolmark-striation-matcher/index';
|
|
17
18
|
|
|
18
19
|
export const ALL_TOOLS: ToolDefinition[] = [
|
|
19
20
|
FORENSIC_AGE_ESTIMATOR_TOOL,
|
|
@@ -29,5 +30,6 @@ export const ALL_TOOLS: ToolDefinition[] = [
|
|
|
29
30
|
FORENSIC_FIBER_COMPARISON_MICROSCOPE_TOOL,
|
|
30
31
|
BLOODSTAIN_PATTERN_ORIGIN_ANALYZER_TOOL,
|
|
31
32
|
FORENSIC_FINGERPRINT_MINUTIAE_IDENTIFIER_TOOL,
|
|
32
|
-
FIRE_PATTERN_ORIGIN_ANALYZER_TOOL
|
|
33
|
+
FIRE_PATTERN_ORIGIN_ANALYZER_TOOL,
|
|
34
|
+
FORENSIC_TOOLMARK_STRIATION_MATCHER_TOOL
|
|
33
35
|
];
|