@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.
- package/package.json +1 -1
- package/src/category/index.ts +5 -1
- package/src/entries.ts +9 -1
- package/src/index.ts +2 -0
- package/src/tests/locale_completeness.test.ts +2 -2
- package/src/tests/tool_validation.test.ts +2 -2
- package/src/tool/fire-pattern-origin-analyzer/bibliography.astro +6 -0
- package/src/tool/fire-pattern-origin-analyzer/bibliography.ts +12 -0
- package/src/tool/fire-pattern-origin-analyzer/component.astro +196 -0
- package/src/tool/fire-pattern-origin-analyzer/entry.ts +32 -0
- package/src/tool/fire-pattern-origin-analyzer/fire-pattern-origin-analyzer.css +681 -0
- package/src/tool/fire-pattern-origin-analyzer/i18n/de.ts +250 -0
- package/src/tool/fire-pattern-origin-analyzer/i18n/en.ts +250 -0
- package/src/tool/fire-pattern-origin-analyzer/i18n/es.ts +250 -0
- package/src/tool/fire-pattern-origin-analyzer/i18n/fr.ts +250 -0
- package/src/tool/fire-pattern-origin-analyzer/i18n/id.ts +250 -0
- package/src/tool/fire-pattern-origin-analyzer/i18n/it.ts +250 -0
- package/src/tool/fire-pattern-origin-analyzer/i18n/ja.ts +250 -0
- package/src/tool/fire-pattern-origin-analyzer/i18n/ko.ts +250 -0
- package/src/tool/fire-pattern-origin-analyzer/i18n/nl.ts +250 -0
- package/src/tool/fire-pattern-origin-analyzer/i18n/pl.ts +250 -0
- package/src/tool/fire-pattern-origin-analyzer/i18n/pt.ts +250 -0
- package/src/tool/fire-pattern-origin-analyzer/i18n/ru.ts +250 -0
- package/src/tool/fire-pattern-origin-analyzer/i18n/sv.ts +250 -0
- package/src/tool/fire-pattern-origin-analyzer/i18n/tr.ts +250 -0
- package/src/tool/fire-pattern-origin-analyzer/i18n/zh.ts +246 -0
- package/src/tool/fire-pattern-origin-analyzer/index.ts +11 -0
- package/src/tool/fire-pattern-origin-analyzer/logic.ts +123 -0
- package/src/tool/fire-pattern-origin-analyzer/render.ts +159 -0
- package/src/tool/fire-pattern-origin-analyzer/seo.astro +15 -0
- package/src/tool/fire-pattern-origin-analyzer/view-bindings.ts +171 -0
- package/src/tool/fire-pattern-origin-analyzer/view-model.ts +244 -0
- package/src/tool/fire-pattern-origin-analyzer/view.ts +30 -0
- package/src/tool/forensic-fingerprint-minutiae-identifier/bibliography.astro +6 -0
- package/src/tool/forensic-fingerprint-minutiae-identifier/bibliography.ts +16 -0
- package/src/tool/forensic-fingerprint-minutiae-identifier/component.astro +429 -0
- package/src/tool/forensic-fingerprint-minutiae-identifier/drawer.ts +94 -0
- package/src/tool/forensic-fingerprint-minutiae-identifier/entry.ts +32 -0
- package/src/tool/forensic-fingerprint-minutiae-identifier/forensic-fingerprint-minutiae-identifier.css +969 -0
- package/src/tool/forensic-fingerprint-minutiae-identifier/i18n/de.ts +298 -0
- package/src/tool/forensic-fingerprint-minutiae-identifier/i18n/en.ts +289 -0
- package/src/tool/forensic-fingerprint-minutiae-identifier/i18n/es.ts +291 -0
- package/src/tool/forensic-fingerprint-minutiae-identifier/i18n/fr.ts +298 -0
- package/src/tool/forensic-fingerprint-minutiae-identifier/i18n/id.ts +298 -0
- package/src/tool/forensic-fingerprint-minutiae-identifier/i18n/it.ts +298 -0
- package/src/tool/forensic-fingerprint-minutiae-identifier/i18n/ja.ts +281 -0
- package/src/tool/forensic-fingerprint-minutiae-identifier/i18n/ko.ts +281 -0
- package/src/tool/forensic-fingerprint-minutiae-identifier/i18n/nl.ts +298 -0
- package/src/tool/forensic-fingerprint-minutiae-identifier/i18n/pl.ts +298 -0
- package/src/tool/forensic-fingerprint-minutiae-identifier/i18n/pt.ts +298 -0
- package/src/tool/forensic-fingerprint-minutiae-identifier/i18n/ru.ts +298 -0
- package/src/tool/forensic-fingerprint-minutiae-identifier/i18n/sv.ts +298 -0
- package/src/tool/forensic-fingerprint-minutiae-identifier/i18n/tr.ts +298 -0
- package/src/tool/forensic-fingerprint-minutiae-identifier/i18n/zh.ts +281 -0
- package/src/tool/forensic-fingerprint-minutiae-identifier/index.ts +11 -0
- package/src/tool/forensic-fingerprint-minutiae-identifier/logic.ts +168 -0
- package/src/tool/forensic-fingerprint-minutiae-identifier/renderer.ts +116 -0
- package/src/tool/forensic-fingerprint-minutiae-identifier/seo.astro +15 -0
- 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,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
|
+
];
|