95 lines
3.8 KiB
TypeScript
95 lines
3.8 KiB
TypeScript
import type { CaseState, Evidence, Viewport, WidgetRelation } from './types'
|
|||
|
|
|
||
|
|
export const MIN_BOARD_ZOOM = 0.45
|
||
|
|
export const MAX_BOARD_ZOOM = 1.5
|
||
|
|
|
||
|
|
export function clampBoardZoom(zoom: number) {
|
||
|
|
return Math.max(MIN_BOARD_ZOOM, Math.min(MAX_BOARD_ZOOM, zoom))
|
||
|
|
}
|
||
|
|
|
||
|
|
export function zoomFromWheel(currentZoom: number, deltaY: number) {
|
||
|
|
return clampBoardZoom(currentZoom - deltaY * 0.006)
|
||
|
|
}
|
||
|
|
|
||
|
|
export function moveBoardPoint(origin: { x: number; y: number }, screenDelta: { x: number; y: number }, zoom: number) {
|
||
|
|
if (!Number.isFinite(zoom) || zoom <= 0) throw new RangeError('Board zoom must be positive')
|
||
|
|
return { x: origin.x + screenDelta.x / zoom, y: origin.y + screenDelta.y / zoom }
|
||
|
|
}
|
||
|
|
|
||
|
|
export function panViewport(origin: Viewport, screenDelta: { x: number; y: number }): Viewport {
|
||
|
|
return { ...origin, x: origin.x + screenDelta.x, y: origin.y + screenDelta.y }
|
||
|
|
}
|
||
|
|
|
||
|
|
export function dateValue(date: string) {
|
||
|
|
const parsed = Date.parse(date)
|
||
|
|
return Number.isFinite(parsed) ? parsed : 0
|
||
|
|
}
|
||
|
|
|
||
|
|
export function timelineRange(dates: string[], fallbackYear = new Date().getFullYear()) {
|
||
|
|
const years = dates
|
||
|
|
.map(date => Number(date.slice(0, 4)))
|
||
|
|
.filter(year => Number.isFinite(year) && year >= 1 && year <= 9999)
|
||
|
|
let startYear = years.length ? Math.min(...years) : fallbackYear - 2
|
||
|
|
let endYear = years.length ? Math.max(...years) : startYear + 4
|
||
|
|
if (endYear - startYear < 4) {
|
||
|
|
const missing = 4 - (endYear - startYear)
|
||
|
|
startYear -= Math.floor(missing / 2)
|
||
|
|
endYear += Math.ceil(missing / 2)
|
||
|
|
}
|
||
|
|
return { startYear, endYear, start: Date.UTC(startYear, 0, 1), end: Date.UTC(endYear, 11, 31) }
|
||
|
|
}
|
||
|
|
|
||
|
|
export function timelinePositionPercent(date: string, range: Pick<ReturnType<typeof timelineRange>, 'start' | 'end'>) {
|
||
|
|
if (range.end <= range.start) throw new RangeError('Timeline range must have positive duration')
|
||
|
|
return Math.max(0, Math.min(100, ((dateValue(date) - range.start) / (range.end - range.start)) * 100))
|
||
|
|
}
|
||
|
|
|
||
|
|
export function containedIds(state: CaseState, widgetId: string) {
|
||
|
|
return state.relations
|
||
|
|
.filter(relation => relation.type === 'contains' && relation.fromWidgetId === widgetId)
|
||
|
|
.sort((a, b) => (a.sortOrder || 0) - (b.sortOrder || 0))
|
||
|
|
.map(relation => relation.toWidgetId)
|
||
|
|
}
|
||
|
|
|
||
|
|
export function folderIsOpen(folder: Evidence) {
|
||
|
|
return folder.config?.open === true
|
||
|
|
}
|
||
|
|
|
||
|
|
export function relationPosition(state: CaseState, relation: WidgetRelation) {
|
||
|
|
const folder = state.evidence.find(widget => widget.id === relation.fromWidgetId)
|
||
|
|
const order = relation.sortOrder || 0
|
||
|
|
const configuredX = Number(relation.config?.x)
|
||
|
|
const configuredY = Number(relation.config?.y)
|
||
|
|
return {
|
||
|
|
x: Number.isFinite(configuredX) ? configuredX : (folder?.x || 100) + (folder?.width || 240) + 90 + (order % 3) * 205,
|
||
|
|
y: Number.isFinite(configuredY) ? configuredY : (folder?.y || 100) - 30 + Math.floor(order / 3) * 185,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
export function normalizeCase(state: CaseState): CaseState {
|
||
|
|
const relations = Array.isArray(state.relations)
|
||
|
|
? state.relations
|
||
|
|
: state.evidence.flatMap(widget => (widget.containedDocumentIds || (widget.sourceDocumentId ? [widget.sourceDocumentId] : [])).map((documentId, index) => ({
|
||
|
|
id: `contains:${widget.id}:${documentId}`,
|
||
|
|
fromWidgetId: widget.id,
|
||
|
|
toWidgetId: documentId,
|
||
|
|
type: 'contains',
|
||
|
|
sortOrder: index,
|
||
|
|
})))
|
||
|
|
const normalized = { ...state, relations }
|
||
|
|
return {
|
||
|
|
...normalized,
|
||
|
|
documents: state.documents.map(document => ({
|
||
|
|
...document,
|
||
|
|
fileType: document.fileType || (document.mimeType?.startsWith('image/') ? 'image' : document.mimeType === 'application/pdf' ? 'pdf' : 'file'),
|
||
|
|
metadata: document.metadata || {},
|
||
|
|
})),
|
||
|
|
evidence: state.evidence.map(widget => ({
|
||
|
|
...widget,
|
||
|
|
type: widget.type === 'evidence' ? 'folder' : widget.type,
|
||
|
|
config: widget.config || {},
|
||
|
|
containedDocumentIds: containedIds(normalized, widget.id),
|
||
|
|
})),
|
||
|
|
}
|
||
|
|
}
|