@jjlmoya/utils-forensic-science 1.3.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/package.json +1 -1
  2. package/src/category/index.ts +5 -1
  3. package/src/entries.ts +9 -1
  4. package/src/index.ts +2 -0
  5. package/src/tests/locale_completeness.test.ts +2 -2
  6. package/src/tests/tool_validation.test.ts +2 -2
  7. package/src/tool/fire-pattern-origin-analyzer/bibliography.astro +6 -0
  8. package/src/tool/fire-pattern-origin-analyzer/bibliography.ts +12 -0
  9. package/src/tool/fire-pattern-origin-analyzer/component.astro +196 -0
  10. package/src/tool/fire-pattern-origin-analyzer/entry.ts +32 -0
  11. package/src/tool/fire-pattern-origin-analyzer/fire-pattern-origin-analyzer.css +681 -0
  12. package/src/tool/fire-pattern-origin-analyzer/i18n/de.ts +250 -0
  13. package/src/tool/fire-pattern-origin-analyzer/i18n/en.ts +250 -0
  14. package/src/tool/fire-pattern-origin-analyzer/i18n/es.ts +250 -0
  15. package/src/tool/fire-pattern-origin-analyzer/i18n/fr.ts +250 -0
  16. package/src/tool/fire-pattern-origin-analyzer/i18n/id.ts +250 -0
  17. package/src/tool/fire-pattern-origin-analyzer/i18n/it.ts +250 -0
  18. package/src/tool/fire-pattern-origin-analyzer/i18n/ja.ts +250 -0
  19. package/src/tool/fire-pattern-origin-analyzer/i18n/ko.ts +250 -0
  20. package/src/tool/fire-pattern-origin-analyzer/i18n/nl.ts +250 -0
  21. package/src/tool/fire-pattern-origin-analyzer/i18n/pl.ts +250 -0
  22. package/src/tool/fire-pattern-origin-analyzer/i18n/pt.ts +250 -0
  23. package/src/tool/fire-pattern-origin-analyzer/i18n/ru.ts +250 -0
  24. package/src/tool/fire-pattern-origin-analyzer/i18n/sv.ts +250 -0
  25. package/src/tool/fire-pattern-origin-analyzer/i18n/tr.ts +250 -0
  26. package/src/tool/fire-pattern-origin-analyzer/i18n/zh.ts +246 -0
  27. package/src/tool/fire-pattern-origin-analyzer/index.ts +11 -0
  28. package/src/tool/fire-pattern-origin-analyzer/logic.ts +123 -0
  29. package/src/tool/fire-pattern-origin-analyzer/render.ts +159 -0
  30. package/src/tool/fire-pattern-origin-analyzer/seo.astro +15 -0
  31. package/src/tool/fire-pattern-origin-analyzer/view-bindings.ts +171 -0
  32. package/src/tool/fire-pattern-origin-analyzer/view-model.ts +244 -0
  33. package/src/tool/fire-pattern-origin-analyzer/view.ts +30 -0
  34. package/src/tool/forensic-fingerprint-minutiae-identifier/bibliography.astro +6 -0
  35. package/src/tool/forensic-fingerprint-minutiae-identifier/bibliography.ts +16 -0
  36. package/src/tool/forensic-fingerprint-minutiae-identifier/component.astro +429 -0
  37. package/src/tool/forensic-fingerprint-minutiae-identifier/drawer.ts +94 -0
  38. package/src/tool/forensic-fingerprint-minutiae-identifier/entry.ts +32 -0
  39. package/src/tool/forensic-fingerprint-minutiae-identifier/forensic-fingerprint-minutiae-identifier.css +969 -0
  40. package/src/tool/forensic-fingerprint-minutiae-identifier/i18n/de.ts +298 -0
  41. package/src/tool/forensic-fingerprint-minutiae-identifier/i18n/en.ts +289 -0
  42. package/src/tool/forensic-fingerprint-minutiae-identifier/i18n/es.ts +291 -0
  43. package/src/tool/forensic-fingerprint-minutiae-identifier/i18n/fr.ts +298 -0
  44. package/src/tool/forensic-fingerprint-minutiae-identifier/i18n/id.ts +298 -0
  45. package/src/tool/forensic-fingerprint-minutiae-identifier/i18n/it.ts +298 -0
  46. package/src/tool/forensic-fingerprint-minutiae-identifier/i18n/ja.ts +281 -0
  47. package/src/tool/forensic-fingerprint-minutiae-identifier/i18n/ko.ts +281 -0
  48. package/src/tool/forensic-fingerprint-minutiae-identifier/i18n/nl.ts +298 -0
  49. package/src/tool/forensic-fingerprint-minutiae-identifier/i18n/pl.ts +298 -0
  50. package/src/tool/forensic-fingerprint-minutiae-identifier/i18n/pt.ts +298 -0
  51. package/src/tool/forensic-fingerprint-minutiae-identifier/i18n/ru.ts +298 -0
  52. package/src/tool/forensic-fingerprint-minutiae-identifier/i18n/sv.ts +298 -0
  53. package/src/tool/forensic-fingerprint-minutiae-identifier/i18n/tr.ts +298 -0
  54. package/src/tool/forensic-fingerprint-minutiae-identifier/i18n/zh.ts +281 -0
  55. package/src/tool/forensic-fingerprint-minutiae-identifier/index.ts +11 -0
  56. package/src/tool/forensic-fingerprint-minutiae-identifier/logic.ts +168 -0
  57. package/src/tool/forensic-fingerprint-minutiae-identifier/renderer.ts +116 -0
  58. package/src/tool/forensic-fingerprint-minutiae-identifier/seo.astro +15 -0
  59. package/src/tools.ts +5 -1
