Files
Web_FreeCAD_Bitbybit/src/facade/topologyReferences.ts

101 lines
5.2 KiB
TypeScript

import type { SubshapeRef, TopoRefValue } from './types'
import { matchSubshapes, type SubshapeMatch, type SubshapeSignature } from './topologyNaming'
export type PersistedTopoRef = TopoRefValue
export type TopoRefResolution = {
status: 'resolved' | 'ambiguous' | 'deleted'
ref?: SubshapeRef
candidates?: SubshapeRef[]
}
export type TopologyMigration = {
records: PersistedTopoRef[]
matches: SubshapeMatch[]
counts: Record<NonNullable<SubshapeRef['status']>, number>
}
const transientIndexKeys = new Set(['faceIndex', 'edgeIndex', 'vertexIndex', 'subshapeIndex'])
const statuses = new Set<NonNullable<SubshapeRef['status']>>(['stable', 'ambiguous', 'new', 'deleted'])
const kinds = new Set<SubshapeRef['kind']>(['face', 'edge', 'vertex'])
const finiteNonNegativeInteger = (value: unknown) => typeof value === 'number' && Number.isSafeInteger(value) && value >= 0
export const createPersistedTopoRef = (objectId: string, ref: SubshapeRef, generation: number): PersistedTopoRef => {
if (!objectId.trim()) throw new RangeError('TopoRef objectId is required.')
if (!finiteNonNegativeInteger(generation)) throw new RangeError('TopoRef generation must be a non-negative integer.')
if (!ref.persistentId.trim()) throw new RangeError('TopoRef persistentId is required.')
if (!finiteNonNegativeInteger(ref.topologyVersion)) throw new RangeError('TopoRef topologyVersion must be a non-negative integer.')
return {
schemaVersion: 1,
objectId,
kind: ref.kind,
persistentId: ref.persistentId,
topologyVersion: ref.topologyVersion,
generation,
status: ref.status ?? 'stable',
signature: ref.signature,
candidates: ref.candidates ? [...new Set(ref.candidates)] : undefined,
}
}
export const serializeTopoRef = (record: PersistedTopoRef) => JSON.stringify(createPersistedTopoRef(record.objectId, {
shapeId: record.objectId,
kind: record.kind,
persistentId: record.persistentId,
topologyVersion: record.topologyVersion,
status: record.status,
signature: record.signature,
candidates: record.candidates,
}, record.generation))
export const parseTopoRef = (json: string): PersistedTopoRef => {
let value: unknown
try { value = JSON.parse(json) } catch { throw new SyntaxError('TopoRef JSON is invalid.') }
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new TypeError('TopoRef must be a JSON object.')
const source = value as Record<string, unknown>
for (const key of transientIndexKeys) if (key in source) throw new RangeError(`TopoRef must not persist transient ${key}.`)
if (source.schemaVersion !== 1) throw new RangeError('TopoRef schemaVersion is not supported.')
if (typeof source.objectId !== 'string' || typeof source.persistentId !== 'string') throw new TypeError('TopoRef objectId and persistentId must be strings.')
if (!kinds.has(source.kind as SubshapeRef['kind'])) throw new RangeError('TopoRef kind is invalid.')
if (!statuses.has(source.status as NonNullable<SubshapeRef['status']>)) throw new RangeError('TopoRef status is invalid.')
if (!finiteNonNegativeInteger(source.topologyVersion) || !finiteNonNegativeInteger(source.generation)) throw new RangeError('TopoRef version fields must be non-negative integers.')
if (source.signature !== undefined && typeof source.signature !== 'string') throw new TypeError('TopoRef signature must be a string.')
if (source.candidates !== undefined && (!Array.isArray(source.candidates) || !source.candidates.every((candidate) => typeof candidate === 'string'))) throw new TypeError('TopoRef candidates must be strings.')
return createPersistedTopoRef(source.objectId, {
shapeId: source.objectId,
kind: source.kind as SubshapeRef['kind'],
persistentId: source.persistentId,
topologyVersion: source.topologyVersion as number,
status: source.status as NonNullable<SubshapeRef['status']>,
signature: source.signature as string | undefined,
candidates: source.candidates as string[] | undefined,
}, source.generation as number)
}
export const resolveTopoRef = (record: PersistedTopoRef, current: SubshapeRef[]): TopoRefResolution => {
const sameKind = current.filter((ref) => ref.kind === record.kind)
const byId = sameKind.filter((ref) => ref.persistentId === record.persistentId)
if (byId.length === 1 && byId[0].status !== 'ambiguous') return { status: 'resolved', ref: byId[0] }
const bySignature = record.signature ? sameKind.filter((ref) => ref.signature === record.signature) : []
const candidates = byId.length > 0 ? byId : bySignature
if (candidates.length === 1 && candidates[0].status !== 'ambiguous') return { status: 'resolved', ref: candidates[0] }
if (candidates.length > 0) return { status: 'ambiguous', candidates }
return { status: 'deleted' }
}
export const migrateTopoRefs = (
objectId: string,
previous: Array<{ ref: SubshapeRef; signature: SubshapeSignature }>,
current: Array<{ ref: SubshapeRef; signature: SubshapeSignature }>,
generation: number,
): TopologyMigration => {
const matches = matchSubshapes(previous, current)
const counts: TopologyMigration['counts'] = { stable: 0, ambiguous: 0, new: 0, deleted: 0 }
const records = matches.map((match) => {
counts[match.status] += 1
return createPersistedTopoRef(objectId, match.current, generation)
})
return { records, matches, counts }
}