feat: advance FreeCAD parity and native OCCT history
This commit is contained in:
@@ -22,11 +22,19 @@ export type FcstdEntryMetadata = {
|
||||
|
||||
export type FcstdObjectSupport = 'recognized' | 'proxy' | 'blocked'
|
||||
|
||||
export type FcstdPropertySummary = {
|
||||
name: string
|
||||
typeId: string
|
||||
element: string
|
||||
value: string
|
||||
}
|
||||
|
||||
export type FcstdObjectSummary = {
|
||||
name: string
|
||||
label: string
|
||||
typeId: string
|
||||
propertyCount: number
|
||||
properties: FcstdPropertySummary[]
|
||||
support: FcstdObjectSupport
|
||||
}
|
||||
|
||||
@@ -179,6 +187,18 @@ const propertyValue = (property: Record<string, unknown>): string => {
|
||||
return ''
|
||||
}
|
||||
|
||||
const propertyElement = (property: Record<string, unknown>): string => {
|
||||
const entry = Object.entries(property).find(([name, value]) => !name.startsWith('@_') && Boolean(value && typeof value === 'object'))
|
||||
return entry?.[0] || ''
|
||||
}
|
||||
|
||||
const propertySummaries = (properties: Record<string, unknown>[]): FcstdPropertySummary[] => properties.map((property) => ({
|
||||
name: attribute(property, 'name') || '<unnamed>',
|
||||
typeId: attribute(property, 'type') || propertyElement(property) || 'unknown',
|
||||
element: propertyElement(property),
|
||||
value: propertyValue(property),
|
||||
}))
|
||||
|
||||
const parseDocumentXml = (bytes: Uint8Array) => {
|
||||
const xml = new TextDecoder('utf-8', { fatal: true }).decode(bytes)
|
||||
if (/<!DOCTYPE|<!ENTITY/i.test(xml)) throw new Error('FCStd Document.xml declarations and entities are not allowed.')
|
||||
@@ -202,7 +222,7 @@ const parseDocumentXml = (bytes: Uint8Array) => {
|
||||
const properties = asArray((((data?.Properties as Record<string, unknown> | undefined)?.Property) as Record<string, unknown> | Record<string, unknown>[] | undefined))
|
||||
const objectLabelProperty = properties.find((property) => attribute(property, 'name') === 'Label')
|
||||
const support: FcstdObjectSupport = blockedTypeId(typeId) ? 'blocked' : recognizedTypeIds.has(typeId) ? 'recognized' : 'proxy'
|
||||
return { name, label: objectLabelProperty ? propertyValue(objectLabelProperty) || name : name, typeId, propertyCount: properties.length, support }
|
||||
return { name, label: objectLabelProperty ? propertyValue(objectLabelProperty) || name : name, typeId, propertyCount: properties.length, properties: propertySummaries(properties), support }
|
||||
})
|
||||
return {
|
||||
schemaVersion: attribute(root, 'SchemaVersion') || attribute(root, 'schemaVersion') || 'unknown',
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { BitByBitOCCT, OccStateEnum } from '@bitbybit-dev/occt-worker'
|
||||
import type { Inputs } from '@bitbybit-dev/occt'
|
||||
import type { ApplyPlacementInput, BooleanCutInput, BooleanIntersectionInput, BooleanUnionInput, ChamferInput, CreateBoxInput, CreateConeInput, CreateCylinderInput, CreateSphereInput, FilletInput, GeometryCapabilities, GeometryDocumentContext, GeometryFileExport, LinearFeatureParameters, MeshAsset, MirrorInput, PadInput, PlanarProfile, PocketInput, Point3, RevolutionInput, ShapeHandle, SubshapeRef, SubshapeTopology } from './types'
|
||||
import type { ApplyPlacementInput, BooleanCutInput, BooleanIntersectionInput, BooleanUnionInput, ChamferInput, CreateBoxInput, CreateConeInput, CreateCylinderInput, CreateSphereInput, FilletInput, GeometryCapabilities, GeometryDocumentContext, GeometryFileExport, LinearFeatureParameters, MeshAsset, MirrorInput, NativeTopologyHistoryInput, NativeTopologyHistoryRecord, PadInput, PlanarProfile, PocketInput, Point3, RevolutionInput, ShapeHandle, SubshapeRef, SubshapeTopology } from './types'
|
||||
import { mapNativeOcctHistoryRecords } from './nativeHistoryProvider'
|
||||
import { NativeOcctHistoryCoordinator, type NativeOcctHistoryProvider } from './nativeHistoryProtocol'
|
||||
import { createEdgeSubshapeRefs, createSubshapeRefs, createVertexSubshapeRefs } from './topologyNaming'
|
||||
|
||||
type KernelShapeReference = Inputs.OCCT.TopoDSShapePointer
|
||||
@@ -230,6 +232,34 @@ export class BitbybitGeometryRuntime {
|
||||
private sequence = 0
|
||||
private readonly shapes = new Map<string, ShapeEntry>()
|
||||
private readonly kernelReferences = new Map<number, KernelReferenceEntry>()
|
||||
private nativeHistory: { provider: NativeOcctHistoryProvider; coordinator: NativeOcctHistoryCoordinator } | null = null
|
||||
private readonly nativeHistoryDocumentVersions = new Map<string, number>()
|
||||
|
||||
configureNativeHistory(provider: NativeOcctHistoryProvider | null, timeoutMs = 120_000) {
|
||||
this.nativeHistory?.coordinator.cancel()
|
||||
if (provider === null) {
|
||||
this.nativeHistory = null
|
||||
this.nativeHistoryDocumentVersions.clear()
|
||||
return
|
||||
}
|
||||
if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0) throw new RangeError('Native history timeoutMs must be a positive safe integer.')
|
||||
this.nativeHistory = {
|
||||
provider,
|
||||
coordinator: new NativeOcctHistoryCoordinator((documentId) => this.nativeHistoryDocumentVersions.get(documentId) ?? null, timeoutMs),
|
||||
}
|
||||
}
|
||||
|
||||
nativeHistoryCapabilities() {
|
||||
return this.nativeHistory?.provider.capabilities() ?? {
|
||||
providerId: 'occt-native.history-step',
|
||||
providerVersion: 'unconfigured',
|
||||
occtVersion: 'unknown',
|
||||
availability: 'unavailable' as const,
|
||||
operations: [],
|
||||
transport: 'step-text' as const,
|
||||
reason: 'Native OCCT history provider is not configured.',
|
||||
}
|
||||
}
|
||||
|
||||
capabilities(): GeometryCapabilities { return { ...this.capabilitiesState } }
|
||||
|
||||
@@ -471,6 +501,30 @@ export class BitbybitGeometryRuntime {
|
||||
return { faces: faceTopology.refs, edges: edgeTopology.refs, vertices: vertexTopology.refs, entries }
|
||||
}
|
||||
|
||||
async topologyHistory(input: NativeTopologyHistoryInput): Promise<NativeTopologyHistoryRecord[]> {
|
||||
const nativeHistory = this.nativeHistory
|
||||
if (!nativeHistory) throw new Error('Native OCCT history provider is not configured.')
|
||||
if (!input.operation) throw new Error('Native OCCT history requires a supported Boolean operation.')
|
||||
if (input.inputs.length !== 2) throw new Error('Native OCCT history requires exactly an object and a tool input.')
|
||||
validateDocumentContext(input)
|
||||
this.nativeHistoryDocumentVersions.set(input.documentId, input.documentVersion)
|
||||
const [objectInput, toolInput] = input.inputs
|
||||
const [objectStep, toolStep] = await Promise.all([
|
||||
this.exportStep(objectInput.shape, `${objectInput.objectId}.step`),
|
||||
this.exportStep(toolInput.shape, `${toolInput.objectId}.step`),
|
||||
])
|
||||
const execution = await nativeHistory.coordinator.capture(nativeHistory.provider, {
|
||||
documentId: input.documentId,
|
||||
documentVersion: input.documentVersion,
|
||||
operationId: input.operationId,
|
||||
operation: input.operation,
|
||||
objectStep: objectStep.text,
|
||||
toolStep: toolStep.text,
|
||||
})
|
||||
if (execution.status !== 'completed' || !execution.response) throw new Error(`Native OCCT history ${execution.status}.`)
|
||||
return mapNativeOcctHistoryRecords(execution.response.history, { object: objectInput.objectId, tool: toolInput.objectId })
|
||||
}
|
||||
|
||||
async release(shape: ShapeHandle): Promise<void> {
|
||||
const entry = this.shapes.get(shape.id)
|
||||
if (!entry) return
|
||||
@@ -487,6 +541,7 @@ export class BitbybitGeometryRuntime {
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.nativeHistory?.coordinator.cancel()
|
||||
this.cancelInitialization?.()
|
||||
this.shapes.clear()
|
||||
this.kernelReferences.clear()
|
||||
@@ -496,6 +551,8 @@ export class BitbybitGeometryRuntime {
|
||||
this.worker = null
|
||||
this.initialization = null
|
||||
this.cancelInitialization = null
|
||||
this.nativeHistoryDocumentVersions.clear()
|
||||
this.nativeHistory = null
|
||||
this.capabilitiesState = unavailableCapabilities()
|
||||
}
|
||||
|
||||
|
||||
@@ -11,8 +11,15 @@ export type { ApplyPlacementInput, BitBybitViewportAdapter, BitBybitWebCadFacade
|
||||
export { createEdgeSubshapeRefs, createSubshapeRefs, createVertexSubshapeRefs, matchSubshapes, signatureForEdge, signatureForFace, signatureForVertex } from './topologyNaming'
|
||||
export { cloneObjectTopologySnapshot, createPersistedTopoRef, migrateDocumentTopologyReferences, migrateTopoRefs, parseTopoRef, resolveDocumentTopologyReference, resolveTopoRef, serializeTopoRef } from './topologyReferences'
|
||||
export type { DocumentTopologyReferenceMigration, PersistedTopoRef, TopologyMigration, TopologyReferenceMigrationIssue, TopoRefResolution } from './topologyReferences'
|
||||
export { captureSignatureTopologyHistory } from './topologyHistory'
|
||||
export { captureNativeTopologyHistory, captureSignatureTopologyHistory } from './topologyHistory'
|
||||
export type { TopologyHistoryEntry, TopologyHistoryRelation, TopologyHistoryResult } from './topologyHistory'
|
||||
export { createNativeOcctStepHistoryBridge, mapNativeOcctHistoryRecords } from './nativeHistoryProvider'
|
||||
export type { NativeOcctHistoryOperation, NativeOcctHistoryRecord, NativeOcctHistoryResponse, NativeOcctHistoryStepGeometry, NativeOcctHistoryStepProvider } from './nativeHistoryProvider'
|
||||
export { DirectNativeOcctHistoryProvider, NativeOcctHistoryCoordinator, NativeOcctHistoryUnavailableError, UnavailableNativeOcctHistoryProvider } from './nativeHistoryProtocol'
|
||||
export { NATIVE_OCCT_HISTORY_PROTOCOL_VERSION } from './nativeHistoryProtocol'
|
||||
export type { NativeOcctHistoryCapabilities, NativeOcctHistoryExecution, NativeOcctHistoryProvider, NativeOcctHistoryProtocolResponse, NativeOcctHistoryRequest } from './nativeHistoryProtocol'
|
||||
export { NativeOcctHistoryWorkerProvider } from './nativeHistoryWorkerClient'
|
||||
export type { NativeOcctHistoryWorkerOptions } from './nativeHistoryWorkerClient'
|
||||
export { BasicSketchSolverAdapter, cloneSketch, cloneSketchConstraint, cloneSketchGeometry, createSketch, solveSketch } from './sketcher'
|
||||
export type { SketchConstraint, SketchDiagnostic, SketchExternalGeometry, SketchGeometry, SketchPoint, SketchPointRef, SketchSnapshot, SketchSolveOptions, SketchSolveResult, SketchSolverAdapter, SketchSolverStatus } from './sketcher'
|
||||
export { BasicSketchSolverProvider, SKETCH_SOLVER_PROTOCOL_VERSION, SketchSolverCoordinator, SketchSolverUnavailableError, UnavailablePlanegcsProvider, runSketchSolverReplay } from './sketchSolverProtocol'
|
||||
@@ -20,4 +27,4 @@ export type { SketchSolverCapabilities, SketchSolverCompatibility, SketchSolverE
|
||||
export { createFacadeGeometryRecomputeExecutor, executeFacadeRecomputeNode, RecomputeCoordinator } from './recomputeEngine'
|
||||
export type { RecomputeExecutionError, RecomputeExecutionOptions, RecomputeExecutionResult, RecomputeExecutionStatus, RecomputeGeometryRuntime, RecomputeNodeContext, RecomputeNodeExecutor, RecomputeNodeResult, RecomputeProgress } from './recomputeEngine'
|
||||
export { DEFAULT_FCSTD_LIMITS, inspectFcstdArchive } from './fcstd'
|
||||
export type { FcstdArchiveLimits, FcstdCompatibilityReport, FcstdEntryMetadata, FcstdEntryRole, FcstdInspection, FcstdObjectSummary, FcstdObjectSupport } from './fcstd'
|
||||
export type { FcstdArchiveLimits, FcstdCompatibilityReport, FcstdEntryMetadata, FcstdEntryRole, FcstdInspection, FcstdObjectSummary, FcstdObjectSupport, FcstdPropertySummary } from './fcstd'
|
||||
|
||||
@@ -809,7 +809,7 @@ export function createMockFacade(): BitBybitWebCadFacade {
|
||||
task: { getActive: () => getState().task, begin: beginTask, update: (draft) => { if (state.task) state = { ...state, task: { ...state.task, draft: { ...state.task.draft, ...draft } } }; emitState() }, apply: applyTask, cancel: () => { if (state.task) state = { ...state, task: { ...state.task, status: 'cancelled' } }; emitState() } },
|
||||
diagnostics: { list: () => state.diagnostics.map(cloneDiagnostic), tree: () => buildDiagnosticTree(state.diagnostics), repair: repairDiagnostic },
|
||||
project: { capabilities: () => projectPersistence.capabilities(), subscribeExternalChanges: (listener) => projectPersistence.subscribeExternalChanges(listener), list: () => projectPersistence.list(), save: (document = getState().document) => projectPersistence.save(document), load: (documentId) => projectPersistence.load(documentId), loadCheckpoint: (documentId, version) => projectPersistence.loadCheckpoint(documentId, version), recovery: (documentId) => projectPersistence.recovery(documentId), fcstd: { inspect: (bytes, limits) => inspectFcstdArchive(bytes, limits) }, resource: projectPersistence.resource },
|
||||
geometry: { capabilities: () => geometryRuntime.capabilities(), initialize: () => geometryRuntime.initialize(), createBox: (input) => geometryRuntime.createBox(input), createCylinder: (input) => geometryRuntime.createCylinder(input), createSphere: (input) => geometryRuntime.createSphere(input), createCone: (input) => geometryRuntime.createCone(input), applyPlacement: (input) => geometryRuntime.applyPlacement(input), mirror: (input) => geometryRuntime.mirror(input), union: (input) => geometryRuntime.union(input), cut: (input) => geometryRuntime.cut(input), intersection: (input) => geometryRuntime.intersection(input), fillet: (input) => geometryRuntime.fillet(input), chamfer: (input) => geometryRuntime.chamfer(input), exportStep: (shape, fileName) => geometryRuntime.exportStep(shape, fileName), exportStl: (shape, fileName, precision) => geometryRuntime.exportStl(shape, fileName, precision), pad: (input) => geometryRuntime.pad(input), pocket: (input) => geometryRuntime.pocket(input), revolution: (input) => geometryRuntime.revolution(input), mesh: (shape, precision) => geometryRuntime.mesh(shape, precision), subshapes: (shape, precision) => geometryRuntime.subshapes(shape, precision), topology: (shape, precision) => geometryRuntime.topology(shape, precision), getObjectShape: (objectId) => { const shape = featureShapes.get(objectId); return shape ? { ...shape } : null }, release: (shape) => geometryRuntime.release(shape), dispose: () => { clearFeatureShapes(); geometryRuntime.dispose() } },
|
||||
geometry: { capabilities: () => geometryRuntime.capabilities(), configureNativeHistory: (provider, timeoutMs) => geometryRuntime.configureNativeHistory(provider, timeoutMs), nativeHistoryCapabilities: () => geometryRuntime.nativeHistoryCapabilities(), initialize: () => geometryRuntime.initialize(), createBox: (input) => geometryRuntime.createBox(input), createCylinder: (input) => geometryRuntime.createCylinder(input), createSphere: (input) => geometryRuntime.createSphere(input), createCone: (input) => geometryRuntime.createCone(input), applyPlacement: (input) => geometryRuntime.applyPlacement(input), mirror: (input) => geometryRuntime.mirror(input), union: (input) => geometryRuntime.union(input), cut: (input) => geometryRuntime.cut(input), intersection: (input) => geometryRuntime.intersection(input), fillet: (input) => geometryRuntime.fillet(input), chamfer: (input) => geometryRuntime.chamfer(input), exportStep: (shape, fileName) => geometryRuntime.exportStep(shape, fileName), exportStl: (shape, fileName, precision) => geometryRuntime.exportStl(shape, fileName, precision), pad: (input) => geometryRuntime.pad(input), pocket: (input) => geometryRuntime.pocket(input), revolution: (input) => geometryRuntime.revolution(input), mesh: (shape, precision) => geometryRuntime.mesh(shape, precision), subshapes: (shape, precision) => geometryRuntime.subshapes(shape, precision), topology: (shape, precision) => geometryRuntime.topology(shape, precision), topologyHistory: (input) => geometryRuntime.topologyHistory(input), getObjectShape: (objectId) => { const shape = featureShapes.get(objectId); return shape ? { ...shape } : null }, release: (shape) => geometryRuntime.release(shape), dispose: () => { clearFeatureShapes(); geometryRuntime.dispose() } },
|
||||
viewport: { createAdapter: () => new ThreeViewportAdapter() },
|
||||
getState, subscribe: (listener) => { listeners.add(listener); return () => { listeners.delete(listener) } }, notify,
|
||||
}
|
||||
|
||||
138
src/facade/nativeHistoryProtocol.ts
Normal file
138
src/facade/nativeHistoryProtocol.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
import type { NativeOcctHistoryOperation, NativeOcctHistoryResponse, NativeOcctHistoryStepProvider } from './nativeHistoryProvider'
|
||||
|
||||
export const NATIVE_OCCT_HISTORY_PROTOCOL_VERSION = 1 as const
|
||||
|
||||
export type NativeOcctHistoryCapabilities = {
|
||||
providerId: string
|
||||
providerVersion: string
|
||||
occtVersion: string
|
||||
availability: 'available' | 'unavailable'
|
||||
operations: NativeOcctHistoryOperation[]
|
||||
transport: 'step-text'
|
||||
reason?: string
|
||||
}
|
||||
|
||||
export type NativeOcctHistoryRequest = {
|
||||
protocolVersion: typeof NATIVE_OCCT_HISTORY_PROTOCOL_VERSION
|
||||
requestId: string
|
||||
documentId: string
|
||||
documentVersion: number
|
||||
operationId: string
|
||||
operation: NativeOcctHistoryOperation
|
||||
objectStep: string
|
||||
toolStep: string
|
||||
}
|
||||
|
||||
export type NativeOcctHistoryProtocolResponse = {
|
||||
protocolVersion: typeof NATIVE_OCCT_HISTORY_PROTOCOL_VERSION
|
||||
requestId: string
|
||||
documentId: string
|
||||
documentVersion: number
|
||||
operationId: string
|
||||
provider: NativeOcctHistoryCapabilities
|
||||
history: NativeOcctHistoryResponse
|
||||
}
|
||||
|
||||
export interface NativeOcctHistoryProvider {
|
||||
capabilities(): NativeOcctHistoryCapabilities
|
||||
capture(request: NativeOcctHistoryRequest, signal: AbortSignal): Promise<NativeOcctHistoryProtocolResponse>
|
||||
}
|
||||
|
||||
export class NativeOcctHistoryUnavailableError extends Error {
|
||||
readonly code = 'NATIVE_OCCT_HISTORY_UNAVAILABLE'
|
||||
|
||||
constructor(readonly provider: NativeOcctHistoryCapabilities) {
|
||||
super(provider.reason || `Native OCCT history provider '${provider.providerId}' is unavailable.`)
|
||||
this.name = 'NativeOcctHistoryUnavailableError'
|
||||
}
|
||||
}
|
||||
|
||||
const abortError = () => new DOMException('Native OCCT history request cancelled.', 'AbortError')
|
||||
|
||||
const assertRequest = (request: NativeOcctHistoryRequest) => {
|
||||
if (request.protocolVersion !== NATIVE_OCCT_HISTORY_PROTOCOL_VERSION) throw new RangeError(`Unsupported native OCCT history protocol version: ${request.protocolVersion}.`)
|
||||
if (!request.requestId.trim() || !request.documentId.trim() || !request.operationId.trim()) throw new TypeError('Native OCCT history request IDs must be non-empty strings.')
|
||||
if (!Number.isSafeInteger(request.documentVersion) || request.documentVersion < 0) throw new RangeError('Native OCCT history documentVersion must be a non-negative integer.')
|
||||
if (!request.objectStep.startsWith('ISO-10303-21;') || !request.toolStep.startsWith('ISO-10303-21;')) throw new TypeError('Native OCCT history transport requires ISO-10303-21 STEP text.')
|
||||
}
|
||||
|
||||
const assertResponse = (request: NativeOcctHistoryRequest, response: NativeOcctHistoryProtocolResponse) => {
|
||||
if (response.protocolVersion !== request.protocolVersion || response.requestId !== request.requestId || response.documentId !== request.documentId || response.documentVersion !== request.documentVersion || response.operationId !== request.operationId) throw new Error('Native OCCT history response does not match its request context.')
|
||||
if (response.history.provider !== 'occt-native' || response.history.occtVersion !== response.provider.occtVersion) throw new Error('Native OCCT history response provider metadata is inconsistent.')
|
||||
return response
|
||||
}
|
||||
|
||||
export class DirectNativeOcctHistoryProvider implements NativeOcctHistoryProvider {
|
||||
constructor(private readonly module: NativeOcctHistoryStepProvider, private readonly providerVersion = '8.0.0-embind') {}
|
||||
|
||||
capabilities(): NativeOcctHistoryCapabilities {
|
||||
const occtVersion = this.module.occtVersion()
|
||||
return {
|
||||
providerId: 'occt-native.history-step',
|
||||
providerVersion: this.providerVersion,
|
||||
occtVersion,
|
||||
availability: 'available',
|
||||
operations: ['fuse', 'cut', 'common'],
|
||||
transport: 'step-text',
|
||||
}
|
||||
}
|
||||
|
||||
async capture(request: NativeOcctHistoryRequest, signal: AbortSignal): Promise<NativeOcctHistoryProtocolResponse> {
|
||||
assertRequest(request)
|
||||
if (signal.aborted) throw abortError()
|
||||
const provider = this.capabilities()
|
||||
const history = await Promise.resolve().then(() => this.module.booleanHistoryFromStep(request.objectStep, request.toolStep, request.operation))
|
||||
if (signal.aborted) throw abortError()
|
||||
return assertResponse(request, { protocolVersion: request.protocolVersion, requestId: request.requestId, documentId: request.documentId, documentVersion: request.documentVersion, operationId: request.operationId, provider, history })
|
||||
}
|
||||
}
|
||||
|
||||
export class UnavailableNativeOcctHistoryProvider implements NativeOcctHistoryProvider {
|
||||
constructor(private readonly message = 'No native OCCT history WASM artifact is installed.') {}
|
||||
|
||||
capabilities(): NativeOcctHistoryCapabilities {
|
||||
return { providerId: 'occt-native.history-step', providerVersion: 'unavailable', occtVersion: 'unknown', availability: 'unavailable', operations: [], transport: 'step-text', reason: this.message }
|
||||
}
|
||||
|
||||
capture(): Promise<NativeOcctHistoryProtocolResponse> {
|
||||
return Promise.reject(new NativeOcctHistoryUnavailableError(this.capabilities()))
|
||||
}
|
||||
}
|
||||
|
||||
export type NativeOcctHistoryExecution = {
|
||||
status: 'completed' | 'cancelled' | 'stale' | 'timed-out'
|
||||
requestId: string
|
||||
response?: NativeOcctHistoryProtocolResponse
|
||||
}
|
||||
|
||||
export class NativeOcctHistoryCoordinator {
|
||||
private active: { requestId: string; controller: AbortController } | null = null
|
||||
private sequence = 0
|
||||
|
||||
constructor(private readonly currentDocumentVersion: (documentId: string) => number | null, private readonly timeoutMs = 120_000) {}
|
||||
|
||||
cancel() { this.active?.controller.abort() }
|
||||
|
||||
async capture(provider: NativeOcctHistoryProvider, input: Omit<NativeOcctHistoryRequest, 'protocolVersion' | 'requestId'>): Promise<NativeOcctHistoryExecution> {
|
||||
this.active?.controller.abort()
|
||||
const requestId = `native-history-${++this.sequence}`
|
||||
const controller = new AbortController()
|
||||
this.active = { requestId, controller }
|
||||
const request: NativeOcctHistoryRequest = { ...input, protocolVersion: NATIVE_OCCT_HISTORY_PROTOCOL_VERSION, requestId }
|
||||
let timeout: ReturnType<typeof setTimeout> | undefined
|
||||
try {
|
||||
const timeoutPromise = new Promise<never>((_, reject) => { timeout = setTimeout(() => { controller.abort(); reject(new DOMException('Native OCCT history request timed out.', 'TimeoutError')) }, this.timeoutMs) })
|
||||
const response = await Promise.race([provider.capture(request, controller.signal), timeoutPromise])
|
||||
if (controller.signal.aborted || this.active?.requestId !== requestId) return { status: 'cancelled', requestId }
|
||||
if (this.currentDocumentVersion(input.documentId) !== input.documentVersion) return { status: 'stale', requestId }
|
||||
return { status: 'completed', requestId, response }
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.name === 'TimeoutError') return { status: 'timed-out', requestId }
|
||||
if (controller.signal.aborted || (error instanceof Error && error.name === 'AbortError')) return { status: 'cancelled', requestId }
|
||||
throw error
|
||||
} finally {
|
||||
if (timeout) clearTimeout(timeout)
|
||||
if (this.active?.requestId === requestId) this.active = null
|
||||
}
|
||||
}
|
||||
}
|
||||
80
src/facade/nativeHistoryProvider.ts
Normal file
80
src/facade/nativeHistoryProvider.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import type { NativeTopologyHistoryInput, NativeTopologyHistoryRecord, ShapeHandle, SubshapeRef } from './types'
|
||||
|
||||
export type NativeOcctHistoryRecord = {
|
||||
relation: 'modified' | 'generated' | 'deleted'
|
||||
source: 'object' | 'tool'
|
||||
kind: string
|
||||
sourceIndex: number
|
||||
resultIndex: number
|
||||
}
|
||||
|
||||
export type NativeOcctHistoryResponse = {
|
||||
provider: 'occt-native'
|
||||
occtVersion: string
|
||||
resultStep?: string
|
||||
records: NativeOcctHistoryRecord[]
|
||||
hasModified: boolean
|
||||
hasGenerated: boolean
|
||||
hasDeleted: boolean
|
||||
}
|
||||
|
||||
export type NativeOcctHistoryOperation = 'fuse' | 'cut' | 'common'
|
||||
|
||||
export type NativeOcctHistoryStepProvider = {
|
||||
occtVersion(): string
|
||||
booleanHistoryFromStep(objectStep: string, toolStep: string, operation: NativeOcctHistoryOperation): NativeOcctHistoryResponse
|
||||
}
|
||||
|
||||
export type NativeOcctHistoryStepGeometry = {
|
||||
exportStep(shape: ShapeHandle, fileName?: string): Promise<{ format: 'step'; fileName: string; mediaType: string; text: string }>
|
||||
}
|
||||
|
||||
const kinds: SubshapeRef['kind'][] = ['vertex', 'edge', 'face']
|
||||
const isKind = (value: string): value is SubshapeRef['kind'] => kinds.includes(value as SubshapeRef['kind'])
|
||||
|
||||
export const mapNativeOcctHistoryRecords = (
|
||||
response: NativeOcctHistoryResponse,
|
||||
sourceObjectIds: { object: string; tool: string },
|
||||
): NativeTopologyHistoryRecord[] => {
|
||||
if (response.provider !== 'occt-native' || !/^8\./.test(response.occtVersion)) throw new Error('Unsupported native OCCT history provider response.')
|
||||
return response.records.map((record) => {
|
||||
if (!isKind(record.kind)) throw new Error(`Unsupported native OCCT subshape kind: ${record.kind}`)
|
||||
if (!Number.isSafeInteger(record.sourceIndex) || record.sourceIndex < 0) throw new RangeError('Native OCCT sourceIndex must be a non-negative safe integer.')
|
||||
const sourceObjectId = sourceObjectIds[record.source]
|
||||
if (!sourceObjectId) throw new Error(`Missing native OCCT source object ID for ${record.source}.`)
|
||||
if (record.relation === 'deleted') {
|
||||
return { sourceObjectId, sourceKind: record.kind, sourceIndex: record.sourceIndex, relation: 'deleted' }
|
||||
}
|
||||
if (!Number.isSafeInteger(record.resultIndex) || record.resultIndex < 0) throw new RangeError('Native OCCT resultIndex must be a non-negative safe integer.')
|
||||
return {
|
||||
sourceObjectId,
|
||||
sourceKind: record.kind,
|
||||
sourceIndex: record.sourceIndex,
|
||||
relation: record.relation,
|
||||
resultKind: record.kind,
|
||||
resultIndexes: [record.resultIndex],
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Adapts Bitbybit STEP exports to the standalone OCCT history WASM module.
|
||||
* ShapeHandle values never cross this boundary; only versioned STEP text does.
|
||||
*/
|
||||
export const createNativeOcctStepHistoryBridge = (
|
||||
geometry: NativeOcctHistoryStepGeometry,
|
||||
provider: NativeOcctHistoryStepProvider,
|
||||
) => async (input: NativeTopologyHistoryInput): Promise<NativeTopologyHistoryRecord[]> => {
|
||||
if (!input.operation) throw new Error('Native OCCT STEP history requires a supported Boolean operation.')
|
||||
if (input.inputs.length !== 2) throw new Error('Native OCCT STEP history requires exactly an object and a tool input.')
|
||||
const [objectInput, toolInput] = input.inputs
|
||||
const [objectStep, toolStep] = await Promise.all([
|
||||
geometry.exportStep(objectInput.shape, `${objectInput.objectId}.step`),
|
||||
geometry.exportStep(toolInput.shape, `${toolInput.objectId}.step`),
|
||||
])
|
||||
if (objectStep.format !== 'step' || toolStep.format !== 'step' || typeof objectStep.text !== 'string' || typeof toolStep.text !== 'string') {
|
||||
throw new Error('Bitbybit STEP export returned an invalid transport payload.')
|
||||
}
|
||||
const response = provider.booleanHistoryFromStep(objectStep.text, toolStep.text, input.operation)
|
||||
return mapNativeOcctHistoryRecords(response, { object: objectInput.objectId, tool: toolInput.objectId })
|
||||
}
|
||||
111
src/facade/nativeHistoryWorkerClient.ts
Normal file
111
src/facade/nativeHistoryWorkerClient.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import type { NativeOcctHistoryResponse, NativeOcctHistoryStepProvider } from './nativeHistoryProvider'
|
||||
import { NATIVE_OCCT_HISTORY_PROTOCOL_VERSION, type NativeOcctHistoryCapabilities, type NativeOcctHistoryProvider, type NativeOcctHistoryProtocolResponse, type NativeOcctHistoryRequest } from './nativeHistoryProtocol'
|
||||
|
||||
type WorkerLike = Pick<Worker, 'postMessage' | 'terminate'> & {
|
||||
addEventListener(type: 'message', listener: (event: MessageEvent) => void): void
|
||||
addEventListener(type: 'error', listener: (event: ErrorEvent) => void): void
|
||||
removeEventListener(type: 'message', listener: (event: MessageEvent) => void): void
|
||||
removeEventListener(type: 'error', listener: (event: ErrorEvent) => void): void
|
||||
}
|
||||
|
||||
type WorkerResponse = { type: 'ready'; capabilities: NativeOcctHistoryCapabilities } | { type: 'response'; response: NativeOcctHistoryProtocolResponse } | { type: 'error'; requestId?: string; error: string }
|
||||
|
||||
const abortError = () => new DOMException('Native OCCT history Worker request cancelled.', 'AbortError')
|
||||
|
||||
export type NativeOcctHistoryWorkerOptions = {
|
||||
moduleUrl?: string
|
||||
initializationTimeoutMs?: number
|
||||
workerFactory?: () => WorkerLike
|
||||
}
|
||||
|
||||
const defaultWorkerFactory = () => new Worker(new URL('./nativeHistoryWorkerEntry.ts', import.meta.url), { type: 'module', name: 'occt-native-history' })
|
||||
|
||||
export class NativeOcctHistoryWorkerProvider implements NativeOcctHistoryProvider {
|
||||
private readonly worker: WorkerLike
|
||||
private readonly options: Required<Pick<NativeOcctHistoryWorkerOptions, 'moduleUrl' | 'initializationTimeoutMs'>>
|
||||
private readonly pending = new Map<string, { resolve: (response: NativeOcctHistoryProtocolResponse) => void; reject: (error: Error) => void }>()
|
||||
private initialization: Promise<NativeOcctHistoryCapabilities> | null = null
|
||||
private initializationReject: ((error: Error) => void) | null = null
|
||||
private current: NativeOcctHistoryCapabilities = { providerId: 'occt-native.history-step', providerVersion: 'unavailable', occtVersion: 'unknown', availability: 'unavailable', operations: [], transport: 'step-text', reason: 'Native OCCT history Worker has not been initialized.' }
|
||||
private readonly onMessage = (event: MessageEvent<WorkerResponse>) => {
|
||||
if (event.data.type === 'ready') {
|
||||
this.current = event.data.capabilities
|
||||
return
|
||||
}
|
||||
if (event.data.type === 'error') {
|
||||
const requestId = event.data.requestId
|
||||
const pending = requestId ? this.pending.get(requestId) : null
|
||||
if (pending && requestId) { this.pending.delete(requestId); pending.reject(new Error(event.data.error)) }
|
||||
if (!requestId) this.initializationReject?.(new Error(event.data.error))
|
||||
return
|
||||
}
|
||||
const pending = this.pending.get(event.data.response.requestId)
|
||||
if (!pending) return
|
||||
this.pending.delete(event.data.response.requestId)
|
||||
pending.resolve(event.data.response)
|
||||
}
|
||||
private readonly onError = (event: ErrorEvent) => {
|
||||
const error = new Error(event.message || 'Native OCCT history Worker failed.')
|
||||
for (const pending of this.pending.values()) pending.reject(error)
|
||||
this.pending.clear()
|
||||
this.initialization = null
|
||||
this.initializationReject?.(error)
|
||||
this.initializationReject = null
|
||||
this.current = { ...this.current, availability: 'unavailable', reason: error.message }
|
||||
}
|
||||
|
||||
constructor(options: NativeOcctHistoryWorkerOptions = {}) {
|
||||
this.worker = (options.workerFactory || defaultWorkerFactory)()
|
||||
this.options = {
|
||||
moduleUrl: options.moduleUrl || '/native/occt-history/bitbybit-occt-history.js',
|
||||
initializationTimeoutMs: options.initializationTimeoutMs ?? 120_000,
|
||||
}
|
||||
this.worker.addEventListener('message', this.onMessage)
|
||||
this.worker.addEventListener('error', this.onError)
|
||||
}
|
||||
|
||||
capabilities(): NativeOcctHistoryCapabilities { return { ...this.current, operations: [...this.current.operations] } }
|
||||
|
||||
initialize(): Promise<NativeOcctHistoryCapabilities> {
|
||||
if (this.current.availability === 'available') return Promise.resolve(this.capabilities())
|
||||
if (this.initialization) return this.initialization
|
||||
this.initialization = new Promise<NativeOcctHistoryCapabilities>((resolve, reject) => {
|
||||
let settled = false
|
||||
const timer = setTimeout(() => fail(new Error('Native OCCT history Worker initialization timed out.')), this.options.initializationTimeoutMs)
|
||||
const fail = (error: Error) => { if (settled) return; settled = true; clearTimeout(timer); this.worker.removeEventListener('message', waitForReady); this.initialization = null; this.initializationReject = null; this.current = { ...this.current, reason: error.message }; reject(error) }
|
||||
this.initializationReject = fail
|
||||
const waitForReady = (event: MessageEvent<WorkerResponse>) => { if (event.data.type === 'error') return fail(new Error(event.data.error)); if (event.data.type !== 'ready') return; settled = true; clearTimeout(timer); this.worker.removeEventListener('message', waitForReady); this.initializationReject = null; resolve(this.capabilities()) }
|
||||
this.worker.addEventListener('message', waitForReady)
|
||||
this.worker.postMessage({ type: 'initialize', moduleUrl: this.options.moduleUrl })
|
||||
})
|
||||
return this.initialization
|
||||
}
|
||||
|
||||
async capture(request: NativeOcctHistoryRequest, signal: AbortSignal): Promise<NativeOcctHistoryProtocolResponse> {
|
||||
if (request.protocolVersion !== NATIVE_OCCT_HISTORY_PROTOCOL_VERSION) throw new RangeError(`Unsupported native OCCT history protocol version: ${request.protocolVersion}.`)
|
||||
await this.initialize()
|
||||
if (signal.aborted) throw abortError()
|
||||
const response = new Promise<NativeOcctHistoryProtocolResponse>((resolve, reject) => {
|
||||
this.pending.set(request.requestId, { resolve, reject })
|
||||
this.worker.postMessage({ type: 'capture', request })
|
||||
})
|
||||
const cancel = () => { this.worker.postMessage({ type: 'cancel', requestId: request.requestId }); this.pending.get(request.requestId)?.reject(abortError()); this.pending.delete(request.requestId) }
|
||||
signal.addEventListener('abort', cancel, { once: true })
|
||||
try { return await response } finally { signal.removeEventListener('abort', cancel) }
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.worker.postMessage({ type: 'dispose' })
|
||||
this.worker.removeEventListener('message', this.onMessage)
|
||||
this.worker.removeEventListener('error', this.onError)
|
||||
this.worker.terminate()
|
||||
this.initializationReject?.(new Error('Native OCCT history Worker disposed.'))
|
||||
this.initializationReject = null
|
||||
for (const pending of this.pending.values()) pending.reject(new Error('Native OCCT history Worker disposed.'))
|
||||
this.pending.clear()
|
||||
this.initialization = null
|
||||
}
|
||||
}
|
||||
|
||||
export type NativeOcctHistoryWorkerModule = NativeOcctHistoryStepProvider & { default?: never }
|
||||
export type NativeOcctHistoryWorkerResponse = NativeOcctHistoryResponse
|
||||
49
src/facade/nativeHistoryWorkerEntry.ts
Normal file
49
src/facade/nativeHistoryWorkerEntry.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
/// <reference lib="webworker" />
|
||||
|
||||
import type { NativeOcctHistoryStepProvider } from './nativeHistoryProvider'
|
||||
import type { NativeOcctHistoryCapabilities, NativeOcctHistoryRequest, NativeOcctHistoryProtocolResponse } from './nativeHistoryProtocol'
|
||||
|
||||
type WorkerRequest = { type: 'initialize'; moduleUrl: string } | { type: 'capture'; request: NativeOcctHistoryRequest } | { type: 'cancel'; requestId: string } | { type: 'dispose' }
|
||||
type WorkerResponse = { type: 'ready'; capabilities: NativeOcctHistoryCapabilities } | { type: 'response'; response: NativeOcctHistoryProtocolResponse } | { type: 'error'; requestId?: string; error: string }
|
||||
|
||||
const scope = self as DedicatedWorkerGlobalScope
|
||||
let provider: NativeOcctHistoryStepProvider | null = null
|
||||
const cancelled = new Set<string>()
|
||||
|
||||
const send = (message: WorkerResponse) => scope.postMessage(message)
|
||||
|
||||
const initialize = async (moduleUrl: string) => {
|
||||
const imported = await import(/* @vite-ignore */ moduleUrl) as { default?: () => Promise<NativeOcctHistoryStepProvider> }
|
||||
if (typeof imported.default !== 'function') throw new Error('Native OCCT history module has no default Emscripten factory export.')
|
||||
provider = await imported.default()
|
||||
send({
|
||||
type: 'ready',
|
||||
capabilities: {
|
||||
providerId: 'occt-native.history-step',
|
||||
providerVersion: '8.0.0-embind',
|
||||
occtVersion: provider.occtVersion(),
|
||||
availability: 'available',
|
||||
operations: ['fuse', 'cut', 'common'],
|
||||
transport: 'step-text',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
scope.onmessage = ({ data }: MessageEvent<WorkerRequest>) => {
|
||||
void (async () => {
|
||||
try {
|
||||
if (data.type === 'initialize') return await initialize(data.moduleUrl)
|
||||
if (data.type === 'dispose') { provider = null; scope.close(); return }
|
||||
if (data.type === 'cancel') { cancelled.add(data.requestId); return }
|
||||
if (!provider) throw new Error('Native OCCT history Worker is not initialized.')
|
||||
if (cancelled.delete(data.request.requestId)) return
|
||||
const history = provider.booleanHistoryFromStep(data.request.objectStep, data.request.toolStep, data.request.operation)
|
||||
if (cancelled.delete(data.request.requestId)) return
|
||||
send({ type: 'response', response: { protocolVersion: data.request.protocolVersion, requestId: data.request.requestId, documentId: data.request.documentId, documentVersion: data.request.documentVersion, operationId: data.request.operationId, provider: { providerId: 'occt-native.history-step', providerVersion: '8.0.0-embind', occtVersion: history.occtVersion, availability: 'available', operations: ['fuse', 'cut', 'common'], transport: 'step-text' }, history } })
|
||||
} catch (error) {
|
||||
send({ type: 'error', requestId: data.type === 'capture' ? data.request.requestId : undefined, error: error instanceof Error ? error.message : String(error) })
|
||||
}
|
||||
})()
|
||||
}
|
||||
|
||||
export {}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { DependencyGraph, type RecomputeState } from './dependencyGraph'
|
||||
import { cloneSketch, solveSketch } from './sketcher'
|
||||
import type { ApplyPlacementInput, BooleanCutInput, BooleanIntersectionInput, BooleanUnionInput, ChamferInput, CreateBoxInput, CreateConeInput, CreateCylinderInput, CreateSphereInput, DocumentObjectSnapshot, DocumentSnapshot, FilletInput, MirrorInput, MultiTransformStep, ObjectTopologySnapshot, PadInput, PlanarProfile, PocketInput, RevolutionInput, ShapeHandle, SubshapeTopology } from './types'
|
||||
import { captureSignatureTopologyHistory } from './topologyHistory'
|
||||
import { captureNativeTopologyHistory, captureSignatureTopologyHistory } from './topologyHistory'
|
||||
import { migrateTopoRefs } from './topologyReferences'
|
||||
|
||||
export type RecomputeExecutionStatus = 'completed' | 'failed' | 'cancelled' | 'stale'
|
||||
@@ -49,6 +49,7 @@ export type RecomputeGeometryRuntime = {
|
||||
fillet(input: FilletInput): Promise<ShapeHandle>
|
||||
chamfer(input: ChamferInput): Promise<ShapeHandle>
|
||||
topology?(shape: ShapeHandle, precision?: number): Promise<SubshapeTopology>
|
||||
topologyHistory?(input: { documentId: string; documentVersion: number; operationId: string; operation?: 'fuse' | 'cut' | 'common'; inputs: Array<{ objectId: string; shape: ShapeHandle }>; result: ShapeHandle }): Promise<import('./types').NativeTopologyHistoryRecord[]>
|
||||
release(shape: ShapeHandle): Promise<void>
|
||||
}
|
||||
|
||||
@@ -303,6 +304,8 @@ const placementForObject = (object: DocumentObjectSnapshot) => {
|
||||
const topologyForObject = async (
|
||||
geometry: RecomputeGeometryRuntime,
|
||||
object: DocumentObjectSnapshot,
|
||||
document: DocumentSnapshot,
|
||||
shapes: Map<string, ShapeHandle>,
|
||||
shape: ShapeHandle,
|
||||
context: RecomputeNodeContext,
|
||||
): Promise<ObjectTopologySnapshot | undefined> => {
|
||||
@@ -319,11 +322,28 @@ const topologyForObject = async (
|
||||
},
|
||||
signature: { ...entry.signature, centroid: [...entry.signature.centroid], bounds: { min: [...entry.signature.bounds.min], max: [...entry.signature.bounds.max] }, normal: [...entry.signature.normal] },
|
||||
})) as ObjectTopologySnapshot['entries']
|
||||
const history = captureSignatureTopologyHistory(
|
||||
`${object.id}:generation:${context.generation}`,
|
||||
previous.length > 0 ? [{ objectId: object.id, entries: previous }] : [],
|
||||
entries,
|
||||
)
|
||||
const operationId = `${object.id}:generation:${context.generation}`
|
||||
const linkedSources = ['Base', 'Tool', 'Profile']
|
||||
.map((propertyName) => linkedObject(object, propertyName, document))
|
||||
.filter((candidate): candidate is DocumentObjectSnapshot => Boolean(candidate))
|
||||
const sourceObjects = [...new Map(linkedSources.map((source) => [source.id, source])).values()]
|
||||
const nativeInputs = sourceObjects
|
||||
.map((source) => ({ source, shape: shapes.get(source.id) }))
|
||||
.filter((entry): entry is { source: DocumentObjectSnapshot; shape: ShapeHandle } => Boolean(entry.shape && entry.source.topology?.entries.length))
|
||||
const nativeOperation = object.typeId === 'Part::Fuse' ? 'fuse' : object.typeId === 'Part::Cut' ? 'cut' : object.typeId === 'Part::Common' ? 'common' : undefined
|
||||
const nativeRecords = geometry.topologyHistory && nativeOperation && nativeInputs.length > 0
|
||||
? await geometry.topologyHistory({
|
||||
documentId: context.documentId,
|
||||
documentVersion: context.documentVersion,
|
||||
operationId,
|
||||
operation: nativeOperation,
|
||||
inputs: nativeInputs.map(({ source, shape: inputShape }) => ({ objectId: source.id, shape: inputShape })),
|
||||
result: shape,
|
||||
})
|
||||
: undefined
|
||||
const history = nativeRecords
|
||||
? captureNativeTopologyHistory(operationId, nativeInputs.map(({ source }) => ({ objectId: source.id, entries: source.topology!.entries })), entries, nativeRecords)
|
||||
: captureSignatureTopologyHistory(operationId, previous.length > 0 ? [{ objectId: object.id, entries: previous }] : [], entries)
|
||||
return {
|
||||
shapeId: shape.id,
|
||||
documentVersion: context.documentVersion,
|
||||
@@ -597,7 +617,7 @@ export const createFacadeGeometryRecomputeExecutor = (
|
||||
}
|
||||
let topology: ObjectTopologySnapshot | undefined
|
||||
try {
|
||||
topology = await topologyForObject(geometry, object, result, context)
|
||||
topology = await topologyForObject(geometry, object, document, shapes, result, context)
|
||||
} catch (error) {
|
||||
await Promise.allSettled([geometry.release(result)])
|
||||
throw error
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { SubshapeRef, TopologyHistoryRelation as StoredTopologyHistoryRelation, TopologyHistoryResult as StoredTopologyHistoryResult } from './types'
|
||||
import type { NativeTopologyHistoryRecord, SubshapeRef, TopologyHistoryRelation as StoredTopologyHistoryRelation, TopologyHistoryResult as StoredTopologyHistoryResult } from './types'
|
||||
import { matchSubshapes, type SubshapeSignature } from './topologyNaming'
|
||||
|
||||
export type TopologyHistoryEntry = { ref: SubshapeRef; signature: SubshapeSignature }
|
||||
@@ -7,6 +7,53 @@ export type TopologyHistoryRelation = StoredTopologyHistoryRelation
|
||||
export type TopologyHistoryResult = StoredTopologyHistoryResult
|
||||
|
||||
const sourceKey = (objectId: string, persistentId: string) => `${encodeURIComponent(objectId)}::${persistentId}`
|
||||
const emptyCounts = (): TopologyHistoryResult['counts'] => ({ preserved: 0, modified: 0, generated: 0, deleted: 0, ambiguous: 0 })
|
||||
const entriesOfKind = (entries: TopologyHistoryEntry[], kind: SubshapeRef['kind']) => entries.filter((entry) => entry.ref.kind === kind)
|
||||
|
||||
export const captureNativeTopologyHistory = (
|
||||
operationId: string,
|
||||
inputs: Array<{ objectId: string; entries: TopologyHistoryEntry[] }>,
|
||||
output: TopologyHistoryEntry[],
|
||||
records: NativeTopologyHistoryRecord[],
|
||||
): TopologyHistoryResult => {
|
||||
if (!operationId.trim()) throw new RangeError('Topology history operationId is required.')
|
||||
const inputById = new Map(inputs.map((input) => [input.objectId, input]))
|
||||
if (inputById.size !== inputs.length) throw new RangeError('Native topology history input object IDs must be unique.')
|
||||
const counts = emptyCounts()
|
||||
const relations: TopologyHistoryRelation[] = []
|
||||
const seen = new Set<string>()
|
||||
|
||||
for (const record of records) {
|
||||
const input = inputById.get(record.sourceObjectId)
|
||||
if (!input) throw new RangeError(`Native topology history references unknown input object ${record.sourceObjectId}.`)
|
||||
const sources = entriesOfKind(input.entries, record.sourceKind)
|
||||
if (!Number.isSafeInteger(record.sourceIndex) || record.sourceIndex < 0 || record.sourceIndex >= sources.length) throw new RangeError(`Native topology history source index is out of range for ${record.sourceObjectId} ${record.sourceKind}.`)
|
||||
const source = sources[record.sourceIndex]
|
||||
if (record.relation === 'deleted') {
|
||||
if (record.resultIndexes?.length) throw new RangeError('Deleted native topology history records cannot reference result indexes.')
|
||||
const key = `${record.sourceObjectId}:${source.ref.persistentId}:deleted`
|
||||
if (!seen.has(key)) {
|
||||
seen.add(key)
|
||||
relations.push({ relation: 'deleted', sourceObjectId: record.sourceObjectId, sourcePersistentId: source.ref.persistentId, score: 1 })
|
||||
counts.deleted += 1
|
||||
}
|
||||
continue
|
||||
}
|
||||
const resultKind = record.resultKind ?? record.sourceKind
|
||||
const results = entriesOfKind(output, resultKind)
|
||||
if (!record.resultIndexes?.length) throw new RangeError(`${record.relation} native topology history records require result indexes.`)
|
||||
for (const resultIndex of record.resultIndexes) {
|
||||
if (!Number.isSafeInteger(resultIndex) || resultIndex < 0 || resultIndex >= results.length) throw new RangeError(`Native topology history result index is out of range for ${resultKind}.`)
|
||||
const result = results[resultIndex]
|
||||
const key = `${record.sourceObjectId}:${source.ref.persistentId}:${record.relation}:${result.ref.persistentId}`
|
||||
if (seen.has(key)) continue
|
||||
seen.add(key)
|
||||
relations.push({ relation: record.relation, sourceObjectId: record.sourceObjectId, sourcePersistentId: source.ref.persistentId, resultPersistentId: result.ref.persistentId, score: 1 })
|
||||
counts[record.relation] += 1
|
||||
}
|
||||
}
|
||||
return { operationId, provider: 'occt-native', relations, counts }
|
||||
}
|
||||
|
||||
export const captureSignatureTopologyHistory = (
|
||||
operationId: string,
|
||||
@@ -21,7 +68,7 @@ export const captureSignatureTopologyHistory = (
|
||||
return { ref: { ...entry.ref, persistentId: key }, signature: entry.signature }
|
||||
}))
|
||||
const matches = matchSubshapes(previous, output)
|
||||
const counts: TopologyHistoryResult['counts'] = { preserved: 0, modified: 0, generated: 0, deleted: 0, ambiguous: 0 }
|
||||
const counts = emptyCounts()
|
||||
const relations = matches.map((match): TopologyHistoryRelation => {
|
||||
if (match.status === 'new') return { relation: 'generated', resultPersistentId: match.current.persistentId, score: match.score }
|
||||
if (match.status === 'deleted') {
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { Quantity, QuantityDimension } from './units'
|
||||
import type { SketchConstraint, SketchExternalGeometry, SketchGeometry, SketchSnapshot, SketchSolveResult } from './sketcher'
|
||||
import type { RecomputeExecutionOptions, RecomputeExecutionResult } from './recomputeEngine'
|
||||
import type { FcstdArchiveLimits, FcstdInspection } from './fcstd'
|
||||
import type { NativeOcctHistoryCapabilities, NativeOcctHistoryProvider } from './nativeHistoryProtocol'
|
||||
|
||||
export type ModelTreeItem = {
|
||||
id: string
|
||||
@@ -221,11 +222,27 @@ export type TopologyHistoryRelation = {
|
||||
|
||||
export type TopologyHistoryResult = {
|
||||
operationId: string
|
||||
provider: 'signature-fallback'
|
||||
provider: 'occt-native' | 'signature-fallback'
|
||||
relations: TopologyHistoryRelation[]
|
||||
counts: Record<TopologyHistoryRelation['relation'], number>
|
||||
}
|
||||
|
||||
export type NativeTopologyHistoryRecord = {
|
||||
sourceObjectId: string
|
||||
sourceKind: SubshapeRef['kind']
|
||||
sourceIndex: number
|
||||
relation: 'preserved' | 'modified' | 'generated' | 'deleted'
|
||||
resultKind?: SubshapeRef['kind']
|
||||
resultIndexes?: number[]
|
||||
}
|
||||
|
||||
export type NativeTopologyHistoryInput = GeometryDocumentContext & {
|
||||
operationId: string
|
||||
operation?: 'fuse' | 'cut' | 'common'
|
||||
inputs: Array<{ objectId: string; shape: ShapeHandle }>
|
||||
result: ShapeHandle
|
||||
}
|
||||
|
||||
export type ObjectTopologySnapshot = {
|
||||
shapeId: string
|
||||
documentVersion: number
|
||||
@@ -555,6 +572,8 @@ export interface BitBybitWebCadFacade {
|
||||
}
|
||||
readonly geometry: {
|
||||
capabilities(): GeometryCapabilities
|
||||
configureNativeHistory?(provider: NativeOcctHistoryProvider | null, timeoutMs?: number): void
|
||||
nativeHistoryCapabilities?(): NativeOcctHistoryCapabilities
|
||||
initialize(): Promise<GeometryCapabilities>
|
||||
createBox(input: CreateBoxInput): Promise<ShapeHandle>
|
||||
createCylinder(input: CreateCylinderInput): Promise<ShapeHandle>
|
||||
@@ -575,6 +594,7 @@ export interface BitBybitWebCadFacade {
|
||||
mesh(shape: ShapeHandle, precision?: number): Promise<MeshAsset>
|
||||
subshapes(shape: ShapeHandle, precision?: number): Promise<SubshapeRef[]>
|
||||
topology(shape: ShapeHandle, precision?: number): Promise<SubshapeTopology>
|
||||
topologyHistory?(input: NativeTopologyHistoryInput): Promise<NativeTopologyHistoryRecord[]>
|
||||
getObjectShape(objectId: string): ShapeHandle | null
|
||||
release(shape: ShapeHandle): Promise<void>
|
||||
dispose(): void
|
||||
|
||||
Reference in New Issue
Block a user