@@ -0,0 +1,171 @@
1
+ import type { FireOriginEstimator, FireVector } from './logic';
2
+ import { drawHeatmap, drawOriginMarker, drawVector, pointFromPointer, snapPoint, WIDTH, HEIGHT } from './render';
3
+ import {
4
+ animateToState,
5
+ closeRadialMenu,
6
+ commitDraft,
7
+ exportReport,
8
+ filteredVectors,
9
+ interpolatedState,
10
+ loadPlanFile,
11
+ openRadialMenu,
12
+ syncLayers,
13
+ type Pattern,
14
+ type ViewRefs,
15
+ type ViewState,
16
+ updateOriginOverlay,
17
+ updatePointerOverlay,
18
+ } from './view-model';
19
+
20
+ export function bindLayerToggles(refs: ViewRefs, estimator: FireOriginEstimator, state: ViewState): void {
21
+ refs.root.querySelectorAll<HTMLButtonElement>('[data-fire-layer-toggle]').forEach((button) => {
22
+ button.addEventListener('click', () => {
23
+ const pattern = button.dataset.fireLayerToggle as Pattern;
24
+ state.visibleLayers[pattern] = !state.visibleLayers[pattern];
25
+ syncLayers(refs, state);
26
+ animateToState(refs, estimator, state);
27
+ });
28
+ });
29
+ }
30
+
31
+ export function bindRadialMenu(refs: ViewRefs, state: ViewState, ui: Record<string, string>): void {
32
+ refs.radialMenu.querySelectorAll<HTMLButtonElement>('[data-fire-pattern]').forEach((button) => {
33
+ button.addEventListener('click', () => {
34
+ if (!state.pendingStart) return;
35
+ state.currentPattern = button.dataset.firePattern as Pattern;
36
+ state.draftVector = {
37
+ id: `vector-${Date.now()}`,
38
+ start: state.pendingStart,
39
+ end: state.pendingStart,
40
+ pattern: state.currentPattern,
41
+ confidence: 78,
42
+ };
43
+ closeRadialMenu(refs);
44
+ refs.modeLabel.textContent = `${ui.refiningVector}: ${ui[state.currentPattern] || state.currentPattern}`;
45
+ });
46
+ });
47
+ }
48
+
49
+ function handlePointerMove(refs: ViewRefs, state: ViewState, event: PointerEvent): void {
50
+ state.pointer = pointFromPointer(refs.svg, event);
51
+ state.pointerClient = { x: event.offsetX, y: event.offsetY };
52
+ updatePointerOverlay(refs, state);
53
+ if (state.draftVector && state.pendingStart) {
54
+ state.draftVector = { ...state.draftVector, end: snapPoint(state.pendingStart, state.pointer) };
55
+ }
56
+ }
57
+
58
+ export function bindCanvasInteractions(
59
+ refs: ViewRefs,
60
+ estimator: FireOriginEstimator,
61
+ state: ViewState,
62
+ ui: Record<string, string>,
63
+ ): void {
64
+ refs.svg.addEventListener('pointermove', (event) => {
65
+ handlePointerMove(refs, state, event);
66
+ });
67
+ refs.svg.addEventListener('pointerdown', (event) => {
68
+ state.pointer = pointFromPointer(refs.svg, event);
69
+ state.pointerClient = { x: event.offsetX, y: event.offsetY };
70
+ updatePointerOverlay(refs, state);
71
+ if (state.draftVector) {
72
+ commitDraft(refs, estimator, state, ui);
73
+ return;
74
+ }
75
+ openRadialMenu(refs, state, ui, state.pointer);
76
+ });
77
+ refs.svg.addEventListener('pointerleave', () => {
78
+ refs.pointerOverlay.style.opacity = '0';
79
+ });
80
+ refs.svg.addEventListener('pointerenter', () => {
81
+ refs.pointerOverlay.style.opacity = '1';
82
+ });
83
+ }
84
+
85
+ export function bindShortcuts(refs: ViewRefs, estimator: FireOriginEstimator, state: ViewState, ui: Record<string, string>): void {
86
+ const shortcutMap: Record<string, Pattern> = { v: 'vPattern', d: 'deepChar', s: 'sootShadow', c: 'cleanBurn' };
87
+
88
+ document.addEventListener('keydown', (event) => {
89
+ if (event.key === 'Escape') {
90
+ state.draftVector = null;
91
+ state.pendingStart = null;
92
+ closeRadialMenu(refs);
93
+ refs.modeLabel.textContent = ui.awaitingVector;
94
+ return;
95
+ }
96
+ const pattern = shortcutMap[event.key.toLowerCase()];
97
+ if (!state.hasLoadedPlan || !pattern) return;
98
+ state.visibleLayers[pattern] = !state.visibleLayers[pattern];
99
+ syncLayers(refs, state);
100
+ animateToState(refs, estimator, state);
101
+ });
102
+ }
103
+
104
+ export function bindUpload(refs: ViewRefs, estimator: FireOriginEstimator, state: ViewState): void {
105
+ refs.upload.addEventListener('change', () => {
106
+ const file = refs.upload.files?.[0];
107
+ if (file) loadPlanFile(refs, estimator, state, file);
108
+ });
109
+ refs.dropzone.addEventListener('click', () => {
110
+ refs.upload.click();
111
+ });
112
+ ['dragenter', 'dragover'].forEach((eventName) => {
113
+ refs.svg.addEventListener(eventName, (event) => {
114
+ event.preventDefault();
115
+ refs.dropzone.hidden = false;
116
+ });
117
+ });
118
+ ['dragleave', 'drop'].forEach((eventName) => {
119
+ refs.svg.addEventListener(eventName, (event) => {
120
+ event.preventDefault();
121
+ refs.dropzone.hidden = true;
122
+ });
123
+ });
124
+ refs.svg.addEventListener('drop', (event) => {
125
+ const file = event.dataTransfer?.files?.[0];
126
+ if (file) loadPlanFile(refs, estimator, state, file);
127
+ });
128
+ }
129
+
130
+ export function bindAuxiliaryActions(refs: ViewRefs, estimator: FireOriginEstimator, state: ViewState, ui: Record<string, string>): void {
131
+ document.addEventListener('click', (event) => {
132
+ const target = event.target as Node;
133
+ if (!refs.radialMenu.hidden && !refs.radialMenu.contains(target) && !refs.svg.contains(target)) {
134
+ closeRadialMenu(refs);
135
+ if (!state.draftVector) refs.modeLabel.textContent = ui.awaitingVector;
136
+ }
137
+ });
138
+ refs.exportButton.addEventListener('click', () => {
139
+ exportReport(state);
140
+ });
141
+ refs.resetButton.addEventListener('click', () => {
142
+ state.vectors = [];
143
+ state.draftVector = null;
144
+ state.pendingStart = null;
145
+ animateToState(refs, estimator, state);
146
+ refs.modeLabel.textContent = ui.awaitingVector;
147
+ });
148
+ }
149
+
150
+ export function startRenderLoop(refs: ViewRefs, state: ViewState): void {
151
+ const render = (now: number) => {
152
+ refs.ctx.clearRect(0, 0, WIDTH, HEIGHT);
153
+ if (state.hasLoadedPlan) {
154
+ const frame = interpolatedState(state, now);
155
+ filteredVectors(state).forEach((vector: FireVector) => drawVector(refs.ctx, vector, 0.96));
156
+ if (state.draftVector) drawVector(refs.ctx, state.draftVector, 0.52);
157
+ drawHeatmap(refs.ctx, {
158
+ point: frame.origin,
159
+ radius: frame.radius,
160
+ confidence: frame.confidence,
161
+ intersections: frame.intersections,
162
+ }, now);
163
+ drawOriginMarker(refs.ctx, frame.origin);
164
+ updateOriginOverlay(refs, frame.origin);
165
+ }
166
+ state.animationFrame = requestAnimationFrame(render);
167
+ };
168
+
169
+ state.animationFrame = requestAnimationFrame(render);
170
+ window.addEventListener('beforeunload', () => cancelAnimationFrame(state.animationFrame), { once: true });
171
+ }
@@ -0,0 +1,244 @@
1
+ import type { FireOriginEstimator, FirePoint, FireVector } from './logic';
2
+ import { HEIGHT, TRANSITION_MS, WIDTH, clamp, ease, lerp, patternColor, toCanvas } from './render';
3
+
4
+ export type Pattern = FireVector['pattern'];
5
+ type PatternState = Record<Pattern, boolean>;
6
+
7
+ export interface AnimatedState {
8
+ origin: FirePoint;
9
+ radius: number;
10
+ confidence: number;
11
+ intersections: FirePoint[];
12
+ vectorCount: number;
13
+ statusKey: 'needsMoreVectors' | 'wideArea' | 'focusedArea';
14
+ }
15
+
16
+ export interface ViewRefs {
17
+ root: HTMLElement;
18
+ svg: SVGSVGElement;
19
+ ctx: CanvasRenderingContext2D;
20
+ radialMenu: HTMLDivElement;
21
+ pointerOverlay: HTMLDivElement;
22
+ originOverlay: HTMLDivElement;
23
+ pointerCoordinate: HTMLElement;
24
+ originCoordinate: HTMLElement;
25
+ vectorCount: HTMLElement;
26
+ confidenceScore: HTMLElement;
27
+ variance: HTMLElement;
28
+ modeLabel: HTMLElement;
29
+ upload: HTMLInputElement;
30
+ exportButton: HTMLButtonElement;
31
+ resetButton: HTMLButtonElement;
32
+ dropzone: HTMLButtonElement;
33
+ planImage: SVGImageElement;
34
+ }
35
+
36
+ export interface ViewState {
37
+ visibleLayers: PatternState;
38
+ vectors: FireVector[];
39
+ pendingStart: FirePoint | null;
40
+ draftVector: FireVector | null;
41
+ pointer: FirePoint;
42
+ pointerClient: FirePoint;
43
+ hasLoadedPlan: boolean;
44
+ currentPattern: Pattern;
45
+ importedPlanName: string;
46
+ renderStart: number;
47
+ animationFrame: number;
48
+ previousState: AnimatedState | null;
49
+ currentState: AnimatedState;
50
+ }
51
+
52
+ const DEFAULT_STATE: AnimatedState = {
53
+ origin: { x: 50, y: 50 },
54
+ radius: 24,
55
+ confidence: 12,
56
+ intersections: [],
57
+ vectorCount: 0,
58
+ statusKey: 'needsMoreVectors',
59
+ };
60
+
61
+ function required<T>(value: T | null): T {
62
+ if (!value) throw new Error('Missing fire analyzer DOM node');
63
+ return value;
64
+ }
65
+
66
+ function download(name: string, content: string, type: string): void {
67
+ const blob = new Blob([content], { type });
68
+ const url = URL.createObjectURL(blob);
69
+ const anchor = document.createElement('a');
70
+ anchor.href = url;
71
+ anchor.download = name;
72
+ anchor.click();
73
+ setTimeout(() => URL.revokeObjectURL(url), 0);
74
+ }
75
+
76
+ export function getRefs(): ViewRefs | null {
77
+ try {
78
+ const canvas = required(document.getElementById('fire-overlay-canvas') as HTMLCanvasElement | null);
79
+ return {
80
+ root: required(document.querySelector<HTMLElement>('[data-fire-origin-tool]')),
81
+ svg: required(document.getElementById('fire-plan-svg') as SVGSVGElement | null),
82
+ ctx: required(canvas.getContext('2d')),
83
+ radialMenu: required(document.getElementById('fire-radial-menu') as HTMLDivElement | null),
84
+ pointerOverlay: required(document.getElementById('fire-pointer-overlay') as HTMLDivElement | null),
85
+ originOverlay: required(document.getElementById('fire-origin-overlay') as HTMLDivElement | null),
86
+ pointerCoordinate: required(document.getElementById('fire-pointer-coordinate')),
87
+ originCoordinate: required(document.getElementById('fire-origin-coordinate')),
88
+ vectorCount: required(document.getElementById('fire-vector-count')),
89
+ confidenceScore: required(document.getElementById('fire-confidence-score')),
90
+ variance: required(document.getElementById('fire-origin-variance')),
91
+ modeLabel: required(document.getElementById('fire-mode-label')),
92
+ upload: required(document.getElementById('fire-plan-upload') as HTMLInputElement | null),
93
+ exportButton: required(document.getElementById('fire-export') as HTMLButtonElement | null),
94
+ resetButton: required(document.getElementById('fire-reset') as HTMLButtonElement | null),
95
+ dropzone: required(document.getElementById('fire-dropzone') as HTMLButtonElement | null),
96
+ planImage: required(document.getElementById('fire-plan-image') as SVGImageElement | null),
97
+ };
98
+ } catch {
99
+ return null;
100
+ }
101
+ }
102
+
103
+ export function createState(): ViewState {
104
+ return {
105
+ visibleLayers: { vPattern: true, deepChar: true, sootShadow: true, cleanBurn: true },
106
+ vectors: [],
107
+ pendingStart: null,
108
+ draftVector: null,
109
+ pointer: { x: 50, y: 50 },
110
+ pointerClient: { x: 0, y: 0 },
111
+ hasLoadedPlan: false,
112
+ currentPattern: 'vPattern',
113
+ importedPlanName: 'room-plan',
114
+ renderStart: performance.now(),
115
+ animationFrame: 0,
116
+ previousState: null,
117
+ currentState: { ...DEFAULT_STATE },
118
+ };
119
+ }
120
+
121
+ export function filteredVectors(state: ViewState): FireVector[] {
122
+ if (!state.hasLoadedPlan) return [];
123
+ return state.vectors.filter((vector) => state.visibleLayers[vector.pattern]);
124
+ }
125
+
126
+ export function syncControls(refs: ViewRefs, enabled: boolean): void {
127
+ refs.resetButton.disabled = !enabled;
128
+ refs.exportButton.disabled = !enabled;
129
+ refs.root.dataset.hasPlan = enabled ? 'true' : 'false';
130
+ refs.pointerOverlay.hidden = !enabled;
131
+ refs.originOverlay.hidden = !enabled;
132
+ refs.root.querySelectorAll<HTMLButtonElement>('[data-fire-layer-toggle]').forEach((button) => {
133
+ button.disabled = !enabled;
134
+ });
135
+ }
136
+
137
+ export function syncLayers(refs: ViewRefs, state: ViewState): void {
138
+ (Object.keys(state.visibleLayers) as Pattern[]).forEach((pattern) => {
139
+ refs.root.querySelectorAll<HTMLElement>(`[data-fire-layer-toggle="${pattern}"]`).forEach((node) => {
140
+ node.dataset.active = state.visibleLayers[pattern] ? 'true' : 'false';
141
+ });
142
+ });
143
+ }
144
+
145
+ export function animateToState(refs: ViewRefs, estimator: FireOriginEstimator, state: ViewState): void {
146
+ state.previousState = state.currentState;
147
+ state.currentState = estimator.estimate(filteredVectors(state));
148
+ state.renderStart = performance.now();
149
+ refs.vectorCount.textContent = String(state.currentState.vectorCount);
150
+ refs.confidenceScore.textContent = `${state.currentState.confidence.toFixed(1)}%`;
151
+ refs.variance.textContent = `+-${(state.currentState.radius / 10).toFixed(1)} m`;
152
+ refs.originCoordinate.textContent = `${state.currentState.origin.x.toFixed(1)}, ${state.currentState.origin.y.toFixed(1)}`;
153
+ }
154
+
155
+ export function interpolatedState(state: ViewState, now: number): AnimatedState {
156
+ if (!state.previousState) return state.currentState;
157
+ const progress = ease(clamp((now - state.renderStart) / TRANSITION_MS, 0, 1));
158
+ return {
159
+ origin: {
160
+ x: lerp(state.previousState.origin.x, state.currentState.origin.x, progress),
161
+ y: lerp(state.previousState.origin.y, state.currentState.origin.y, progress),
162
+ },
163
+ radius: lerp(state.previousState.radius, state.currentState.radius, progress),
164
+ confidence: lerp(state.previousState.confidence, state.currentState.confidence, progress),
165
+ intersections: state.currentState.intersections,
166
+ vectorCount: state.currentState.vectorCount,
167
+ statusKey: state.currentState.statusKey,
168
+ };
169
+ }
170
+
171
+ export function updatePointerOverlay(refs: ViewRefs, state: ViewState): void {
172
+ refs.pointerCoordinate.textContent = `${state.pointer.x.toFixed(1)}, ${state.pointer.y.toFixed(1)}`;
173
+ refs.pointerOverlay.style.left = `${state.pointerClient.x + 18}px`;
174
+ refs.pointerOverlay.style.top = `${state.pointerClient.y - 24}px`;
175
+ }
176
+
177
+ export function updateOriginOverlay(refs: ViewRefs, point: FirePoint): void {
178
+ const screen = toCanvas(point);
179
+ refs.originOverlay.style.left = `${screen.x + 18}px`;
180
+ refs.originOverlay.style.top = `${screen.y + 18}px`;
181
+ }
182
+
183
+ export function closeRadialMenu(refs: ViewRefs): void {
184
+ refs.radialMenu.hidden = true;
185
+ }
186
+
187
+ export function openRadialMenu(refs: ViewRefs, state: ViewState, ui: Record<string, string>, point: FirePoint): void {
188
+ if (!state.hasLoadedPlan) return;
189
+ state.pendingStart = point;
190
+ const canvasPoint = toCanvas(point);
191
+ refs.radialMenu.style.left = `${canvasPoint.x}px`;
192
+ refs.radialMenu.style.top = `${canvasPoint.y}px`;
193
+ refs.radialMenu.hidden = false;
194
+ refs.modeLabel.textContent = ui.selectIndicator;
195
+ }
196
+
197
+ export function commitDraft(refs: ViewRefs, estimator: FireOriginEstimator, state: ViewState, ui: Record<string, string>): void {
198
+ if (!state.draftVector) return;
199
+ state.vectors.push(state.draftVector);
200
+ state.draftVector = null;
201
+ state.pendingStart = null;
202
+ animateToState(refs, estimator, state);
203
+ refs.modeLabel.textContent = ui.awaitingVector;
204
+ }
205
+
206
+ export function loadPlanFile(refs: ViewRefs, estimator: FireOriginEstimator, state: ViewState, file: File): void {
207
+ state.importedPlanName = file.name.replace(/\.[^.]+$/, '') || 'room-plan';
208
+ refs.planImage.setAttribute('href', URL.createObjectURL(file));
209
+ state.hasLoadedPlan = true;
210
+ refs.dropzone.hidden = true;
211
+ refs.dropzone.setAttribute('aria-hidden', 'true');
212
+ syncControls(refs, true);
213
+ animateToState(refs, estimator, state);
214
+ }
215
+
216
+ export function exportReport(state: ViewState): void {
217
+ const payload = {
218
+ exportedAt: new Date().toISOString(),
219
+ plan: state.importedPlanName,
220
+ origin: state.currentState.origin,
221
+ confidence: Number(state.currentState.confidence.toFixed(1)),
222
+ varianceMeters: Number((state.currentState.radius / 10).toFixed(1)),
223
+ vectorCount: state.currentState.vectorCount,
224
+ vectors: state.vectors,
225
+ visibleLayers: state.visibleLayers,
226
+ };
227
+ const vectorMarkup = state.vectors.map((vector) => {
228
+ const start = toCanvas(vector.start);
229
+ const end = toCanvas(vector.end);
230
+ return `<line x1="${start.x.toFixed(2)}" y1="${start.y.toFixed(2)}" x2="${end.x.toFixed(2)}" y2="${end.y.toFixed(2)}" stroke="${patternColor(vector.pattern)}" stroke-width="4" stroke-linecap="round" />`;
231
+ }).join('');
232
+ const origin = toCanvas(state.currentState.origin);
233
+ const report = `<?xml version="1.0" encoding="UTF-8"?>
234
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${WIDTH} ${HEIGHT}">
235
+ <metadata>${JSON.stringify(payload)}</metadata>
236
+ <rect width="${WIDTH}" height="${HEIGHT}" fill="#f7f2ea" />
237
+ ${vectorMarkup}
238
+ <circle cx="${origin.x.toFixed(2)}" cy="${origin.y.toFixed(2)}" r="10" fill="#161e28" stroke="#fff7ed" stroke-width="3" />
239
+ <text x="24" y="${HEIGHT - 26}" font-family="IBM Plex Mono, monospace" font-size="16" fill="#16314a">
240
+ VECTORS ${state.currentState.vectorCount} | CONFIDENCE ${state.currentState.confidence.toFixed(1)}% | VARIANCE +-${(state.currentState.radius / 10).toFixed(1)} m
241
+ </text>
242
+ </svg>`;
243
+ download(`${state.importedPlanName}-fire-origin-report.svg`, report, 'image/svg+xml');
244
+ }
@@ -0,0 +1,30 @@
1
+ import { FireOriginEstimator } from './logic';
2
+ import {
3
+ bindAuxiliaryActions,
4
+ bindCanvasInteractions,
5
+ bindLayerToggles,
6
+ bindRadialMenu,
7
+ bindShortcuts,
8
+ bindUpload,
9
+ startRenderLoop,
10
+ } from './view-bindings';
11
+ import { createState, getRefs, syncControls, syncLayers, updatePointerOverlay } from './view-model';
12
+
13
+ export function initFirePatternOriginAnalyzer(ui: Record<string, string>): void {
14
+ const refs = getRefs();
15
+ if (!refs) return;
16
+
17
+ const estimator = new FireOriginEstimator();
18
+ const state = createState();
19
+
20
+ syncControls(refs, false);
21
+ syncLayers(refs, state);
22
+ updatePointerOverlay(refs, state);
23
+ bindLayerToggles(refs, estimator, state);
24
+ bindRadialMenu(refs, state, ui);
25
+ bindCanvasInteractions(refs, estimator, state, ui);
26
+ bindShortcuts(refs, estimator, state, ui);
27
+ bindUpload(refs, estimator, state);
28
+ bindAuxiliaryActions(refs, estimator, state, ui);
29
+ startRenderLoop(refs, state);
30
+ }
@@ -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: 'FBI. The Science of Fingerprints: Classification and Uses.',
6
+ url: 'https://www.ojp.gov/ncjrs/virtual-library/abstracts/science-fingerprints-classification-and-uses',
7
+ },
8
+ {
9
+ name: 'NIST Special Publication 500-290: Data Format for the Interchange of Fingerprint, Facial & Other Biometric Information.',
10
+ url: 'https://www.nist.gov/publications/data-format-interchange-fingerprint-facial-other-biometric-information-ansinist-itl-1-1',
11
+ },
12
+ {
13
+ name: 'SWGFAST. Standards for Examining Friction Ridge Impressions and Resulting Conclusions.',
14
+ url: 'https://www.nist.gov/system/files/documents/2016/10/26/swgfast_examinations-conclusions_2.0_130427.pdf',
15
+ },
16
+ ];