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, CreateEllipsoidInput, CreateHelixInput, CreatePrismInput, CreateSphereInput, CreateTorusInput, CreateWedgeInput, DraftInput, ExtrudeInput, FilletInput, GeometryCapabilities, GeometryDocumentContext, GeometryFileExport, GeometryFileImport, GrooveInput, LinearFeatureParameters, LoftInput, MeshAsset, MirrorInput, ModeledThreadInput, NativeTopologyHistoryInput, NativeTopologyHistoryRecord, NativeTopologyHistoryRecords, NativeTopologyHistoryStageCaptureResult, PadInput, PipeInput, PlanarProfile, PocketInput, Point3, ProfileClassification, RevolutionInput, ShapeHandle, ShapeMassProperties, ShapeQualityReport, SubshapeRef, SubshapeTopology, ThicknessInput } from './types' import { mapNativeOcctHistoryRecords, type NativeOcctHistoryOperation, type NativeOcctHistoryResponse } from './nativeHistoryProvider' import { NativeOcctHistoryCoordinator, type NativeOcctHistoryProvider, type NativeOcctHistoryRequest } from './nativeHistoryProtocol' import { assertNativeNamingEvidence, createFinalShapeOnlyNamingEvidence } from './nativeNamingEvidence' import { createAnalyticSubshapeRefs, createEdgeSubshapeRefs, createSubshapeRefs, createTopologyAdjacency, createVertexSubshapeRefs, signatureForEdge, signatureForVertex, type AnalyticEdgeInput, type AnalyticFaceInput, type AnalyticVertexInput } from './topologyNaming' type KernelShapeReference = Inputs.OCCT.TopoDSShapePointer type KernelMesh = Inputs.OCCT.DecomposedMeshDto type ShapeEntry = { handle: ShapeHandle; reference: KernelShapeReference } type KernelReferenceEntry = { count: number; reference: KernelShapeReference } type AnalyticTopologyDescription = { faces?: AnalyticFaceInput[] edges?: AnalyticEdgeInput[] vertices?: AnalyticVertexInput[] adjacency?: { faceNeighbors?: number[][]; faceEdges?: number[][]; edgeFaces?: number[][]; edgeVertices?: number[][]; vertexEdges?: number[][] } } const unavailableCapabilities = (): GeometryCapabilities => ({ provider: 'Bitbybit OCCT', version: '1.1.1', status: typeof Worker === 'undefined' ? 'unavailable' : 'idle', worker: typeof Worker !== 'undefined', wasm: typeof WebAssembly !== 'undefined', shapeCount: 0, kernelReferenceCount: 0, releasedShapeCount: 0, peakShapeCount: 0, peakKernelReferenceCount: 0, reason: typeof Worker === 'undefined' ? 'Web Workers are not available in this runtime.' : undefined, }) const finitePositive = (value: number, name: string) => { if (!Number.isFinite(value) || value <= 0) throw new RangeError(`${name} must be a finite number greater than zero.`) } const finiteNonNegative = (value: number, name: string) => { if (!Number.isFinite(value) || value < 0) throw new RangeError(`${name} must be a finite non-negative number.`) } export const MAX_GEOMETRY_IMPORT_TEXT_BYTES = 128 * 1024 * 1024 export const collectGeometryImportText = async (chunks: AsyncIterable, signal?: AbortSignal, maxBytes = MAX_GEOMETRY_IMPORT_TEXT_BYTES): Promise => { if (!Number.isSafeInteger(maxBytes) || maxBytes <= 0) throw new RangeError('Geometry import stream maxBytes must be a positive safe integer.') const decoder = new TextDecoder('utf-8', { fatal: true }) let total = 0 let text = '' for await (const chunk of chunks) { if (signal?.aborted) throw new DOMException('Geometry import stream cancelled.', 'AbortError') if (!(chunk instanceof Uint8Array)) throw new TypeError('Geometry import stream chunks must be Uint8Array values.') total += chunk.byteLength if (total > maxBytes) throw new RangeError(`Geometry import payload exceeds ${maxBytes} bytes.`) text += decoder.decode(chunk, { stream: true }) } if (signal?.aborted) throw new DOMException('Geometry import stream cancelled.', 'AbortError') text += decoder.decode() if (!text.trim()) throw new TypeError('Geometry import text must be non-empty.') return text } const validateDocumentContext = (input: GeometryDocumentContext) => { if (!input.documentId.trim()) throw new RangeError('documentId must be a non-empty string.') if (!Number.isSafeInteger(input.documentVersion) || input.documentVersion < 0) throw new RangeError('documentVersion must be a non-negative safe integer.') } const validateVector = (value: [number, number, number], name: string, allowZero = true) => { if (value.length !== 3 || value.some((coordinate) => !Number.isFinite(coordinate))) throw new RangeError(`${name} must contain three finite coordinates.`) if (!allowZero && value.every((coordinate) => coordinate === 0)) throw new RangeError(`${name} must not be the zero vector.`) } const validateAngle = (value: number, name: string, allowZero = false) => { if (!Number.isFinite(value) || value > 360 || value < (allowZero ? 0 : Number.EPSILON)) throw new RangeError(`${name} must be between ${allowZero ? '0' : '0 (exclusive)'} and 360 degrees.`) } export const validateBoxInput = (input: CreateBoxInput) => { finitePositive(input.width, 'width') finitePositive(input.length, 'length') finitePositive(input.height, 'height') validateDocumentContext(input) const center = input.center ?? [0, 0, 0] validateVector(center, 'center') } export const validateCylinderInput = (input: CreateCylinderInput) => { finitePositive(input.radius, 'radius') finitePositive(input.height, 'height') validateVector(input.center ?? [0, 0, 0], 'center') validateVector(input.direction ?? [0, 1, 0], 'direction', false) validateAngle(input.angle ?? 360, 'angle') validateDocumentContext(input) } export const validateSphereInput = (input: CreateSphereInput) => { finitePositive(input.radius, 'radius') validateVector(input.center ?? [0, 0, 0], 'center') validateDocumentContext(input) } export const validateEllipsoidInput = (input: CreateEllipsoidInput) => { validateDocumentContext(input) finitePositive(input.radius1, 'radius1') finitePositive(input.radius2, 'radius2') if (input.radius3 !== undefined && (!Number.isFinite(input.radius3) || input.radius3 < 0)) throw new RangeError('radius3 must be finite and non-negative.') const angle1 = input.angle1 ?? -90 const angle2 = input.angle2 ?? 90 const angle3 = input.angle3 ?? 360 if (!Number.isFinite(angle1) || angle1 < -90 || angle1 > 90) throw new RangeError('angle1 must be between -90 and 90 degrees.') if (!Number.isFinite(angle2) || angle2 < -90 || angle2 > 90 || angle1 >= angle2) throw new RangeError('angle2 must be between -90 and 90 degrees and greater than angle1.') if (!Number.isFinite(angle3) || angle3 <= 0 || angle3 > 360) throw new RangeError('angle3 must be greater than zero and no more than 360 degrees.') validateVector(input.center ?? [0, 0, 0], 'center') } export const validateConeInput = (input: CreateConeInput) => { finiteNonNegative(input.radius1, 'radius1') finiteNonNegative(input.radius2, 'radius2') if (input.radius1 === 0 && input.radius2 === 0) throw new RangeError('At least one cone radius must be greater than zero.') finitePositive(input.height, 'height') validateVector(input.center ?? [0, 0, 0], 'center') validateVector(input.direction ?? [0, 1, 0], 'direction', false) validateAngle(input.angle ?? 360, 'angle') validateDocumentContext(input) } export const validateTorusInput = (input: CreateTorusInput) => { finitePositive(input.majorRadius, 'majorRadius') finitePositive(input.minorRadius, 'minorRadius') validateVector(input.center ?? [0, 0, 0], 'center') validateVector(input.direction ?? [0, 1, 0], 'direction', false) validateAngle(input.angle ?? 360, 'angle') validateDocumentContext(input) } export const validateHelixInput = (input: CreateHelixInput) => { validateDocumentContext(input) finitePositive(input.pitch, 'pitch') finitePositive(input.height, 'height') finitePositive(input.radius, 'radius') if (input.height / input.pitch > 10000) throw new RangeError('helix turn count must not exceed 10000.') const angle = input.angle ?? 0 if (!Number.isFinite(angle) || angle <= -89.99999 || angle >= 89.99999) throw new RangeError('angle must be between -89.99999 and 89.99999 degrees (exclusive).') const endRadius = input.radius + input.height * Math.tan(angle * Math.PI / 180) if (!(endRadius > 0) || !Number.isFinite(endRadius)) throw new RangeError('helix end radius must be finite and greater than zero.') validateVector(input.center ?? [0, 0, 0], 'center') validateVector(input.direction ?? [0, 0, 1], 'direction', false) if (input.tolerance !== undefined) finitePositive(input.tolerance, 'tolerance') } export const validatePrismInput = (input: CreatePrismInput) => { validateDocumentContext(input) if (!Number.isSafeInteger(input.polygon) || input.polygon < 3 || input.polygon > 10000) throw new RangeError('polygon must be an integer between 3 and 10000.') finitePositive(input.circumradius, 'circumradius') finitePositive(input.height, 'height') const firstAngle = input.firstAngle ?? 0 const secondAngle = input.secondAngle ?? 0 if (!Number.isFinite(firstAngle) || firstAngle < -89.99999 || firstAngle > 89.99999) throw new RangeError('firstAngle must be between -89.99999 and 89.99999 degrees.') if (!Number.isFinite(secondAngle) || secondAngle < -89.99999 || secondAngle > 89.99999) throw new RangeError('secondAngle must be between -89.99999 and 89.99999 degrees.') validateVector(input.center ?? [0, 0, 0], 'center') } export const validateWedgeInput = (input: CreateWedgeInput) => { validateDocumentContext(input) for (const name of ['xmin', 'ymin', 'zmin', 'z2min', 'x2min', 'xmax', 'ymax', 'zmax', 'z2max', 'x2max'] as const) { if (!Number.isFinite(input[name])) throw new RangeError(`${name} must be finite.`) } if (!(input.xmax - input.xmin > Number.EPSILON)) throw new RangeError('xmax - xmin must be greater than zero.') if (!(input.ymax - input.ymin > Number.EPSILON)) throw new RangeError('ymax - ymin must be greater than zero.') if (!(input.zmax - input.zmin > Number.EPSILON)) throw new RangeError('zmax - zmin must be greater than zero.') if (input.z2max - input.z2min < 0) throw new RangeError('z2max - z2min must not be negative.') if (input.x2max - input.x2min < 0) throw new RangeError('x2max - x2min must not be negative.') validateVector(input.center ?? [0, 0, 0], 'center') } export const validatePlacementInput = (input: ApplyPlacementInput) => { validateVector(input.placement.translation, 'translation') validateVector(input.placement.rotationAxis, 'rotationAxis', false) validateAngle(input.placement.rotationAngle, 'rotationAngle', true) validateDocumentContext(input) validateShapeContext(input, input.shape) } export const validateMirrorInput = (input: MirrorInput) => { validateDocumentContext(input) validateShapeContext(input, input.shape) validateVector(input.origin, 'origin') validateVector(input.normal, 'normal', false) } const validateShapeContext = (input: GeometryDocumentContext, shape: ShapeHandle) => { if (shape.documentId !== input.documentId) throw new Error(`Shape belongs to another document: ${shape.id}`) if (shape.documentVersion > input.documentVersion) throw new Error(`Shape version is newer than the result context: ${shape.id}`) } const validateBooleanShapes = (input: GeometryDocumentContext, shapes: ShapeHandle[], minimum: number) => { validateDocumentContext(input) if (shapes.length < minimum) throw new RangeError(`Boolean operation requires at least ${minimum} shape${minimum === 1 ? '' : 's'}.`) for (const shape of shapes) validateShapeContext(input, shape) } export const validateBooleanUnionInput = (input: BooleanUnionInput) => validateBooleanShapes(input, input.shapes, 2) export const validateBooleanCutInput = (input: BooleanCutInput) => { validateBooleanShapes(input, [input.base, ...input.tools], 2) if (input.tools.length === 0) throw new RangeError('Boolean cut requires at least one tool shape.') } export const validateBooleanIntersectionInput = (input: BooleanIntersectionInput) => validateBooleanShapes(input, input.shapes, 2) const validateEdgeFeature = (input: GeometryDocumentContext & { base: ShapeHandle; indexes?: number[]; radius?: number; distance?: number }, value: number, name: string) => { validateDocumentContext(input) validateShapeContext(input, input.base) finitePositive(value, name) if (input.indexes?.some((index) => !Number.isSafeInteger(index) || index < 0)) throw new RangeError('Edge indexes must be non-negative safe integers.') } export const validateFilletInput = (input: FilletInput) => validateEdgeFeature(input, input.radius, 'radius') export const validateChamferInput = (input: ChamferInput) => validateEdgeFeature(input, input.distance, 'distance') export const validateDraftInput = (input: DraftInput) => { validateDocumentContext(input) validateShapeContext(input, input.base) if (!Number.isFinite(input.angle) || input.angle <= -89.999 || input.angle >= 89.999 || input.angle === 0) throw new RangeError('Draft angle must be finite, non-zero and between -89.999 and 89.999 degrees.') validateVector(input.direction ?? [0, 1, 0], 'direction', false) validateVector(input.neutralPlaneOrigin ?? [0, 0, 0], 'neutralPlaneOrigin') validateVector(input.neutralPlaneDirection ?? [0, 0, 1], 'neutralPlaneDirection', false) if (input.indexes?.some((index) => !Number.isSafeInteger(index) || index < 0)) throw new RangeError('Draft face indexes must be non-negative safe integers.') } export const validateThicknessInput = (input: ThicknessInput) => { validateDocumentContext(input) validateShapeContext(input, input.base) if (!Number.isFinite(input.offset) || input.offset === 0) throw new RangeError('Thickness offset must be finite and non-zero.') if (input.removeFaceIndexes?.some((index) => !Number.isSafeInteger(index) || index < 0)) throw new RangeError('Thickness remove-face indexes must be non-negative safe integers.') if (input.joinType !== undefined && input.joinType !== 'Arc' && input.joinType !== 'Intersection') throw new RangeError(`Unsupported Thickness join type: ${String(input.joinType)}`) } const samePoint = (left: Point3, right: Point3, tolerance = 1e-9) => left.every((coordinate, axis) => Math.abs(coordinate - right[axis]) <= tolerance) const subtract = (left: Point3, right: Point3): Point3 => [left[0] - right[0], left[1] - right[1], left[2] - right[2]] const cross = (left: Point3, right: Point3): Point3 => [left[1] * right[2] - left[2] * right[1], left[2] * right[0] - left[0] * right[2], left[0] * right[1] - left[1] * right[0]] const dot = (left: Point3, right: Point3) => left[0] * right[0] + left[1] * right[1] + left[2] * right[2] const magnitude = (value: Point3) => Math.hypot(value[0], value[1], value[2]) const normalizedRing = (ring: Point3[]) => ring.length > 1 && samePoint(ring[0], ring.at(-1) as Point3) ? ring.slice(0, -1) : [...ring] const offsetProfileRing = (ring: Point3[], direction: Point3, distance: number): Point3[] => { const points = normalizedRing(ring) const directionLength = magnitude(direction) const normal = direction.map((coordinate) => coordinate / directionLength) as Point3 const origin = points[0] const firstEdge = subtract(points[1], origin) const firstEdgeLength = magnitude(firstEdge) const u = firstEdge.map((coordinate) => coordinate / firstEdgeLength) as Point3 const v = cross(normal, u) const projected = points.map((point): Point2 => { const relative = subtract(point, origin) return [dot(relative, u), dot(relative, v)] }) const signedArea = projected.reduce((sum, point, index) => { const next = projected[(index + 1) % projected.length] return sum + point[0] * next[1] - next[0] * point[1] }, 0) / 2 if (Math.abs(signedArea) <= 1e-9) throw new RangeError('Tapered profile outer ring is degenerate.') const orientation = Math.sign(signedArea) const shifted = projected.map((point, index) => { const next = projected[(index + 1) % projected.length] const edge: Point2 = [next[0] - point[0], next[1] - point[1]] const edgeLength = Math.hypot(...edge) if (edgeLength <= 1e-9) throw new RangeError('Tapered profile contains a degenerate edge.') const inward: Point2 = [-edge[1] / edgeLength * orientation, edge[0] / edgeLength * orientation] return { point: [point[0] + inward[0] * distance, point[1] + inward[1] * distance] as Point2, direction: edge } }) const top = shifted.map((current, index): Point2 => { const previous = shifted[(index + shifted.length - 1) % shifted.length] const denominator = previous.direction[0] * current.direction[1] - previous.direction[1] * current.direction[0] if (Math.abs(denominator) <= 1e-10) throw new RangeError('Tapered profile contains adjacent parallel edges that cannot be offset.') const delta: Point2 = [current.point[0] - previous.point[0], current.point[1] - previous.point[1]] const parameter = (delta[0] * current.direction[1] - delta[1] * current.direction[0]) / denominator return [previous.point[0] + previous.direction[0] * parameter, previous.point[1] + previous.direction[1] * parameter] }) const topArea = top.reduce((sum, point, index) => { const next = top[(index + 1) % top.length] return sum + point[0] * next[1] - next[0] * point[1] }, 0) / 2 if (Math.sign(topArea) !== orientation || Math.abs(topArea) <= 1e-9) throw new RangeError('Taper angle collapses or inverts the profile.') return top.map((point) => [ origin[0] + u[0] * point[0] + v[0] * point[1] + direction[0], origin[1] + u[1] * point[0] + v[1] * point[1] + direction[1], origin[2] + u[2] * point[0] + v[2] * point[1] + direction[2], ]) } const ringNormal = (ring: Point3[]): Point3 | null => { const origin = ring[0] for (let index = 1; index < ring.length - 1; index += 1) { const candidate = cross(subtract(ring[index], origin), subtract(ring[index + 1], origin)) if (magnitude(candidate) > 1e-9) return candidate } return null } type Point2 = [number, number] const projectRing = (ring: Point3[], normal: Point3): Point2[] => { const axis = normal.reduce((best, value, index) => Math.abs(value) > Math.abs(normal[best]) ? index : best, 0) return ring.map((point) => axis === 0 ? [point[1], point[2]] : axis === 1 ? [point[0], point[2]] : [point[0], point[1]]) } const orientation2d = (a: Point2, b: Point2, c: Point2) => (b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0]) const onSegment2d = (a: Point2, b: Point2, point: Point2, tolerance = 1e-9) => Math.abs(orientation2d(a, b, point)) <= tolerance && point[0] >= Math.min(a[0], b[0]) - tolerance && point[0] <= Math.max(a[0], b[0]) + tolerance && point[1] >= Math.min(a[1], b[1]) - tolerance && point[1] <= Math.max(a[1], b[1]) + tolerance const segmentsIntersect2d = (a: Point2, b: Point2, c: Point2, d: Point2) => { const abC = orientation2d(a, b, c) const abD = orientation2d(a, b, d) const cdA = orientation2d(c, d, a) const cdB = orientation2d(c, d, b) if (((abC > 1e-9 && abD < -1e-9) || (abC < -1e-9 && abD > 1e-9)) && ((cdA > 1e-9 && cdB < -1e-9) || (cdA < -1e-9 && cdB > 1e-9))) return true return onSegment2d(a, b, c) || onSegment2d(a, b, d) || onSegment2d(c, d, a) || onSegment2d(c, d, b) } const countRingIntersections = (left: Point2[], right: Point2[] = left, sameRing = true) => { let intersections = 0 for (let leftIndex = 0; leftIndex < left.length; leftIndex += 1) { const leftNext = (leftIndex + 1) % left.length for (let rightIndex = sameRing ? leftIndex + 1 : 0; rightIndex < right.length; rightIndex += 1) { const rightNext = (rightIndex + 1) % right.length if (sameRing && (leftNext === rightIndex || rightNext === leftIndex)) continue if (segmentsIntersect2d(left[leftIndex], left[leftNext], right[rightIndex], right[rightNext])) intersections += 1 } } return intersections } const pointInRing2d = (point: Point2, ring: Point2[]) => { let inside = false for (let index = 0, previous = ring.length - 1; index < ring.length; previous = index, index += 1) { const left = ring[index] const right = ring[previous] if ((left[1] > point[1]) !== (right[1] > point[1]) && point[0] < (right[0] - left[0]) * (point[1] - left[1]) / (right[1] - left[1]) + left[0]) inside = !inside } return inside } export const classifyPlanarProfile = (profile: PlanarProfile): ProfileClassification => { const regions = [ { outer: normalizedRing(profile.outer), holes: (profile.holes ?? []).map(normalizedRing) }, ...(profile.additionalRegions ?? []).map((region) => ({ outer: normalizedRing(region.outer), holes: (region.holes ?? []).map(normalizedRing) })), ] const rings = regions.flatMap((region) => [region.outer, ...region.holes]) if (profile.closed === false) return { status: 'open', ringCount: rings.length, selfIntersections: 0 } if (rings.some((ring) => ring.length < 3 || !ringNormal(ring))) return { status: 'degenerate', ringCount: rings.length, selfIntersections: 0 } const normal = ringNormal(rings[0]) as Point3 const projectedRegions = regions.map((region) => ({ outer: projectRing(region.outer, normal), holes: region.holes.map((ring) => projectRing(ring, normal)) })) const selfIntersections = projectedRegions.reduce((count, region) => { const projected = [region.outer, ...region.holes] return count + projected.reduce((inner, ring) => inner + countRingIntersections(ring), 0) + projected.reduce((inner, ring, index) => inner + projected.slice(index + 1).reduce((sum, other) => sum + countRingIntersections(ring, other, false), 0), 0) }, 0) if (selfIntersections > 0) return { status: 'self-intersecting', ringCount: rings.length, selfIntersections } const invalidNesting = projectedRegions.some((region) => region.holes.some((hole, holeIndex) => !pointInRing2d(hole[0], region.outer) || region.holes.some((other, otherIndex) => otherIndex !== holeIndex && pointInRing2d(hole[0], other)))) if (invalidNesting) return { status: 'invalid-nesting', ringCount: rings.length, selfIntersections: 0 } const origin = rings[0][0] const normalLength = magnitude(normal) if (rings.some((ring) => ring.some((point) => Math.abs(dot(normal, subtract(point, origin))) / normalLength > 1e-7))) return { status: 'non-planar', ringCount: rings.length, selfIntersections: 0 } return { status: rings.length > 1 ? 'multi-ring' : 'closed', ringCount: rings.length, selfIntersections: 0 } } export const validatePlanarProfile = (profile: PlanarProfile) => { const classification = classifyPlanarProfile(profile) if (classification.status === 'open') throw new RangeError('Profile ring 0 is open.') if (classification.status === 'self-intersecting') throw new RangeError('Profile contains self-intersecting edges.') if (classification.status === 'invalid-nesting') throw new RangeError('Profile holes must be contained directly inside the outer ring.') const rings = [ normalizedRing(profile.outer), ...(profile.holes ?? []).map(normalizedRing), ...(profile.additionalRegions ?? []).flatMap((region) => [normalizedRing(region.outer), ...(region.holes ?? []).map(normalizedRing)]), ] for (const [ringIndex, ring] of rings.entries()) { if (ring.length < 3) throw new RangeError(`Profile ring ${ringIndex} requires at least three distinct points.`) ring.forEach((point, pointIndex) => { validateVector(point, `profile ring ${ringIndex} point ${pointIndex}`) if (samePoint(point, ring[(pointIndex + 1) % ring.length])) throw new RangeError(`Profile ring ${ringIndex} contains consecutive duplicate points.`) }) } const origin = rings[0][0] const normal = ringNormal(rings[0]) if (!normal) throw new RangeError('Profile ring 0 is collinear.') const normalLength = magnitude(normal) for (const [ringIndex, ring] of rings.entries()) { if (!ringNormal(ring)) throw new RangeError(`Profile ring ${ringIndex} is collinear.`) for (const point of ring) if (Math.abs(dot(normal, subtract(point, origin))) / normalLength > 1e-7) throw new RangeError(`Profile ring ${ringIndex} is not coplanar with the outer ring.`) } } const validateLinearFeature = (input: GeometryDocumentContext & LinearFeatureParameters) => { validateDocumentContext(input) validatePlanarProfile(input.profile) finitePositive(input.length, 'length') validateVector(input.direction ?? [0, 1, 0], 'direction', false) const taperAngle = input.taperAngle ?? 0 if (!Number.isFinite(taperAngle) || taperAngle <= -89.999 || taperAngle >= 89.999) throw new RangeError('taperAngle must be finite and between -89.999 and 89.999 degrees.') if (taperAngle !== 0 && ((input.profile.holes?.length ?? 0) > 0 || (input.profile.additionalRegions?.length ?? 0) > 0)) throw new RangeError('Tapered linear features currently require one outer profile ring without holes.') if (taperAngle !== 0 && input.symmetricToPlane) throw new RangeError('Tapered symmetric linear features must be split into two explicit directions.') } export const validatePadInput = (input: PadInput) => validateLinearFeature(input) export const validateExtrudeInput = (input: ExtrudeInput) => validateLinearFeature(input) export const validatePocketInput = (input: PocketInput) => { validateLinearFeature(input) validateShapeContext(input, input.base) } export const validateRevolutionInput = (input: RevolutionInput) => { validateDocumentContext(input) validatePlanarProfile(input.profile) validateVector(input.axisOrigin ?? [0, 0, 0], 'axisOrigin') validateVector(input.axisDirection ?? [0, 1, 0], 'axisDirection', false) validateAngle(input.angle ?? 360, 'angle') if (input.profileRotationAngle !== undefined && (!Number.isFinite(input.profileRotationAngle) || Math.abs(input.profileRotationAngle) > 360)) throw new RangeError('profileRotationAngle must be finite and within 360 degrees.') } export const validateGrooveInput = (input: GrooveInput) => { validateDocumentContext(input) validateShapeContext(input, input.base) validatePlanarProfile(input.profile) validateVector(input.axisOrigin ?? [0, 0, 0], 'axisOrigin') validateVector(input.axisDirection ?? [0, 1, 0], 'axisDirection', false) validateAngle(input.angle ?? 360, 'angle') if (input.profileRotationAngle !== undefined && (!Number.isFinite(input.profileRotationAngle) || Math.abs(input.profileRotationAngle) > 360)) throw new RangeError('profileRotationAngle must be finite and within 360 degrees.') } export const validateLoftInput = (input: LoftInput) => { validateDocumentContext(input) if (!Array.isArray(input.sections) || input.sections.length < 2) throw new RangeError('Loft requires at least two section profiles.') if (input.closed && input.sections.length < 3) throw new RangeError('Closed loft requires at least three section profiles.') for (const [index, section] of input.sections.entries()) { validatePlanarProfile(section) if ((section.holes?.length ?? 0) > 0 || (section.additionalRegions?.length ?? 0) > 0) throw new RangeError(`Loft section ${index} contains holes or multiple regions, which are not supported by the simple Bitbybit loft contract.`) } const mode = input.mode ?? 'standalone' if (!['standalone', 'additive', 'subtractive'].includes(mode)) throw new RangeError(`Unsupported loft mode: ${String(mode)}`) if (mode === 'standalone') { if (input.base) throw new RangeError('Standalone loft must not specify a base Shape.') } else { if (!input.base) throw new RangeError(`${mode === 'additive' ? 'Additive' : 'Subtractive'} loft requires a base Shape.`) validateShapeContext(input, input.base) } } export const validatePipeInput = (input: PipeInput) => { validateDocumentContext(input) validatePlanarProfile(input.profile) if ((input.profile.holes?.length ?? 0) > 0 || (input.profile.additionalRegions?.length ?? 0) > 0) throw new RangeError('Pipe profile holes and multiple regions are not supported by the simple Bitbybit pipe contract.') if (!Array.isArray(input.path) || input.path.length < 2) throw new RangeError('Pipe path requires at least two points.') input.path.forEach((point, index) => validateVector(point, `path point ${index}`)) if (input.path.some((point, index) => index > 0 && samePoint(point, input.path[index - 1]))) throw new RangeError('Pipe path contains consecutive duplicate points.') const mode = input.mode ?? 'standalone' if (!['standalone', 'additive', 'subtractive'].includes(mode)) throw new RangeError(`Unsupported pipe mode: ${String(mode)}`) if (mode === 'standalone') { if (input.base) throw new RangeError('Standalone pipe must not specify a base Shape.') } else { if (!input.base) throw new RangeError(`${mode === 'additive' ? 'Additive' : 'Subtractive'} pipe requires a base Shape.`) validateShapeContext(input, input.base) } } export const validateGeometryFileImport = (input: GeometryFileImport) => { validateDocumentContext(input) if (!['step', 'iges', 'brep'].includes(input.format)) throw new RangeError(`Unsupported geometry import format: ${input.format}`) if (typeof input.text !== 'string' || !input.text.trim()) throw new TypeError('Geometry import text must be non-empty.') if (new TextEncoder().encode(input.text).byteLength > MAX_GEOMETRY_IMPORT_TEXT_BYTES) throw new RangeError(`Geometry import payload exceeds ${MAX_GEOMETRY_IMPORT_TEXT_BYTES} bytes.`) } export const assertShapeHandleIntegrity = (actual: ShapeHandle, expected: ShapeHandle) => { if (actual.id !== expected.id || actual.kernel !== expected.kernel || actual.kind !== expected.kind || actual.documentId !== expected.documentId || actual.documentVersion !== expected.documentVersion) throw new Error(`Shape handle integrity check failed: ${actual.id}`) } const appendFace = (face: Inputs.OCCT.DecomposedFaceDto, positions: number[], normals: number[], indices: number[]) => { if (face.vertexCoord.length % 3 !== 0 || face.triIndexes.length % 3 !== 0) throw new Error(`Bitbybit OCCT returned malformed arrays for face ${face.faceIndex}.`) if (face.vertexCoord.some((coordinate) => !Number.isFinite(coordinate)) || face.normalCoord.some((coordinate) => !Number.isFinite(coordinate))) throw new Error(`Bitbybit OCCT returned non-finite coordinates for face ${face.faceIndex}.`) if (face.normalCoord.length !== 0 && face.normalCoord.length !== face.vertexCoord.length) throw new Error(`Bitbybit OCCT returned mismatched normals for face ${face.faceIndex}.`) const faceVertexCount = face.vertexCoord.length / 3 if (face.triIndexes.some((index) => !Number.isSafeInteger(index) || index < 0 || index >= faceVertexCount)) throw new Error(`Bitbybit OCCT returned an out-of-range triangle index for face ${face.faceIndex}.`) const vertexOffset = positions.length / 3 for (const coordinate of face.vertexCoord) positions.push(coordinate) if (face.normalCoord.length === face.vertexCoord.length) for (const coordinate of face.normalCoord) normals.push(coordinate) else for (let index = 0; index < face.vertexCoord.length; index += 3) normals.push(0, 0, 0) for (const index of face.triIndexes) indices.push(vertexOffset + index) } export const normalizeBitbybitMesh = (shape: ShapeHandle, mesh: KernelMesh, tolerance = 1e-5, analytic?: AnalyticTopologyDescription | null): MeshAsset => { const positions: number[] = [] const normals: number[] = [] const indices: number[] = [] const kernelEdges = mesh.edgeList ?? [] const kernelVertices = analytic?.vertices?.length ? analytic.vertices.map((vertex) => vertex.point) : (mesh.pointsList ?? []) const faceTriangleRanges = mesh.faceList.map((face) => { const startTriangle = indices.length / 3 appendFace(face, positions, normals, indices) return { startTriangle, triangleCount: face.triIndexes.length / 3 } }) if (positions.length === 0 || indices.length === 0) throw new Error('Bitbybit OCCT returned an empty mesh for the shape.') const min: [number, number, number] = [Infinity, Infinity, Infinity] const max: [number, number, number] = [-Infinity, -Infinity, -Infinity] for (let index = 0; index < positions.length; index += 3) { for (let axis = 0; axis < 3; axis += 1) { min[axis] = Math.min(min[axis], positions[index + axis]) max[axis] = Math.max(max[axis], positions[index + axis]) } } let analyticTopology: ReturnType | null = null if (analytic?.faces?.length || analytic?.edges?.length || analytic?.vertices?.length) { try { analyticTopology = createAnalyticSubshapeRefs(shape.id, shape.documentVersion, analytic.faces ?? [], analytic.edges ?? [], analytic.vertices ?? [], tolerance) } catch { // Invalid or incomplete analytic descriptors must not make tessellation unusable. } } const meshFaces = mesh.faceList.map((face) => ({ vertexCoord: face.vertexCoord, normalCoord: face.normalCoord, triIndexes: face.triIndexes })) const faceTopology = analyticTopology?.faces.refs.length === mesh.faceList.length ? analyticTopology.faces : createSubshapeRefs(shape.id, shape.documentVersion, meshFaces, tolerance) const subshapes: SubshapeRef[] = faceTopology.refs const subshapeRanges = faceTriangleRanges.flatMap((range, index) => { const ref = subshapes[index] return ref ? [{ ...range, ref: { ...ref, candidates: ref.candidates ? [...ref.candidates] : undefined } }] : [] }) const edgeTopology = analyticTopology?.edges.refs.length === kernelEdges.length && kernelEdges.length > 0 ? analyticTopology.edges : createEdgeSubshapeRefs(shape.id, shape.documentVersion, meshFaces, tolerance) const vertexTopology = analyticTopology?.vertices.refs.length === kernelVertices.length && kernelVertices.length > 0 ? analyticTopology.vertices : createVertexSubshapeRefs(shape.id, shape.documentVersion, meshFaces, tolerance) const edgeRefs = new Map(edgeTopology.signatures.map((signature, index) => [signature.hash, edgeTopology.refs[index]])) const vertexRefs = new Map(vertexTopology.signatures.map((signature, index) => [signature.hash, vertexTopology.refs[index]])) const edgeGeometry = new Map() const vertexGeometry = new Map() const pointAt = (coordinates: number[], index: number): [number, number, number] => [coordinates[index * 3], coordinates[index * 3 + 1], coordinates[index * 3 + 2]] const hasKernelEdges = analyticTopology?.edges === edgeTopology && kernelEdges.length > 0 const hasKernelVertices = analyticTopology?.vertices === vertexTopology && kernelVertices.length > 0 if (hasKernelVertices) for (const [index, point] of kernelVertices.entries()) { const ref = vertexTopology.refs[index] if (!ref || point.length !== 3 || point.some((coordinate) => !Number.isFinite(coordinate))) continue vertexGeometry.set(ref.persistentId, [...point] as [number, number, number]) } if (hasKernelEdges) for (const [index, edge] of kernelEdges.entries()) { const ref = edgeTopology.refs[index] if (!ref) continue for (let pointIndex = 0; pointIndex + 1 < edge.vertexCoord.length; pointIndex += 1) { const start = edge.vertexCoord[pointIndex] const end = edge.vertexCoord[pointIndex + 1] if (start.length !== 3 || end.length !== 3 || [...start, ...end].some((coordinate) => !Number.isFinite(coordinate))) continue edgeGeometry.set(`${ref.persistentId}:${pointIndex}`, { start: [...start] as [number, number, number], end: [...end] as [number, number, number] }) } } if (!hasKernelEdges || !hasKernelVertices) for (const face of mesh.faceList) { for (let index = 0; index < face.vertexCoord.length / 3; index += 1) { if (!hasKernelVertices) { const point = pointAt(face.vertexCoord, index) const signature = signatureForVertex(point, tolerance) if (!vertexGeometry.has(signature.hash)) vertexGeometry.set(signature.hash, point) } } if (!hasKernelEdges) for (let index = 0; index + 2 < face.triIndexes.length; index += 3) { const points = [pointAt(face.vertexCoord, face.triIndexes[index]), pointAt(face.vertexCoord, face.triIndexes[index + 1]), pointAt(face.vertexCoord, face.triIndexes[index + 2])] for (let edgeIndex = 0; edgeIndex < 3; edgeIndex += 1) { const start = points[edgeIndex] const end = points[(edgeIndex + 1) % 3] const signature = signatureForEdge(start, end, tolerance) if (!edgeGeometry.has(signature.hash)) edgeGeometry.set(signature.hash, { start, end }) } } } const subshapeEdges = [...edgeGeometry.entries()].flatMap(([hash, geometry]) => { const ref = hasKernelEdges ? edgeTopology.refs.find((candidate) => hash.startsWith(`${candidate.persistentId}:`)) : edgeRefs.get(hash) return ref ? [{ ...geometry, ref: { ...ref, candidates: ref.candidates ? [...ref.candidates] : undefined } }] : [] }) const subshapeVertices = [...vertexGeometry.entries()].flatMap(([hash, position]) => { const ref = hasKernelVertices ? vertexTopology.refs.find((candidate) => candidate.persistentId === hash) : vertexRefs.get(hash) return ref ? [{ position, ref: { ...ref, candidates: ref.candidates ? [...ref.candidates] : undefined } }] : [] }) return { shapeId: shape.id, topologyVersion: shape.documentVersion, positions: new Float32Array(positions), normals: new Float32Array(normals), indices: new Uint32Array(indices), subshapes, subshapeRanges, subshapeEdges, subshapeVertices, bounds: { min, max }, } } export class BitbybitGeometryRuntime { private capabilitiesState = unavailableCapabilities() private client: BitByBitOCCT | null = null private worker: Worker | null = null private initialization: Promise | null = null private cancelInitialization: (() => void) | null = null private sequence = 0 private readonly shapes = new Map() private readonly kernelReferences = new Map() private releasedShapeCount = 0 private peakShapeCount = 0 private peakKernelReferenceCount = 0 private nativeHistory: { provider: NativeOcctHistoryProvider; coordinator: NativeOcctHistoryCoordinator } | null = null private readonly nativeHistoryDocumentVersions = new Map() private readonly nativeKernelSummaries = new Map>() 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.', } } private async nativeOffsetRevolutionResult(input: { documentId: string documentVersion: number profile: KernelShapeReference axisOrigin: Point3 axisDirection: Point3 profileRotationAngle: number angle: number base?: KernelShapeReference }): Promise { const nativeHistory = this.nativeHistory const required: NativeOcctHistoryOperation[] = input.base ? ['rotate', 'revolution', 'cut'] : ['rotate', 'revolution'] if (!nativeHistory || nativeHistory.provider.capabilities().availability !== 'available' || !required.every((operation) => nativeHistory.provider.capabilities().operations.includes(operation))) return null const client = await this.readyClient() const operationId = `${input.documentId}:geometry-offset-revolution:${++this.sequence}` this.nativeHistoryDocumentVersions.set(input.documentId, input.documentVersion) const profileStep = await client.occt.io.saveShapeSTEPAndReturn({ shape: input.profile, fileName: `${operationId}-profile.step`, adjustYtoZ: false, tryDownload: false }) const rotate = await nativeHistory.coordinator.capture(nativeHistory.provider, { documentId: input.documentId, documentVersion: input.documentVersion, operationId: `${operationId}:rotate`, operation: 'rotate', objectStep: profileStep, axisOrigin: input.axisOrigin, direction: input.axisDirection, angle: input.profileRotationAngle, }) const rotatedStep = rotate.response?.history.resultStep if (rotate.status !== 'completed' || !rotatedStep) throw new Error(`Native offset profile rotation ${rotate.status}.`) const revolution = await nativeHistory.coordinator.capture(nativeHistory.provider, { documentId: input.documentId, documentVersion: input.documentVersion, operationId: `${operationId}:revolution`, operation: 'revolution', objectStep: rotatedStep, axisOrigin: input.axisOrigin, direction: input.axisDirection, angle: input.angle, }) let resultStep = revolution.response?.history.resultStep let resultBrep = revolution.response?.history.resultBrep let resultSummary = revolution.response?.history.summary if (revolution.status !== 'completed' || !resultStep) throw new Error(`Native offset Revolution ${revolution.status}.`) if (input.base) { const baseStep = await client.occt.io.saveShapeSTEPAndReturn({ shape: input.base, fileName: `${operationId}-base.step`, adjustYtoZ: false, tryDownload: false }) const cut = await nativeHistory.coordinator.capture(nativeHistory.provider, { documentId: input.documentId, documentVersion: input.documentVersion, operationId: `${operationId}:cut`, operation: 'cut', objectStep: baseStep, toolStep: resultStep, }) resultStep = cut.response?.history.resultStep resultBrep = cut.response?.history.resultBrep resultSummary = cut.response?.history.summary if (cut.status !== 'completed' || !resultStep) throw new Error(`Native offset Groove cut ${cut.status}.`) } const result = await this.importShape({ documentId: input.documentId, documentVersion: input.documentVersion, format: resultBrep ? 'brep' : 'step', text: resultBrep ?? resultStep }) if (resultSummary) this.nativeKernelSummaries.set(result.id, resultSummary) return result } capabilities(): GeometryCapabilities { return { ...this.capabilitiesState } } initialize(): Promise { if (this.capabilitiesState.status === 'ready') return Promise.resolve(this.capabilities()) if (this.initialization) return this.initialization if (typeof Worker === 'undefined' || typeof WebAssembly === 'undefined') { this.capabilitiesState = { ...unavailableCapabilities(), status: 'unavailable', reason: 'WebAssembly and Web Workers are required for Bitbybit OCCT.' } return Promise.resolve(this.capabilities()) } this.capabilitiesState = { ...this.capabilitiesState, status: 'initializing', reason: undefined } this.initialization = new Promise((resolve, reject) => { let settled = false const client = new BitByBitOCCT() const worker = new Worker(new URL('./geometryWorker.ts', import.meta.url), { type: 'module', name: 'bitbybit-occt' }) this.client = client this.worker = worker const timeout = window.setTimeout(() => fail(new Error('Bitbybit OCCT initialization timed out.')), 120_000) const subscription = client.occtWorkerManager.occWorkerState$.subscribe(({ state }) => { if (state !== OccStateEnum.initialised || settled) return settled = true window.clearTimeout(timeout) subscription.unsubscribe() this.cancelInitialization = null this.capabilitiesState = { ...this.capabilitiesState, status: 'ready', worker: true, wasm: true } resolve(this.capabilities()) }) const fail = (error: Error) => { const wasSettled = settled settled = true window.clearTimeout(timeout) subscription.unsubscribe() worker.terminate() this.shapes.clear() this.kernelReferences.clear() this.syncOwnershipMetrics() this.worker = null this.client = null this.initialization = null this.cancelInitialization = null this.capabilitiesState = { ...this.capabilitiesState, status: 'failed', reason: error.message } if (!wasSettled) reject(error) } this.cancelInitialization = () => fail(new Error('Bitbybit OCCT initialization was cancelled.')) worker.addEventListener('error', (event) => fail(new Error(event.message || 'Bitbybit OCCT worker failed.')), { once: true }) worker.addEventListener('message', ({ data }) => { if (data?.type === 'occ-initialization-failed') fail(new Error(data.error || 'Bitbybit OCCT initialization failed.')) }) client.occtWorkerManager.errorCallback = (error) => { this.capabilitiesState = { ...this.capabilitiesState, reason: error } } client.init(worker) }) return this.initialization } async createBox(input: CreateBoxInput): Promise { validateBoxInput(input) const client = await this.readyClient() const kernelShape = await client.occt.shapes.solid.createBox({ width: input.width, length: input.length, height: input.height, center: input.center ?? [0, 0, 0], originOnCenter: input.originOnCenter ?? true, }) return this.registerShape(kernelShape, input.documentId, input.documentVersion) } async createCylinder(input: CreateCylinderInput): Promise { validateCylinderInput(input) const client = await this.readyClient() const kernelShape = await client.occt.shapes.solid.createCylinder({ radius: input.radius, height: input.height, center: input.center ?? [0, 0, 0], direction: input.direction ?? [0, 1, 0], angle: input.angle ?? 360, originOnCenter: input.originOnCenter ?? false, }) return this.registerShape(kernelShape, input.documentId, input.documentVersion) } async createSphere(input: CreateSphereInput): Promise { validateSphereInput(input) const client = await this.readyClient() const kernelShape = await client.occt.shapes.solid.createSphere({ radius: input.radius, center: input.center ?? [0, 0, 0] }) return this.registerShape(kernelShape, input.documentId, input.documentVersion) } async createEllipsoid(input: CreateEllipsoidInput): Promise { validateEllipsoidInput(input) const angle1 = input.angle1 ?? -90 const angle2 = input.angle2 ?? 90 const angle3 = input.angle3 ?? 360 if (angle1 !== -90 || angle2 !== 90 || angle3 !== 360) throw new RangeError('Bitbybit Ellipsoid trim angles are not available in this runtime.') const center = input.center ?? [0, 0, 0] const client = await this.readyClient() const kernelSphere = await client.occt.shapes.solid.createSphere({ radius: input.radius2, center }) const radius3 = input.radius3 && input.radius3 > 0 ? input.radius3 : input.radius2 const kernelShape = await client.occt.transforms.scale3d({ shape: kernelSphere, scale: [1, radius3 / input.radius2, input.radius1 / input.radius2], center }) return this.registerShape(kernelShape, input.documentId, input.documentVersion) } async createCone(input: CreateConeInput): Promise { validateConeInput(input) const client = await this.readyClient() const kernelShape = await client.occt.shapes.solid.createCone({ radius1: input.radius1, radius2: input.radius2, height: input.height, angle: input.angle ?? 360, center: input.center ?? [0, 0, 0], direction: input.direction ?? [0, 1, 0], }) return this.registerShape(kernelShape, input.documentId, input.documentVersion) } async createTorus(input: CreateTorusInput): Promise { validateTorusInput(input) const client = await this.readyClient() const center = input.center ?? [0, 0, 0] const direction = input.direction ?? [0, 1, 0] const angle = input.angle ?? 360 const kernelShape = await client.occt.shapes.solid.createTorus({ majorRadius: input.majorRadius, minorRadius: input.minorRadius, center, direction, angle }) return this.registerShape(kernelShape, input.documentId, input.documentVersion) } async createHelix(input: CreateHelixInput): Promise { validateHelixInput(input) const client = await this.readyClient() const center = input.center ?? [0, 0, 0] const direction = input.direction ?? [0, 0, 1] const clockwise = input.leftHanded ?? false const tolerance = input.tolerance ?? 1e-10 const angle = input.angle ?? 0 const endRadius = input.radius + input.height * Math.tan(angle * Math.PI / 180) const kernelShape = angle === 0 ? await client.occt.shapes.wire.createHelixWire({ radius: input.radius, pitch: input.pitch, height: input.height, center, direction, clockwise, tolerance }) : await client.occt.shapes.wire.createTaperedHelixWire({ startRadius: input.radius, endRadius, pitch: input.pitch, height: input.height, center, direction, clockwise, tolerance }) return this.registerShape(kernelShape, input.documentId, input.documentVersion) } async modeledThread(input: ModeledThreadInput): Promise { validateDocumentContext(input) validateShapeContext(input, input.base) finitePositive(input.minorDiameter, 'minorDiameter') finitePositive(input.majorDiameter, 'majorDiameter') finitePositive(input.pitch, 'pitch') finitePositive(input.depth, 'depth') if (!(input.majorDiameter > input.minorDiameter)) throw new RangeError('majorDiameter must be greater than minorDiameter.') const center = input.center ?? [0, 0, 0] const direction = input.direction ?? [0, 0, 1] validateVector(center, 'center') validateVector(direction, 'direction', false) const directionLength = magnitude(direction) const w = direction.map((coordinate) => coordinate / directionLength) as Point3 const minorRadius = input.minorDiameter / 2 const majorRadius = input.majorDiameter / 2 const client = await this.readyClient() const pathRadius = (minorRadius + majorRadius) / 2 const path = await client.occt.shapes.wire.createHelixWire({ radius: pathRadius, pitch: input.pitch, height: input.depth, center, direction: w, clockwise: input.leftHanded ?? false, tolerance: 1e-10 }) const radialOverlap = Math.min((majorRadius - minorRadius) * 0.05, input.pitch * 0.01) const threadTool = await client.occt.operations.pipeWireCylindrical({ shape: path, radius: (majorRadius - minorRadius) / 2 + radialOverlap, makeSolid: true, trihedronEnum: 'isCorrectedFrenet' as Inputs.OCCT.geomFillTrihedronEnum, forceApproxC1: false }) const cut = await client.occt.booleans.difference({ shape: this.resolveShape(input.base).reference, shapes: [threadTool], keepEdges: true }) return this.registerShape(cut, input.documentId, input.documentVersion) } async createPrism(input: CreatePrismInput): Promise { validatePrismInput(input) const center = input.center ?? [0, 0, 0] const firstAngle = (input.firstAngle ?? 0) * Math.PI / 180 const secondAngle = (input.secondAngle ?? 0) * Math.PI / 180 const translation: Point3 = [input.height * Math.tan(firstAngle), input.height * Math.tan(secondAngle), input.height] const base: Point3[] = Array.from({ length: input.polygon }, (_, index) => { const angle = 2 * Math.PI * index / input.polygon return [center[0] + input.circumradius * Math.cos(angle), center[1] + input.circumradius * Math.sin(angle), center[2]] }) const top = base.map(([x, y, z]) => [x + translation[0], y + translation[1], z + translation[2]] as Point3) const client = await this.readyClient() const face = async (points: Point3[]) => { const wire = await client.occt.shapes.wire.createPolygonWire({ points }) return client.occt.shapes.face.createFaceFromWires({ shapes: [wire], planar: true }) } const faces = [await face([...base].reverse()), await face(top)] for (let index = 0; index < input.polygon; index += 1) { const next = (index + 1) % input.polygon faces.push(await face([base[index], base[next], top[next], top[index]])) } const shell = await client.occt.shapes.shell.sewFaces({ shapes: faces, tolerance: 1e-7 }) const solid = await client.occt.shapes.solid.fromClosedShell({ shape: shell }) return this.registerShape(solid, input.documentId, input.documentVersion) } async createWedge(input: CreateWedgeInput): Promise { validateWedgeInput(input) const center = input.center ?? [0, 0, 0] const point = (x: number, y: number, z: number): Point3 => [center[0] + x, center[1] + y, center[2] + z] const a = point(input.xmin, input.ymin, input.zmin) const b = point(input.xmax, input.ymin, input.zmin) const c = point(input.xmax, input.ymin, input.zmax) const d = point(input.xmin, input.ymin, input.zmax) const e = point(input.x2min, input.ymax, input.z2min) const f = point(input.x2max, input.ymax, input.z2min) const g = point(input.x2max, input.ymax, input.z2max) const h = point(input.x2min, input.ymax, input.z2max) const client = await this.readyClient() const distinct = (points: Point3[]) => { const ring: Point3[] = [] for (const candidate of points) { const previous = ring[ring.length - 1] if (!previous || candidate.some((value, axis) => Math.abs(value - previous[axis]) > 1e-9)) ring.push(candidate) } if (ring.length > 1 && ring[0].every((value, axis) => Math.abs(value - ring[ring.length - 1][axis]) <= 1e-9)) ring.pop() return ring } const face = async (points: Point3[]) => { const ring = distinct(points) if (ring.length < 3) return null const wire = await client.occt.shapes.wire.createPolygonWire({ points: ring }) return client.occt.shapes.face.createFaceFromWires({ shapes: [wire], planar: true }) } const faces = (await Promise.all([ face([a, b, c, d]), face([e, h, g, f]), face([a, e, f, b]), face([d, c, g, h]), face([a, d, h, e]), face([b, f, g, c]), ])).filter((candidate): candidate is NonNullable => candidate !== null) if (faces.length < 4) throw new Error('Wedge construction produced too few non-degenerate faces.') const shell = await client.occt.shapes.shell.sewFaces({ shapes: faces, tolerance: 1e-7 }) const solid = await client.occt.shapes.solid.fromClosedShell({ shape: shell }) return this.registerShape(solid, input.documentId, input.documentVersion) } async applyPlacement(input: ApplyPlacementInput): Promise { validatePlacementInput(input) const source = this.resolveShape(input.shape) const client = await this.readyClient() const kernelShape = await client.occt.transforms.transform({ shape: source.reference, translation: input.placement.translation, rotationAxis: input.placement.rotationAxis, rotationAngle: input.placement.rotationAngle, scaleFactor: 1, }) return this.registerShape(kernelShape, input.documentId, input.documentVersion) } async mirror(input: MirrorInput): Promise { validateMirrorInput(input) const source = this.resolveShape(input.shape) const client = await this.readyClient() const kernelShape = await client.occt.transforms.mirrorAlongNormal({ shape: source.reference, origin: input.origin, normal: input.normal }) return this.registerShape(kernelShape, input.documentId, input.documentVersion) } async union(input: BooleanUnionInput): Promise { validateBooleanUnionInput(input) const shapes = input.shapes.map((shape) => this.resolveShape(shape).reference) const client = await this.readyClient() const kernelShape = await client.occt.booleans.union({ shapes, keepEdges: input.keepEdges ?? false }) return this.registerShape(kernelShape, input.documentId, input.documentVersion) } async cut(input: BooleanCutInput): Promise { validateBooleanCutInput(input) const base = this.resolveShape(input.base).reference const tools = input.tools.map((shape) => this.resolveShape(shape).reference) const client = await this.readyClient() const kernelShape = await client.occt.booleans.difference({ shape: base, shapes: tools, keepEdges: input.keepEdges ?? false }) return this.registerShape(kernelShape, input.documentId, input.documentVersion) } async intersection(input: BooleanIntersectionInput): Promise { validateBooleanIntersectionInput(input) const shapes = input.shapes.map((shape) => this.resolveShape(shape).reference) const client = await this.readyClient() const kernelShape = await client.occtWorkerManager.genericCallToWorkerPromise('plugins.boolean.intersection', { shapes, keepEdges: input.keepEdges ?? false }) as KernelShapeReference return this.registerShape(kernelShape, input.documentId, input.documentVersion) } async fillet(input: FilletInput): Promise { validateFilletInput(input) const base = this.resolveShape(input.base).reference const client = await this.readyClient() const kernelShape = await client.occt.fillets.filletEdges({ shape: base, radius: input.radius, indexes: input.indexes }) return this.registerShape(kernelShape, input.documentId, input.documentVersion) } async chamfer(input: ChamferInput): Promise { validateChamferInput(input) const base = this.resolveShape(input.base).reference const client = await this.readyClient() const kernelShape = await client.occt.fillets.chamferEdges({ shape: base, distance: input.distance, indexes: input.indexes }) return this.registerShape(kernelShape, input.documentId, input.documentVersion) } async draft(input: DraftInput): Promise { validateDraftInput(input) const base = this.resolveShape(input.base).reference const client = await this.readyClient() const faces = input.indexes ? await Promise.all(input.indexes.map((index) => client.occt.shapes.face.getFace({ shape: base, index }))) : await client.occt.shapes.face.getFaces({ shape: base }) const kernelShape = await client.occt.draft.draftAngle({ shape: base, faces, direction: input.direction ?? [0, 1, 0], angle: input.angle, neutralPlaneOrigin: input.neutralPlaneOrigin ?? [0, 0, 0], neutralPlaneDirection: input.neutralPlaneDirection ?? [0, 0, 1], flag: input.reversed !== true }) return this.registerShape(kernelShape, input.documentId, input.documentVersion) } async thickness(input: ThicknessInput): Promise { validateThicknessInput(input) const base = this.resolveShape(input.base).reference const client = await this.readyClient() const kernelShape = input.removeFaceIndexes?.length ? await client.occt.operations.makeThickSolidByJoin({ shape: base, shapes: await Promise.all(input.removeFaceIndexes.map((index) => client.occt.shapes.face.getFace({ shape: base, index }))), offset: input.offset, tolerance: 1e-3, intersection: input.joinType === 'Intersection', selfIntersection: false, joinType: (input.joinType === 'Intersection' ? 'intersection' : 'arc') as Inputs.OCCT.joinTypeEnum, removeIntEdges: false, }) : await client.occt.operations.makeThickSolidSimple({ shape: base, offset: input.offset }) return this.registerShape(kernelShape, input.documentId, input.documentVersion) } async exportStep(shape: ShapeHandle, fileName = `${shape.id}.step`): Promise { const entry = this.resolveShape(shape) if (!fileName.toLowerCase().endsWith('.step') && !fileName.toLowerCase().endsWith('.stp')) throw new RangeError('STEP fileName must end with .step or .stp.') const client = await this.readyClient() const text = await client.occt.io.saveShapeSTEPAndReturn({ shape: entry.reference, fileName, adjustYtoZ: false, tryDownload: false }) return { format: 'step', fileName, mediaType: 'application/step', text } } async exportStl(shape: ShapeHandle, fileName = `${shape.id}.stl`, precision = 0.05): Promise { const entry = this.resolveShape(shape) if (!fileName.toLowerCase().endsWith('.stl')) throw new RangeError('STL fileName must end with .stl.') finitePositive(precision, 'precision') const client = await this.readyClient() const text = await client.occt.io.saveShapeStlAndReturn({ shape: entry.reference, fileName, precision, adjustYtoZ: false, tryDownload: false, binary: false }) return { format: 'stl', fileName, mediaType: 'model/stl', text } } async exportIges(shape: ShapeHandle, fileName = `${shape.id}.iges`): Promise { const entry = this.resolveShape(shape) if (!fileName.toLowerCase().endsWith('.iges') && !fileName.toLowerCase().endsWith('.igs')) throw new RangeError('IGES fileName must end with .iges or .igs.') const client = await this.readyClient() const text = await client.occtWorkerManager.genericCallToWorkerPromise('plugins.io.exportIges', { shape: entry.reference }) as string if (typeof text !== 'string' || !text.trim()) throw new Error('Bitbybit OCCT returned an empty IGES export.') return { format: 'iges', fileName, mediaType: 'model/iges', text } } async exportBrep(shape: ShapeHandle, fileName = `${shape.id}.brep`): Promise { const entry = this.resolveShape(shape) if (!fileName.toLowerCase().endsWith('.brp') && !fileName.toLowerCase().endsWith('.brep')) throw new RangeError('BRep fileName must end with .brp or .brep.') const client = await this.readyClient() const text = await client.occtWorkerManager.genericCallToWorkerPromise('plugins.io.exportBrep', { shape: entry.reference }) as string if (typeof text !== 'string' || !text.trim()) throw new Error('Bitbybit OCCT returned an empty BRep export.') return { format: 'brep', fileName, mediaType: 'application/x-freecad-brep', text } } async importShape(input: GeometryFileImport): Promise { validateGeometryFileImport(input) const client = await this.readyClient() const kernelShape = await client.occtWorkerManager.genericCallToWorkerPromise('plugins.io.importShape', { format: input.format, text: input.text }) as KernelShapeReference return this.registerShape(kernelShape, input.documentId, input.documentVersion) } async pad(input: PadInput): Promise { validatePadInput(input) const client = await this.readyClient() const kernelShape = await this.createExtrusion(client, input) return this.registerShape(kernelShape, input.documentId, input.documentVersion) } async extrude(input: ExtrudeInput): Promise { validateExtrudeInput(input) const client = await this.readyClient() const kernelShape = await this.createExtrusion(client, input) return this.registerShape(kernelShape, input.documentId, input.documentVersion) } async pocket(input: PocketInput): Promise { validatePocketInput(input) const base = this.resolveShape(input.base).reference const client = await this.readyClient() const direction = input.direction ?? [0, 1, 0] const featureInput = input.throughAll ? { ...input, length: await this.throughAllLength(client, base, direction), symmetricToPlane: true } : input const tool = await this.createExtrusion(client, featureInput) const kernelShape = await client.occt.booleans.difference({ shape: base, shapes: [tool], keepEdges: false }) return this.registerShape(kernelShape, input.documentId, input.documentVersion) } async revolution(input: RevolutionInput): Promise { validateRevolutionInput(input) const client = await this.readyClient() const originalFace = await this.createProfileFace(client, input.profile) const axisOrigin = input.axisOrigin ?? [0, 0, 0] const axisDirection = input.axisDirection ?? [0, 1, 0] const profileRotationAngle = input.profileRotationAngle ?? 0 if (profileRotationAngle !== 0) { const nativeResult = await this.nativeOffsetRevolutionResult({ documentId: input.documentId, documentVersion: input.documentVersion, profile: originalFace, axisOrigin, axisDirection, profileRotationAngle, angle: input.angle ?? 360 }) if (nativeResult) return nativeResult } const face = profileRotationAngle === 0 ? originalFace : await client.occt.transforms.rotateAroundCenter({ shape: originalFace, center: axisOrigin, axis: profileRotationAngle < 0 ? axisDirection.map((coordinate) => -coordinate) as Point3 : axisDirection, angle: Math.abs(profileRotationAngle), }) const kernelShape = await client.occtWorkerManager.genericCallToWorkerPromise('plugins.feature.revolution', { shape: face, axisOrigin, axisDirection, angle: input.angle ?? 360, }) as KernelShapeReference const normalizedShape = await client.occt.shapes.shape.unifySameDomain({ shape: kernelShape, unifyEdges: true, unifyFaces: true, concatBSplines: false }) return this.registerShape(normalizedShape, input.documentId, input.documentVersion) } async groove(input: GrooveInput): Promise { validateGrooveInput(input) const base = this.resolveShape(input.base).reference const client = await this.readyClient() const originalFace = await this.createProfileFace(client, input.profile) const axisOrigin = input.axisOrigin ?? [0, 0, 0] const axisDirection = input.axisDirection ?? [0, 1, 0] const profileRotationAngle = input.profileRotationAngle ?? 0 if (profileRotationAngle !== 0) { const nativeResult = await this.nativeOffsetRevolutionResult({ documentId: input.documentId, documentVersion: input.documentVersion, profile: originalFace, axisOrigin, axisDirection, profileRotationAngle, angle: input.angle ?? 360, base }) if (nativeResult) return nativeResult } const face = profileRotationAngle === 0 ? originalFace : await client.occt.transforms.rotateAroundCenter({ shape: originalFace, center: axisOrigin, axis: profileRotationAngle < 0 ? axisDirection.map((coordinate) => -coordinate) as Point3 : axisDirection, angle: Math.abs(profileRotationAngle), }) const tool = await client.occtWorkerManager.genericCallToWorkerPromise('plugins.feature.revolution', { shape: face, axisOrigin, axisDirection, angle: input.angle ?? 360, }) as KernelShapeReference const kernelShape = await client.occt.booleans.difference({ shape: base, shapes: [tool], keepEdges: false }) const normalizedShape = await client.occt.shapes.shape.unifySameDomain({ shape: kernelShape, unifyEdges: true, unifyFaces: true, concatBSplines: false }) return this.registerShape(normalizedShape, input.documentId, input.documentVersion) } async loft(input: LoftInput): Promise { validateLoftInput(input) const client = await this.readyClient() const wires = await Promise.all(input.sections.map((section) => client.occt.shapes.wire.createPolygonWire({ points: normalizedRing(section.outer) }))) let tool: KernelShapeReference | undefined try { tool = input.ruled || input.closed ? await client.occt.operations.loftAdvanced({ shapes: wires, makeSolid: true, closed: input.closed ?? false, periodic: false, straight: input.ruled ?? false, nrPeriodicSections: 10, useSmoothing: false, maxUDegree: 3, tolerance: 1e-7, parType: 'approxCentripetal' as Inputs.OCCT.approxParametrizationTypeEnum, }) : await client.occt.operations.loft({ shapes: wires, makeSolid: true }) const mode = input.mode ?? 'standalone' if (mode === 'standalone') { const result = this.registerShape(tool, input.documentId, input.documentVersion) tool = undefined return result } const kernelShape = mode === 'additive' ? await client.occt.booleans.union({ shapes: [this.resolveShape(input.base as ShapeHandle).reference, tool], keepEdges: false }) : await client.occt.booleans.difference({ shape: this.resolveShape(input.base as ShapeHandle).reference, shapes: [tool], keepEdges: false }) tool = undefined return this.registerShape(kernelShape, input.documentId, input.documentVersion) } finally { // Keep loft input wires alive for the worker's resulting shape cache. } } async pipe(input: PipeInput): Promise { validatePipeInput(input) const client = await this.readyClient() const path = await client.occt.shapes.wire.createPolylineWire({ points: input.path }) const profile = await this.createProfileFace(client, input.profile) let tool: KernelShapeReference | undefined try { const ring = normalizedRing(input.profile.outer) const center: Point3 = ring.reduce((sum, point) => [sum[0] + point[0] / ring.length, sum[1] + point[1] / ring.length, sum[2] + point[2] / ring.length], [0, 0, 0] as Point3) const radii = ring.map((point) => magnitude(subtract(point, center))) const radius = radii[0] const regular = ring.length >= 3 && radii.every((candidate) => Math.abs(candidate - radius) <= Math.max(1e-7, radius * 1e-6)) const tangent = subtract(input.path[1], input.path[0]) const normal = ringNormal(ring) const parallel = normal && magnitude(tangent) > 1e-9 && Math.abs(dot(normal, tangent)) / (magnitude(normal) * magnitude(tangent)) > 1 - 1e-6 if (regular && parallel) { tool = await client.occt.operations.pipePolylineWireNGon({ shape: path, radius, nrCorners: ring.length, makeSolid: true, trihedronEnum: 'isConstantNormal' as Inputs.OCCT.geomFillTrihedronEnum, forceApproxC1: false }) } else tool = await client.occt.operations.pipe({ shape: path, shapes: [profile] }) const mode = input.mode ?? 'standalone' if (mode === 'standalone') { const result = this.registerShape(tool, input.documentId, input.documentVersion) tool = undefined return result } const kernelShape = mode === 'additive' ? await client.occt.booleans.union({ shapes: [this.resolveShape(input.base as ShapeHandle).reference, tool], keepEdges: false }) : await client.occt.booleans.difference({ shape: this.resolveShape(input.base as ShapeHandle).reference, shapes: [tool], keepEdges: false }) tool = undefined return this.registerShape(kernelShape, input.documentId, input.documentVersion) } finally { // Operation inputs remain worker-owned until the next cache sweep. } } async mesh(shape: ShapeHandle, precision = 0.05): Promise { finitePositive(precision, 'precision') const entry = this.resolveShape(shape) const client = await this.readyClient() const [mesh, analyticDescription] = await Promise.all([ client.occt.shapeToMesh({ shape: entry.reference, precision, adjustYtoZ: false }), client.occtWorkerManager.genericCallToWorkerPromise('plugins.topology.describe', { shape: entry.reference }).catch(() => null), ]) return normalizeBitbybitMesh(entry.handle, mesh, Math.max(1e-5, precision * 0.1), analyticDescription as AnalyticTopologyDescription | null) } async subshapes(shape: ShapeHandle, precision = 0.05): Promise { return (await this.mesh(shape, precision)).subshapes ?? [] } async massProperties(shape: ShapeHandle): Promise { const entry = this.resolveShape(shape) const client = await this.readyClient() const result = await client.occtWorkerManager.genericCallToWorkerPromise('plugins.quality.massProperties', { shape: entry.reference }) as ShapeMassProperties if (!result || !Number.isFinite(result.volume) || result.volume < 0 || !Number.isFinite(result.surfaceArea) || result.surfaceArea < 0 || !Array.isArray(result.centerOfMass) || result.centerOfMass.length !== 3 || result.centerOfMass.some((value) => !Number.isFinite(value))) throw new Error('Bitbybit OCCT returned invalid Shape mass properties.') return { volume: result.volume, surfaceArea: result.surfaceArea, centerOfMass: [...result.centerOfMass] as [number, number, number] } } async qualityReport(shape: ShapeHandle): Promise { const entry = this.resolveShape(shape) const client = await this.readyClient() const [shapeType, isNull, analysis, validation, boundingBox] = await Promise.all([ client.occt.shapes.shape.getShapeType({ shape: entry.reference }), client.occt.shapes.shape.isNull({ shape: entry.reference }), client.occt.brepGraph.analyze({ shape: entry.reference }), client.occt.brepGraph.validate({ shape: entry.reference }), client.occt.operations.boundingBoxOfShape({ shape: entry.reference }), ]) if (!analysis?.ok) throw new Error(`Bitbybit OCCT BRep graph analysis failed: ${analysis?.error ?? 'unknown error'}`) if (!validation?.ok) throw new Error(`Bitbybit OCCT BRep graph validation failed: ${validation?.error ?? 'unknown error'}`) const counts = [analysis.solids, analysis.faces, analysis.edges, analysis.vertices, validation.errors, validation.warnings] if (counts.some((value) => !Number.isSafeInteger(value) || value < 0)) throw new Error('Bitbybit OCCT returned invalid BRep graph counts.') if (isNull !== true && isNull !== false && (analysis.solids + analysis.faces + analysis.edges + analysis.vertices === 0)) throw new Error('Bitbybit OCCT returned an indeterminate null state for an empty Shape.') const min = boundingBox?.min const max = boundingBox?.max if (!Array.isArray(min) || min.length !== 3 || min.some((value) => !Number.isFinite(value)) || !Array.isArray(max) || max.length !== 3 || max.some((value) => !Number.isFinite(value))) throw new Error('Bitbybit OCCT returned an invalid Shape bounding box.') return { shapeType, isNull: isNull === true, structuralValid: validation.valid === true, structuralErrors: validation.errors, structuralWarnings: validation.warnings, structuralIssues: validation.issues, nativeKernelValid: this.nativeKernelSummaries.get(shape.id)?.isValid, solids: analysis.solids, faces: analysis.faces, edges: analysis.edges, vertices: analysis.vertices, boundingBox: { min: [...min] as Point3, max: [...max] as Point3 }, } } async linearLength(shape: ShapeHandle): Promise { const entry = this.resolveShape(shape) const client = await this.readyClient() const lengths = await client.occt.shapes.edge.getEdgeLengthsOfShape({ shape: entry.reference }) if (!Array.isArray(lengths) || lengths.some((value) => !Number.isFinite(value) || value < 0)) throw new Error('Bitbybit OCCT returned invalid edge lengths.') return lengths.reduce((sum, value) => sum + value, 0) } async topology(shape: ShapeHandle, precision = 0.05): Promise { finitePositive(precision, 'precision') const entry = this.resolveShape(shape) const client = await this.readyClient() const [mesh, analyticDescription] = await Promise.all([ client.occt.shapeToMesh({ shape: entry.reference, precision, adjustYtoZ: false }), client.occtWorkerManager.genericCallToWorkerPromise('plugins.topology.describe', { shape: entry.reference }).catch(() => null), ]) const faces = mesh.faceList.map((face) => ({ vertexCoord: face.vertexCoord, normalCoord: face.normalCoord, triIndexes: face.triIndexes })) const tolerance = Math.max(1e-5, precision * 0.1) const analytic = analyticDescription as AnalyticTopologyDescription | null const hasAnalytic = Boolean(analytic?.faces?.length || analytic?.edges?.length || analytic?.vertices?.length) let analyticTopology: ReturnType | null = null if (hasAnalytic) { try { analyticTopology = createAnalyticSubshapeRefs(shape.id, shape.documentVersion, analytic?.faces ?? [], analytic?.edges ?? [], analytic?.vertices ?? [], tolerance) } catch { // Some valid OCCT surfaces do not expose finite analytic descriptors. Mesh-derived // topology remains available and avoids persisting invalid analytic signatures. } } const faceTopology = analyticTopology?.faces ?? createSubshapeRefs(shape.id, shape.documentVersion, faces, tolerance) const edgeTopology = analyticTopology?.edges ?? createEdgeSubshapeRefs(shape.id, shape.documentVersion, faces, tolerance) const vertexTopology = analyticTopology?.vertices ?? createVertexSubshapeRefs(shape.id, shape.documentVersion, faces, tolerance) const entries = [faceTopology, edgeTopology, vertexTopology].flatMap((topology) => topology.refs.map((ref, index) => ({ ref, signature: topology.signatures[index] }))) const adjacency = analyticTopology && analytic?.adjacency ? createTopologyAdjacency({ faces: faceTopology.refs, edges: edgeTopology.refs, vertices: vertexTopology.refs }, analytic.adjacency) : undefined return { faces: faceTopology.refs, edges: edgeTopology.refs, vertices: vertexTopology.refs, entries, adjacency } } async topologyHistory(input: NativeTopologyHistoryInput): Promise { 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 operation.') this.nativeHistoryDocumentVersions.set(input.documentId, input.documentVersion) const stepByObjectId = new Map>>() const exportHistoryInput = async (source: NativeTopologyHistoryInput['inputs'][number]) => { const cached = stepByObjectId.get(source.objectId) if (cached) return cached const exported = await this.exportStep(source.shape, `${source.objectId}.step`) stepByObjectId.set(source.objectId, exported) return exported } if (input.stages?.length) await Promise.all(input.inputs.map(exportHistoryInput)) const inputIdByObjectId = new Map(input.inputs.map((source, index) => [source.objectId, source.inputId ?? `${input.operationId}:input:${index}`])) const stageTransport = input.stages?.length ? { inputs: input.inputs.map((source, index) => ({ inputId: source.inputId ?? `${input.operationId}:input:${index}`, objectId: source.objectId, role: source.role, stageId: source.stageId, step: stepByObjectId.get(source.objectId)!.text, objectTag: source.objectTag, namingEvidence: source.namingEvidence, })), stages: input.stages.map((stage) => ({ stageId: stage.stageId, operation: stage.operation, inputIds: stage.inputObjectIds.map((objectId) => { const inputId = inputIdByObjectId.get(objectId) if (!inputId) throw new RangeError(`Native OCCT stage ${stage.stageId} references unknown input object ${objectId}.`) return inputId }), ordinal: stage.ordinal, })), } : {} const finalResultObjectId = () => input.stages?.length ? input.stages[input.stages.length - 1].resultObjectId ?? `${input.operationId}:result` : `${input.operationId}:result` const captureHistory = (request: Parameters[1]) => nativeHistory.coordinator.capture(nativeHistory.provider, { ...request, ...stageTransport, resultObjectId: finalResultObjectId(), resultObjectTag: input.resultObjectTag }) const mapHistoryRecords = (response: Parameters[0], sourceIds: { object: string; tool: string } | Record) => mapNativeOcctHistoryRecords(response, sourceIds, input) type NativeStageRequest = Pick & Partial> const namingEvidenceForResponse = (response: NativeOcctHistoryResponse, stageId: string, resultObjectId: string) => response.namingEvidence ? assertNativeNamingEvidence({ ...response.namingEvidence, stageId, resultObjectId }) : createFinalShapeOnlyNamingEvidence(stageId, resultObjectId) const captureStage = async (stage: { stageId: string operation: NonNullable inputObjectIds: string[] resultObjectId: string ordinal: number sourceIds: { object: string; tool: string } | Record inputs: NativeTopologyHistoryInput['inputs'] request: NativeStageRequest }) => { const transportInputs = stage.inputs.map((source, index) => ({ inputId: `${stage.stageId}:input:${index}`, objectId: source.objectId, role: index === 0 ? 'object' : 'tool', stageId: source.stageId, step: index === 0 ? stage.request.objectStep : stage.request.toolStep ?? stage.request.objectStep, objectTag: source.objectTag, namingEvidence: source.namingEvidence, })) const execution = await nativeHistory.coordinator.capture(nativeHistory.provider, { documentId: input.documentId, documentVersion: input.documentVersion, operationId: stage.stageId, operation: stage.operation, resultObjectId: stage.resultObjectId, ...(stage.resultObjectId === finalResultObjectId() && input.resultObjectTag !== undefined ? { resultObjectTag: input.resultObjectTag } : {}), inputs: transportInputs, stages: [{ stageId: stage.stageId, operation: stage.operation, inputIds: transportInputs.map(({ inputId }) => inputId), ordinal: stage.ordinal }], ...stage.request, }) if (execution.status !== 'completed' || !execution.response) throw new Error(`Native OCCT ${stage.operation} stage history ${execution.status}.`) const response = execution.response.history if (!response.resultStep?.startsWith('ISO-10303-21;')) throw new Error(`Native OCCT ${stage.operation} stage did not return its result STEP.`) const records = mapNativeOcctHistoryRecords(response, stage.sourceIds, { inputs: stage.inputs, stages: [{ stageId: stage.stageId, operation: stage.operation, inputObjectIds: stage.inputObjectIds, resultObjectId: stage.resultObjectId, ordinal: stage.ordinal }], }) const stageShape = await this.importShape({ documentId: input.documentId, documentVersion: input.documentVersion, format: 'step', text: response.resultStep }) let topology: SubshapeTopology try { topology = await this.topology(stageShape, 0.05) } finally { await this.release(stageShape) } const namingEvidence = namingEvidenceForResponse(response, stage.stageId, stage.resultObjectId) return { resultStep: response.resultStep, capture: { stageId: stage.stageId, operation: stage.operation, inputObjectIds: stage.inputObjectIds, resultObjectId: stage.resultObjectId, ordinal: stage.ordinal, topology, records, namingEvidence } satisfies NativeTopologyHistoryStageCaptureResult, } } const withStageCaptures = (captures: NativeTopologyHistoryStageCaptureResult[]) => { for (const capture of captures) if (!capture.namingEvidence) capture.namingEvidence = createFinalShapeOnlyNamingEvidence(capture.stageId, capture.resultObjectId) const records = captures[captures.length - 1].records as NativeTopologyHistoryRecords Object.defineProperty(records, 'stageCaptures', { value: captures, enumerable: false, configurable: false, writable: false }) return records } const validateFeatureSides = (requiresAngle: boolean) => { const sides = input.featureSides ?? (input.direction ? [{ direction: input.direction, ...(requiresAngle ? { angle: input.angle } : {}) }] : []) if (sides.length < 1 || sides.length > 2) throw new Error('Native OCCT feature history requires one or two ordered sides.') for (const side of sides) { if (side.direction.some((value) => !Number.isFinite(value)) || Math.hypot(...side.direction) <= 0) throw new Error('Native OCCT feature history requires finite non-zero side directions.') if (requiresAngle && (typeof side.angle !== 'number' || !Number.isFinite(side.angle) || side.angle <= 0 || side.angle > 360)) throw new Error('Native OCCT feature history requires each side angle in (0, 360].') } return sides } const builderRequest = (operation: 'pad' | 'revolution', profileStep: string, side: { direction: Point3; angle?: number }, axisOrigin?: Point3): NativeStageRequest => operation === 'pad' ? { objectStep: profileStep, direction: side.direction } : { objectStep: profileStep, axisOrigin, direction: side.direction, angle: side.angle } const captureTwoSidedAdditive = async ( builderOperation: 'pad' | 'revolution', profileInput: NativeTopologyHistoryInput['inputs'][number], profileStep: string, sides: Array<{ direction: Point3; angle?: number }>, axisOrigin?: Point3, ) => { const capabilities = nativeHistory.provider.capabilities().operations if (!capabilities.includes(builderOperation) || !capabilities.includes('fuse')) throw new Error(`Native OCCT provider cannot prove two-sided ${builderOperation} history.`) const firstStageId = `${input.operationId}:native-stage:0` const secondStageId = `${input.operationId}:native-stage:1` const fuseStageId = `${input.operationId}:native-stage:2` const firstObjectId = `${firstStageId}:result` const secondObjectId = `${secondStageId}:result` const first = await captureStage({ stageId: firstStageId, operation: builderOperation, inputObjectIds: [profileInput.objectId], resultObjectId: firstObjectId, ordinal: 0, sourceIds: { object: profileInput.objectId, tool: profileInput.objectId }, inputs: [profileInput], request: builderRequest(builderOperation, profileStep, sides[0], axisOrigin), }) const second = await captureStage({ stageId: secondStageId, operation: builderOperation, inputObjectIds: [profileInput.objectId], resultObjectId: secondObjectId, ordinal: 1, sourceIds: { object: profileInput.objectId, tool: profileInput.objectId }, inputs: [profileInput], request: builderRequest(builderOperation, profileStep, sides[1], axisOrigin), }) const fused = await captureStage({ stageId: fuseStageId, operation: 'fuse', inputObjectIds: [firstObjectId, secondObjectId], resultObjectId: finalResultObjectId(), ordinal: 2, sourceIds: { object: firstObjectId, tool: secondObjectId }, inputs: [ { objectId: firstObjectId, shape: profileInput.shape, stageId: firstStageId, namingEvidence: first.capture.namingEvidence }, { objectId: secondObjectId, shape: profileInput.shape, stageId: secondStageId, namingEvidence: second.capture.namingEvidence }, ], request: { objectStep: first.resultStep, toolStep: second.resultStep }, }) return withStageCaptures([first.capture, second.capture, fused.capture]) } const captureTwoSidedSubtractive = async ( builderOperation: 'pad' | 'revolution', baseInput: NativeTopologyHistoryInput['inputs'][number], profileInput: NativeTopologyHistoryInput['inputs'][number], baseStep: string, profileStep: string, sides: Array<{ direction: Point3; angle?: number }>, axisOrigin?: Point3, ) => { const capabilities = nativeHistory.provider.capabilities().operations if (!capabilities.includes(builderOperation) || !capabilities.includes('fuse') || !capabilities.includes('cut')) throw new Error(`Native OCCT provider cannot prove two-sided subtractive ${builderOperation} history.`) const toolOneStageId = `${input.operationId}:native-stage:0` const toolTwoStageId = `${input.operationId}:native-stage:1` const fuseStageId = `${input.operationId}:native-stage:2` const cutStageId = `${input.operationId}:native-stage:3` const toolOneObjectId = `${toolOneStageId}:result` const toolTwoObjectId = `${toolTwoStageId}:result` const fusedToolObjectId = `${fuseStageId}:result` const toolOne = await captureStage({ stageId: toolOneStageId, operation: builderOperation, inputObjectIds: [profileInput.objectId], resultObjectId: toolOneObjectId, ordinal: 0, sourceIds: { object: profileInput.objectId, tool: profileInput.objectId }, inputs: [profileInput], request: builderRequest(builderOperation, profileStep, sides[0], axisOrigin), }) const toolTwo = await captureStage({ stageId: toolTwoStageId, operation: builderOperation, inputObjectIds: [profileInput.objectId], resultObjectId: toolTwoObjectId, ordinal: 1, sourceIds: { object: profileInput.objectId, tool: profileInput.objectId }, inputs: [profileInput], request: builderRequest(builderOperation, profileStep, sides[1], axisOrigin), }) const fusedTool = await captureStage({ stageId: fuseStageId, operation: 'fuse', inputObjectIds: [toolOneObjectId, toolTwoObjectId], resultObjectId: fusedToolObjectId, ordinal: 2, sourceIds: { object: toolOneObjectId, tool: toolTwoObjectId }, inputs: [ { objectId: toolOneObjectId, shape: profileInput.shape, stageId: toolOneStageId, namingEvidence: toolOne.capture.namingEvidence }, { objectId: toolTwoObjectId, shape: profileInput.shape, stageId: toolTwoStageId, namingEvidence: toolTwo.capture.namingEvidence }, ], request: { objectStep: toolOne.resultStep, toolStep: toolTwo.resultStep }, }) const cut = await captureStage({ stageId: cutStageId, operation: 'cut', inputObjectIds: [baseInput.objectId, fusedToolObjectId], resultObjectId: finalResultObjectId(), ordinal: 3, sourceIds: { object: baseInput.objectId, tool: fusedToolObjectId }, inputs: [ baseInput, { objectId: fusedToolObjectId, shape: profileInput.shape, stageId: fuseStageId, namingEvidence: fusedTool.capture.namingEvidence }, ], request: { objectStep: baseStep, toolStep: fusedTool.resultStep }, }) return withStageCaptures([toolOne.capture, toolTwo.capture, fusedTool.capture, cut.capture]) } const offsetRevolutionParameters = (sides: Array<{ direction: Point3; angle?: number }>) => { const firstAngle = sides[0].angle ?? 0 const secondAngle = sides[1].angle ?? 0 const totalAngle = firstAngle + secondAngle if (!(firstAngle > 0) || !(secondAngle > 0) || totalAngle > 360) throw new Error('Native OCCT offset Revolution requires two positive angles with a total no greater than 360 degrees.') return { direction: sides[0].direction, totalAngle, profileOffsetAngle: -secondAngle } } const captureOffsetRevolutionAdditive = async ( profileInput: NativeTopologyHistoryInput['inputs'][number], profileStep: string, sides: Array<{ direction: Point3; angle?: number }>, axisOrigin: Point3, ) => { const capabilities = nativeHistory.provider.capabilities().operations if (!capabilities.includes('rotate') || !capabilities.includes('revolution')) throw new Error('Native OCCT provider cannot prove offset Revolution history.') const parameters = offsetRevolutionParameters(sides) const rotateStageId = `${input.operationId}:native-stage:0` const revolutionStageId = `${input.operationId}:native-stage:1` const rotatedProfileObjectId = `${rotateStageId}:result` const rotated = await captureStage({ stageId: rotateStageId, operation: 'rotate', inputObjectIds: [profileInput.objectId], resultObjectId: rotatedProfileObjectId, ordinal: 0, sourceIds: { object: profileInput.objectId, tool: profileInput.objectId }, inputs: [profileInput], request: { objectStep: profileStep, axisOrigin, direction: parameters.direction, angle: parameters.profileOffsetAngle }, }) const revolution = await captureStage({ stageId: revolutionStageId, operation: 'revolution', inputObjectIds: [rotatedProfileObjectId], resultObjectId: finalResultObjectId(), ordinal: 1, sourceIds: { object: rotatedProfileObjectId, tool: rotatedProfileObjectId }, inputs: [{ objectId: rotatedProfileObjectId, shape: profileInput.shape, stageId: rotateStageId, namingEvidence: rotated.capture.namingEvidence }], request: { objectStep: rotated.resultStep, axisOrigin, direction: parameters.direction, angle: parameters.totalAngle }, }) return withStageCaptures([rotated.capture, revolution.capture]) } const captureOffsetRevolutionSubtractive = async ( baseInput: NativeTopologyHistoryInput['inputs'][number], profileInput: NativeTopologyHistoryInput['inputs'][number], baseStep: string, profileStep: string, sides: Array<{ direction: Point3; angle?: number }>, axisOrigin: Point3, ) => { const capabilities = nativeHistory.provider.capabilities().operations if (!capabilities.includes('rotate') || !capabilities.includes('revolution') || !capabilities.includes('cut')) throw new Error('Native OCCT provider cannot prove offset Groove history.') const parameters = offsetRevolutionParameters(sides) const rotateStageId = `${input.operationId}:native-stage:0` const revolutionStageId = `${input.operationId}:native-stage:1` const cutStageId = `${input.operationId}:native-stage:2` const rotatedProfileObjectId = `${rotateStageId}:result` const revolutionToolObjectId = `${revolutionStageId}:result` const rotated = await captureStage({ stageId: rotateStageId, operation: 'rotate', inputObjectIds: [profileInput.objectId], resultObjectId: rotatedProfileObjectId, ordinal: 0, sourceIds: { object: profileInput.objectId, tool: profileInput.objectId }, inputs: [profileInput], request: { objectStep: profileStep, axisOrigin, direction: parameters.direction, angle: parameters.profileOffsetAngle }, }) const revolution = await captureStage({ stageId: revolutionStageId, operation: 'revolution', inputObjectIds: [rotatedProfileObjectId], resultObjectId: revolutionToolObjectId, ordinal: 1, sourceIds: { object: rotatedProfileObjectId, tool: rotatedProfileObjectId }, inputs: [{ objectId: rotatedProfileObjectId, shape: profileInput.shape, stageId: rotateStageId, namingEvidence: rotated.capture.namingEvidence }], request: { objectStep: rotated.resultStep, axisOrigin, direction: parameters.direction, angle: parameters.totalAngle }, }) const cut = await captureStage({ stageId: cutStageId, operation: 'cut', inputObjectIds: [baseInput.objectId, revolutionToolObjectId], resultObjectId: finalResultObjectId(), ordinal: 2, sourceIds: { object: baseInput.objectId, tool: revolutionToolObjectId }, inputs: [baseInput, { objectId: revolutionToolObjectId, shape: profileInput.shape, stageId: revolutionStageId, namingEvidence: revolution.capture.namingEvidence }], request: { objectStep: baseStep, toolStep: revolution.resultStep }, }) return withStageCaptures([rotated.capture, revolution.capture, cut.capture]) } if (input.operation === 'pad') { if (input.inputs.length !== 1) throw new Error('Native OCCT Pad history requires one profile input.') const sides = validateFeatureSides(false) const [profileInput] = input.inputs const profileStep = await exportHistoryInput(profileInput) if (sides.length === 2) return captureTwoSidedAdditive('pad', profileInput, profileStep.text, sides) const execution = await captureHistory({ documentId: input.documentId, documentVersion: input.documentVersion, operationId: input.operationId, operation: input.operation, objectStep: profileStep.text, direction: sides[0].direction }) if (execution.status !== 'completed' || !execution.response) throw new Error(`Native OCCT history ${execution.status}.`) return mapHistoryRecords(execution.response.history, { object: profileInput.objectId, tool: profileInput.objectId }) } if (input.operation === 'pocket') { if (input.inputs.length !== 2) throw new Error('Native OCCT Pocket history requires a base and profile.') const sides = validateFeatureSides(false) if (!nativeHistory.provider.capabilities().operations.includes('pocket')) throw new Error('Native OCCT history provider does not declare Pocket history.') const [baseInput, profileInput] = input.inputs const [baseStep, profileStep] = await Promise.all([ exportHistoryInput(baseInput), exportHistoryInput(profileInput), ]) this.nativeHistoryDocumentVersions.set(input.documentId, input.documentVersion) const capabilities = nativeHistory.provider.capabilities().operations if (sides.length === 2) return captureTwoSidedSubtractive('pad', baseInput, profileInput, baseStep.text, profileStep.text, sides) if (capabilities.includes('pad') && capabilities.includes('cut')) { const toolStageId = `${input.operationId}:native-stage:0` const cutStageId = `${input.operationId}:native-stage:1` const toolObjectId = `${toolStageId}:result` const resultObjectId = input.stages?.length ? input.stages[input.stages.length - 1].resultObjectId ?? `${cutStageId}:result` : `${cutStageId}:result` const toolExecution = await nativeHistory.coordinator.capture(nativeHistory.provider, { documentId: input.documentId, documentVersion: input.documentVersion, operationId: toolStageId, operation: 'pad', resultObjectId: toolObjectId, inputs: [{ inputId: `${toolStageId}:input:0`, objectId: profileInput.objectId, role: 'object', step: profileStep.text, objectTag: profileInput.objectTag, namingEvidence: profileInput.namingEvidence }], stages: [{ stageId: toolStageId, operation: 'pad', inputIds: [`${toolStageId}:input:0`], ordinal: 0 }], objectStep: profileStep.text, direction: sides[0].direction, }) if (toolExecution.status !== 'completed' || !toolExecution.response) throw new Error(`Native OCCT Pocket tool stage history ${toolExecution.status}.`) const toolResponse = toolExecution.response.history if (!toolResponse.resultStep?.startsWith('ISO-10303-21;')) throw new Error('Native OCCT Pocket tool stage did not return its result STEP.') const toolRecords = mapNativeOcctHistoryRecords(toolResponse, { object: profileInput.objectId, tool: profileInput.objectId }, { inputs: [{ objectId: profileInput.objectId, shape: profileInput.shape }], stages: [{ stageId: toolStageId, operation: 'pad', inputObjectIds: [profileInput.objectId], resultObjectId: toolObjectId, ordinal: 0 }], }) const toolShape = await this.importShape({ documentId: input.documentId, documentVersion: input.documentVersion, format: 'step', text: toolResponse.resultStep }) let toolTopology: SubshapeTopology try { toolTopology = await this.topology(toolShape, 0.05) } finally { await this.release(toolShape) } const cutExecution = await nativeHistory.coordinator.capture(nativeHistory.provider, { documentId: input.documentId, documentVersion: input.documentVersion, operationId: cutStageId, operation: 'cut', resultObjectId, ...(input.resultObjectTag === undefined ? {} : { resultObjectTag: input.resultObjectTag }), inputs: [ { inputId: `${cutStageId}:input:0`, objectId: baseInput.objectId, role: 'object', step: baseStep.text, objectTag: baseInput.objectTag, namingEvidence: baseInput.namingEvidence }, { inputId: `${cutStageId}:input:1`, objectId: toolObjectId, role: 'tool', stageId: toolStageId, step: toolResponse.resultStep, namingEvidence: toolResponse.namingEvidence }, ], stages: [{ stageId: cutStageId, operation: 'cut', inputIds: [`${cutStageId}:input:0`, `${cutStageId}:input:1`], ordinal: 1 }], objectStep: baseStep.text, toolStep: toolResponse.resultStep, }) if (cutExecution.status !== 'completed' || !cutExecution.response) throw new Error(`Native OCCT Pocket cut stage history ${cutExecution.status}.`) const cutResponse = cutExecution.response.history if (!cutResponse.resultStep?.startsWith('ISO-10303-21;')) throw new Error('Native OCCT Pocket cut stage did not return its result STEP.') const cutRecords = mapNativeOcctHistoryRecords(cutResponse, { object: baseInput.objectId, tool: toolObjectId }, { inputs: [{ objectId: baseInput.objectId, shape: baseInput.shape }, { objectId: toolObjectId, shape: profileInput.shape, stageId: toolStageId }], stages: [{ stageId: cutStageId, operation: 'cut', inputObjectIds: [baseInput.objectId, toolObjectId], resultObjectId, ordinal: 1 }], }) const cutShape = await this.importShape({ documentId: input.documentId, documentVersion: input.documentVersion, format: 'step', text: cutResponse.resultStep }) let cutTopology: SubshapeTopology try { cutTopology = await this.topology(cutShape, 0.05) } finally { await this.release(cutShape) } const captures: NativeTopologyHistoryStageCaptureResult[] = [ { stageId: toolStageId, operation: 'pad', inputObjectIds: [profileInput.objectId], resultObjectId: toolObjectId, ordinal: 0, topology: toolTopology, records: toolRecords, namingEvidence: namingEvidenceForResponse(toolResponse, toolStageId, toolObjectId) }, { stageId: cutStageId, operation: 'cut', inputObjectIds: [baseInput.objectId, toolObjectId], resultObjectId, ordinal: 1, topology: cutTopology, records: cutRecords, namingEvidence: namingEvidenceForResponse(cutResponse, cutStageId, resultObjectId) }, ] const records = cutRecords as NativeTopologyHistoryRecords Object.defineProperty(records, 'stageCaptures', { value: captures, enumerable: false, configurable: false, writable: false }) return records } const execution = await captureHistory({ documentId: input.documentId, documentVersion: input.documentVersion, operationId: input.operationId, operation: input.operation, objectStep: baseStep.text, toolStep: profileStep.text, direction: sides[0].direction, }) if (execution.status !== 'completed' || !execution.response) throw new Error(`Native OCCT history ${execution.status}.`) return mapHistoryRecords(execution.response.history, { object: baseInput.objectId, tool: profileInput.objectId }) } if (input.operation === 'revolution') { if (input.inputs.length !== 1) throw new Error('Native OCCT Revolution history requires one profile.') const axisOrigin = input.axisOrigin const sides = validateFeatureSides(true) if (!axisOrigin || axisOrigin.some((value) => !Number.isFinite(value))) throw new Error('Native OCCT Revolution history requires a finite axis origin.') if (!nativeHistory.provider.capabilities().operations.includes('revolution')) throw new Error('Native OCCT history provider does not declare Revolution history.') const [profileInput] = input.inputs const profileStep = await exportHistoryInput(profileInput) this.nativeHistoryDocumentVersions.set(input.documentId, input.documentVersion) if (sides.length === 2) return captureOffsetRevolutionAdditive(profileInput, profileStep.text, sides, axisOrigin) const execution = await captureHistory({ documentId: input.documentId, documentVersion: input.documentVersion, operationId: input.operationId, operation: input.operation, objectStep: profileStep.text, axisOrigin, direction: sides[0].direction, angle: sides[0].angle, }) if (execution.status !== 'completed' || !execution.response) throw new Error(`Native OCCT history ${execution.status}.`) return mapHistoryRecords(execution.response.history, { object: profileInput.objectId, tool: profileInput.objectId }) } if (input.operation === 'groove') { if (input.inputs.length !== 2) throw new Error('Native OCCT Groove history requires a base and profile.') const axisOrigin = input.axisOrigin const sides = validateFeatureSides(true) if (!axisOrigin || axisOrigin.some((value) => !Number.isFinite(value))) throw new Error('Native OCCT Groove history requires a finite axis origin.') if (!nativeHistory.provider.capabilities().operations.includes('groove')) throw new Error('Native OCCT history provider does not declare Groove history.') const [baseInput, profileInput] = input.inputs const [baseStep, profileStep] = await Promise.all([ exportHistoryInput(baseInput), exportHistoryInput(profileInput), ]) this.nativeHistoryDocumentVersions.set(input.documentId, input.documentVersion) const capabilities = nativeHistory.provider.capabilities().operations if (sides.length === 2) return captureOffsetRevolutionSubtractive(baseInput, profileInput, baseStep.text, profileStep.text, sides, axisOrigin) if (capabilities.includes('revolution') && capabilities.includes('cut')) { const toolStageId = `${input.operationId}:native-stage:0` const cutStageId = `${input.operationId}:native-stage:1` const toolObjectId = `${toolStageId}:result` const resultObjectId = input.stages?.length ? input.stages[input.stages.length - 1].resultObjectId ?? `${cutStageId}:result` : `${cutStageId}:result` const toolExecution = await nativeHistory.coordinator.capture(nativeHistory.provider, { documentId: input.documentId, documentVersion: input.documentVersion, operationId: toolStageId, operation: 'revolution', resultObjectId: toolObjectId, inputs: [{ inputId: `${toolStageId}:input:0`, objectId: profileInput.objectId, role: 'object', step: profileStep.text, objectTag: profileInput.objectTag, namingEvidence: profileInput.namingEvidence }], stages: [{ stageId: toolStageId, operation: 'revolution', inputIds: [`${toolStageId}:input:0`], ordinal: 0 }], objectStep: profileStep.text, axisOrigin, direction: sides[0].direction, angle: sides[0].angle, }) if (toolExecution.status !== 'completed' || !toolExecution.response) throw new Error(`Native OCCT Groove tool stage history ${toolExecution.status}.`) const toolResponse = toolExecution.response.history if (!toolResponse.resultStep?.startsWith('ISO-10303-21;')) throw new Error('Native OCCT Groove tool stage did not return its result STEP.') const toolRecords = mapNativeOcctHistoryRecords(toolResponse, { object: profileInput.objectId, tool: profileInput.objectId }, { inputs: [{ objectId: profileInput.objectId, shape: profileInput.shape }], stages: [{ stageId: toolStageId, operation: 'revolution', inputObjectIds: [profileInput.objectId], resultObjectId: toolObjectId, ordinal: 0 }], }) const toolShape = await this.importShape({ documentId: input.documentId, documentVersion: input.documentVersion, format: 'step', text: toolResponse.resultStep }) let toolTopology: SubshapeTopology try { toolTopology = await this.topology(toolShape, 0.05) } finally { await this.release(toolShape) } const cutExecution = await nativeHistory.coordinator.capture(nativeHistory.provider, { documentId: input.documentId, documentVersion: input.documentVersion, operationId: cutStageId, operation: 'cut', resultObjectId, ...(input.resultObjectTag === undefined ? {} : { resultObjectTag: input.resultObjectTag }), inputs: [ { inputId: `${cutStageId}:input:0`, objectId: baseInput.objectId, role: 'object', step: baseStep.text, objectTag: baseInput.objectTag, namingEvidence: baseInput.namingEvidence }, { inputId: `${cutStageId}:input:1`, objectId: toolObjectId, role: 'tool', stageId: toolStageId, step: toolResponse.resultStep, namingEvidence: toolResponse.namingEvidence }, ], stages: [{ stageId: cutStageId, operation: 'cut', inputIds: [`${cutStageId}:input:0`, `${cutStageId}:input:1`], ordinal: 1 }], objectStep: baseStep.text, toolStep: toolResponse.resultStep, }) if (cutExecution.status !== 'completed' || !cutExecution.response) throw new Error(`Native OCCT Groove cut stage history ${cutExecution.status}.`) const cutResponse = cutExecution.response.history if (!cutResponse.resultStep?.startsWith('ISO-10303-21;')) throw new Error('Native OCCT Groove cut stage did not return its result STEP.') const cutRecords = mapNativeOcctHistoryRecords(cutResponse, { object: baseInput.objectId, tool: toolObjectId }, { inputs: [{ objectId: baseInput.objectId, shape: baseInput.shape }, { objectId: toolObjectId, shape: profileInput.shape, stageId: toolStageId }], stages: [{ stageId: cutStageId, operation: 'cut', inputObjectIds: [baseInput.objectId, toolObjectId], resultObjectId, ordinal: 1 }], }) const cutShape = await this.importShape({ documentId: input.documentId, documentVersion: input.documentVersion, format: 'step', text: cutResponse.resultStep }) let cutTopology: SubshapeTopology try { cutTopology = await this.topology(cutShape, 0.05) } finally { await this.release(cutShape) } const captures: NativeTopologyHistoryStageCaptureResult[] = [ { stageId: toolStageId, operation: 'revolution', inputObjectIds: [profileInput.objectId], resultObjectId: toolObjectId, ordinal: 0, topology: toolTopology, records: toolRecords, namingEvidence: namingEvidenceForResponse(toolResponse, toolStageId, toolObjectId) }, { stageId: cutStageId, operation: 'cut', inputObjectIds: [baseInput.objectId, toolObjectId], resultObjectId, ordinal: 1, topology: cutTopology, records: cutRecords, namingEvidence: namingEvidenceForResponse(cutResponse, cutStageId, resultObjectId) }, ] const records = cutRecords as NativeTopologyHistoryRecords Object.defineProperty(records, 'stageCaptures', { value: captures, enumerable: false, configurable: false, writable: false }) return records } const execution = await captureHistory({ documentId: input.documentId, documentVersion: input.documentVersion, operationId: input.operationId, operation: input.operation, objectStep: baseStep.text, toolStep: profileStep.text, axisOrigin, direction: sides[0].direction, angle: sides[0].angle, }) if (execution.status !== 'completed' || !execution.response) throw new Error(`Native OCCT history ${execution.status}.`) return mapHistoryRecords(execution.response.history, { object: baseInput.objectId, tool: profileInput.objectId }) } if (input.operation === 'loft') { if (input.inputs.length !== 2) throw new Error('Native OCCT Loft history requires exactly two section inputs.') if (!nativeHistory.provider.capabilities().operations.includes('loft')) throw new Error('Native OCCT history provider does not declare Loft history.') const [firstInput, secondInput] = input.inputs const [firstStep, secondStep] = await Promise.all([ exportHistoryInput(firstInput), exportHistoryInput(secondInput), ]) this.nativeHistoryDocumentVersions.set(input.documentId, input.documentVersion) const execution = await captureHistory({ documentId: input.documentId, documentVersion: input.documentVersion, operationId: input.operationId, operation: input.operation, objectStep: firstStep.text, toolStep: secondStep.text, ruled: input.ruled === true, }) if (execution.status !== 'completed' || !execution.response) throw new Error(`Native OCCT history ${execution.status}.`) return mapHistoryRecords(execution.response.history, { object: firstInput.objectId, tool: secondInput.objectId }) } if (input.operation === 'pipe') { if (input.inputs.length !== 2) throw new Error('Native OCCT Pipe history requires exactly one profile and one spine input.') if (!nativeHistory.provider.capabilities().operations.includes('pipe')) throw new Error('Native OCCT history provider does not declare Pipe history.') const [profileInput, spineInput] = input.inputs const [profileStep, spineStep] = await Promise.all([ exportHistoryInput(profileInput), exportHistoryInput(spineInput), ]) this.nativeHistoryDocumentVersions.set(input.documentId, input.documentVersion) const execution = await captureHistory({ documentId: input.documentId, documentVersion: input.documentVersion, operationId: input.operationId, operation: input.operation, objectStep: profileStep.text, toolStep: spineStep.text, }) if (execution.status !== 'completed' || !execution.response) throw new Error(`Native OCCT history ${execution.status}.`) return mapHistoryRecords(execution.response.history, { object: profileInput.objectId, tool: spineInput.objectId }) } if (input.operation === 'fillet') { if (input.inputs.length !== 1 || typeof input.radius !== 'number' || !Number.isFinite(input.radius) || input.radius <= 0) throw new Error('Native OCCT Fillet history requires one base input and a finite positive radius.') if (!nativeHistory.provider.capabilities().operations.includes('fillet')) throw new Error('Native OCCT history provider does not declare Fillet history.') const [baseInput] = input.inputs const baseStep = await exportHistoryInput(baseInput) this.nativeHistoryDocumentVersions.set(input.documentId, input.documentVersion) const execution = await captureHistory({ documentId: input.documentId, documentVersion: input.documentVersion, operationId: input.operationId, operation: input.operation, objectStep: baseStep.text, radius: input.radius, }) if (execution.status !== 'completed' || !execution.response) throw new Error(`Native OCCT history ${execution.status}.`) return mapHistoryRecords(execution.response.history, { object: baseInput.objectId, tool: baseInput.objectId }) } if (input.operation === 'chamfer') { if (input.inputs.length !== 1 || typeof input.distance !== 'number' || !Number.isFinite(input.distance) || input.distance <= 0) throw new Error('Native OCCT Chamfer history requires one base input and a finite positive distance.') if (!nativeHistory.provider.capabilities().operations.includes('chamfer')) throw new Error('Native OCCT history provider does not declare Chamfer history.') const [baseInput] = input.inputs const baseStep = await exportHistoryInput(baseInput) this.nativeHistoryDocumentVersions.set(input.documentId, input.documentVersion) const execution = await captureHistory({ documentId: input.documentId, documentVersion: input.documentVersion, operationId: input.operationId, operation: input.operation, objectStep: baseStep.text, distance: input.distance, }) if (execution.status !== 'completed' || !execution.response) throw new Error(`Native OCCT history ${execution.status}.`) return mapHistoryRecords(execution.response.history, { object: baseInput.objectId, tool: baseInput.objectId }) } if (input.operation === 'hole') { const position = input.position const direction = input.direction if (input.inputs.length !== 1 || typeof input.radius !== 'number' || !Number.isFinite(input.radius) || input.radius <= 0 || typeof input.depth !== 'number' || !Number.isFinite(input.depth) || input.depth <= 0 || !position || position.some((value) => !Number.isFinite(value)) || !direction || direction.some((value) => !Number.isFinite(value)) || Math.hypot(...direction) <= 0) throw new Error('Native OCCT Hole history requires one base input, finite positive radius/depth, position and direction.') if (!nativeHistory.provider.capabilities().operations.includes('hole')) throw new Error('Native OCCT history provider does not declare Hole history.') const [baseInput] = input.inputs const baseStep = await exportHistoryInput(baseInput) this.nativeHistoryDocumentVersions.set(input.documentId, input.documentVersion) if (nativeHistory.provider.capabilities().operations.includes('cut')) { const toolStageId = `${input.operationId}:native-stage:0` const cutStageId = `${input.operationId}:native-stage:1` const toolObjectId = `${toolStageId}:result` const resultObjectId = input.stages?.length ? input.stages[input.stages.length - 1].resultObjectId ?? `${cutStageId}:result` : `${cutStageId}:result` const toolShape = await this.createCylinder({ documentId: input.documentId, documentVersion: input.documentVersion, radius: input.radius, height: input.depth, center: position, direction, originOnCenter: false }) let toolTopology: SubshapeTopology let toolStep: Awaited> try { [toolTopology, toolStep] = await Promise.all([this.topology(toolShape, 0.05), this.exportStep(toolShape, `${toolObjectId}.step`)]) } finally { await this.release(toolShape) } const cutExecution = await nativeHistory.coordinator.capture(nativeHistory.provider, { documentId: input.documentId, documentVersion: input.documentVersion, operationId: cutStageId, operation: 'cut', resultObjectId, ...(input.resultObjectTag === undefined ? {} : { resultObjectTag: input.resultObjectTag }), inputs: [ { inputId: `${cutStageId}:input:0`, objectId: baseInput.objectId, role: 'object', step: baseStep.text, objectTag: baseInput.objectTag, namingEvidence: baseInput.namingEvidence }, { inputId: `${cutStageId}:input:1`, objectId: toolObjectId, role: 'tool', stageId: toolStageId, step: toolStep.text, namingEvidence: createFinalShapeOnlyNamingEvidence(toolStageId, toolObjectId, 'Hole tool was synthesized by the geometry runtime; provider did not capture its builder naming evidence.') }, ], stages: [{ stageId: cutStageId, operation: 'cut', inputIds: [`${cutStageId}:input:0`, `${cutStageId}:input:1`], ordinal: 1 }], objectStep: baseStep.text, toolStep: toolStep.text, }) if (cutExecution.status !== 'completed' || !cutExecution.response) throw new Error(`Native OCCT Hole cut stage history ${cutExecution.status}.`) const cutResponse = cutExecution.response.history if (!cutResponse.resultStep?.startsWith('ISO-10303-21;')) throw new Error('Native OCCT Hole cut stage did not return its result STEP.') const cutRecords = mapNativeOcctHistoryRecords(cutResponse, { object: baseInput.objectId, tool: toolObjectId }, { inputs: [{ objectId: baseInput.objectId, shape: baseInput.shape }, { objectId: toolObjectId, shape: baseInput.shape, stageId: toolStageId }], stages: [{ stageId: cutStageId, operation: 'cut', inputObjectIds: [baseInput.objectId, toolObjectId], resultObjectId, ordinal: 1 }], }) const cutShape = await this.importShape({ documentId: input.documentId, documentVersion: input.documentVersion, format: 'step', text: cutResponse.resultStep }) let cutTopology: SubshapeTopology try { cutTopology = await this.topology(cutShape, 0.05) } finally { await this.release(cutShape) } const captures: NativeTopologyHistoryStageCaptureResult[] = [ { stageId: toolStageId, operation: 'hole', inputObjectIds: [], resultObjectId: toolObjectId, ordinal: 0, topology: toolTopology, records: [], namingEvidence: createFinalShapeOnlyNamingEvidence(toolStageId, toolObjectId, 'Hole tool was synthesized by the geometry runtime; provider did not capture its builder naming evidence.') }, { stageId: cutStageId, operation: 'cut', inputObjectIds: [baseInput.objectId, toolObjectId], resultObjectId, ordinal: 1, topology: cutTopology, records: cutRecords, namingEvidence: namingEvidenceForResponse(cutResponse, cutStageId, resultObjectId) }, ] const records = cutRecords as NativeTopologyHistoryRecords Object.defineProperty(records, 'stageCaptures', { value: captures, enumerable: false, configurable: false, writable: false }) return records } const execution = await captureHistory({ documentId: input.documentId, documentVersion: input.documentVersion, operationId: input.operationId, operation: input.operation, objectStep: baseStep.text, radius: input.radius, depth: input.depth, position, direction, }) if (execution.status !== 'completed' || !execution.response) throw new Error(`Native OCCT history ${execution.status}.`) return mapHistoryRecords(execution.response.history, { object: baseInput.objectId, tool: baseInput.objectId }) } if (input.operation === 'draft') { const direction = input.direction const neutralPlaneOrigin = input.axisOrigin const neutralPlaneDirection = input.neutralPlaneDirection const angle = input.angle if (input.inputs.length !== 1 || !input.faceIndexes || input.faceIndexes.length !== 1 || !Number.isSafeInteger(input.faceIndexes[0]) || input.faceIndexes[0] < 0 || typeof angle !== 'number' || !Number.isFinite(angle) || angle === 0 || angle <= -89.999 || angle >= 89.999 || !direction || direction.some((value) => !Number.isFinite(value)) || Math.hypot(...direction) <= 0 || !neutralPlaneOrigin || neutralPlaneOrigin.some((value) => !Number.isFinite(value)) || !neutralPlaneDirection || neutralPlaneDirection.some((value) => !Number.isFinite(value)) || Math.hypot(...neutralPlaneDirection) <= 0) throw new Error('Native OCCT Draft history requires one base input, one face index, finite angle, direction and neutral plane.') if (!nativeHistory.provider.capabilities().operations.includes('draft')) throw new Error('Native OCCT history provider does not declare Draft history.') const [baseInput] = input.inputs const baseStep = await exportHistoryInput(baseInput) this.nativeHistoryDocumentVersions.set(input.documentId, input.documentVersion) const execution = await captureHistory({ documentId: input.documentId, documentVersion: input.documentVersion, operationId: input.operationId, operation: input.operation, objectStep: baseStep.text, faceIndex: input.faceIndexes[0], angle, direction, axisOrigin: neutralPlaneOrigin, neutralPlaneDirection, reversed: input.reversed === true, }) if (execution.status !== 'completed' || !execution.response) throw new Error(`Native OCCT history ${execution.status}.`) return mapHistoryRecords(execution.response.history, { object: baseInput.objectId, tool: baseInput.objectId }) } if (input.operation === 'thickness') { const offset = input.offset if (input.inputs.length !== 1 || !input.faceIndexes || input.faceIndexes.length !== 1 || !Number.isSafeInteger(input.faceIndexes[0]) || input.faceIndexes[0] < 0 || typeof offset !== 'number' || !Number.isFinite(offset) || offset === 0 || (input.joinType !== undefined && input.joinType !== 'Arc' && input.joinType !== 'Intersection')) throw new Error('Native OCCT Thickness history requires one base input, one face index, finite non-zero offset and a supported join type.') if (!nativeHistory.provider.capabilities().operations.includes('thickness')) throw new Error('Native OCCT history provider does not declare Thickness history.') const [baseInput] = input.inputs const baseStep = await exportHistoryInput(baseInput) this.nativeHistoryDocumentVersions.set(input.documentId, input.documentVersion) const execution = await captureHistory({ documentId: input.documentId, documentVersion: input.documentVersion, operationId: input.operationId, operation: input.operation, objectStep: baseStep.text, faceIndex: input.faceIndexes[0], offset, joinType: input.joinType, }) if (execution.status !== 'completed' || !execution.response) throw new Error(`Native OCCT history ${execution.status}.`) return mapHistoryRecords(execution.response.history, { object: baseInput.objectId, tool: baseInput.objectId }) } if (input.operation === 'linear-pattern') { if (input.inputs.length !== 1 || !input.direction || input.direction.some((value) => !Number.isFinite(value)) || Math.hypot(...input.direction) <= 0) throw new Error('Native OCCT LinearPattern history requires one base input and a finite non-zero translation vector.') if (!nativeHistory.provider.capabilities().operations.includes('linear-pattern')) throw new Error('Native OCCT history provider does not declare LinearPattern history.') const [baseInput] = input.inputs const baseStep = await exportHistoryInput(baseInput) this.nativeHistoryDocumentVersions.set(input.documentId, input.documentVersion) const execution = await captureHistory({ documentId: input.documentId, documentVersion: input.documentVersion, operationId: input.operationId, operation: input.operation, objectStep: baseStep.text, direction: input.direction }) if (execution.status !== 'completed' || !execution.response) throw new Error(`Native OCCT history ${execution.status}.`) return mapHistoryRecords(execution.response.history, { object: baseInput.objectId, tool: baseInput.objectId }) } if (input.operation === 'polar-pattern') { if (input.inputs.length !== 1 || !input.axisOrigin || input.axisOrigin.some((value) => !Number.isFinite(value)) || !input.direction || input.direction.some((value) => !Number.isFinite(value)) || Math.hypot(...input.direction) <= 0 || typeof input.angle !== 'number' || !Number.isFinite(input.angle) || input.angle === 0 || Math.abs(input.angle) > 360) throw new Error('Native OCCT PolarPattern history requires one base input, a finite axis and a non-zero angle within 360 degrees.') if (!nativeHistory.provider.capabilities().operations.includes('polar-pattern')) throw new Error('Native OCCT history provider does not declare PolarPattern history.') const [baseInput] = input.inputs const baseStep = await exportHistoryInput(baseInput) this.nativeHistoryDocumentVersions.set(input.documentId, input.documentVersion) const execution = await captureHistory({ documentId: input.documentId, documentVersion: input.documentVersion, operationId: input.operationId, operation: input.operation, objectStep: baseStep.text, axisOrigin: input.axisOrigin, direction: input.direction, angle: input.angle }) if (execution.status !== 'completed' || !execution.response) throw new Error(`Native OCCT history ${execution.status}.`) return mapHistoryRecords(execution.response.history, { object: baseInput.objectId, tool: baseInput.objectId }) } if (input.operation === 'mirrored') { if (input.inputs.length !== 1 || !input.axisOrigin || input.axisOrigin.some((value) => !Number.isFinite(value)) || !input.direction || input.direction.some((value) => !Number.isFinite(value)) || Math.hypot(...input.direction) <= 0) throw new Error('Native OCCT Mirrored history requires one base input and a finite mirror plane.') if (!nativeHistory.provider.capabilities().operations.includes('mirrored')) throw new Error('Native OCCT history provider does not declare Mirrored history.') const [baseInput] = input.inputs const baseStep = await exportHistoryInput(baseInput) this.nativeHistoryDocumentVersions.set(input.documentId, input.documentVersion) const execution = await captureHistory({ documentId: input.documentId, documentVersion: input.documentVersion, operationId: input.operationId, operation: input.operation, objectStep: baseStep.text, axisOrigin: input.axisOrigin, direction: input.direction }) if (execution.status !== 'completed' || !execution.response) throw new Error(`Native OCCT history ${execution.status}.`) return mapHistoryRecords(execution.response.history, { object: baseInput.objectId, tool: baseInput.objectId }) } if (input.operation === 'multi-transform') { if (input.inputs.length !== 1) throw new Error('Native OCCT MultiTransform history requires one base input.') if (input.transforms) { if (input.transforms.length < 2 || input.transforms.length > 6) throw new Error('Native ordered MultiTransform history requires between two and six steps.') for (const step of input.transforms) { if (!['linear', 'polar', 'mirrored'].includes(step.type)) throw new Error('Native OCCT MultiTransform history contains an unsupported transform step.') if (step.type === 'linear' && (step.direction.some((value) => !Number.isFinite(value)) || Math.hypot(...step.direction) <= 0)) throw new Error('Native OCCT MultiTransform linear history requires a finite non-zero translation vector.') if (step.type === 'polar' && (step.axisOrigin.some((value) => !Number.isFinite(value)) || step.direction.some((value) => !Number.isFinite(value)) || Math.hypot(...step.direction) <= 0 || !Number.isFinite(step.angle) || step.angle === 0 || Math.abs(step.angle) > 360)) throw new Error('Native OCCT MultiTransform polar history requires a finite axis and a non-zero angle within 360 degrees.') if (step.type === 'mirrored' && (step.axisOrigin.some((value) => !Number.isFinite(value)) || step.direction.some((value) => !Number.isFinite(value)) || Math.hypot(...step.direction) <= 0)) throw new Error('Native OCCT MultiTransform mirrored history requires a finite mirror plane.') } } else { if (!input.transformKind || !['linear', 'polar', 'mirrored'].includes(input.transformKind)) throw new Error('Native OCCT MultiTransform history requires one supported transform step.') if (input.transformKind === 'linear' && (!input.direction || input.direction.some((value) => !Number.isFinite(value)) || Math.hypot(...input.direction) <= 0)) throw new Error('Native OCCT MultiTransform linear history requires a finite non-zero translation vector.') if (input.transformKind === 'polar' && (!input.axisOrigin || input.axisOrigin.some((value) => !Number.isFinite(value)) || !input.direction || input.direction.some((value) => !Number.isFinite(value)) || Math.hypot(...input.direction) <= 0 || typeof input.angle !== 'number' || !Number.isFinite(input.angle) || input.angle === 0 || Math.abs(input.angle) > 360)) throw new Error('Native OCCT MultiTransform polar history requires a finite axis and a non-zero angle within 360 degrees.') if (input.transformKind === 'mirrored' && (!input.axisOrigin || input.axisOrigin.some((value) => !Number.isFinite(value)) || !input.direction || input.direction.some((value) => !Number.isFinite(value)) || Math.hypot(...input.direction) <= 0)) throw new Error('Native OCCT MultiTransform mirrored history requires a finite mirror plane.') } if (!nativeHistory.provider.capabilities().operations.includes('multi-transform')) throw new Error('Native OCCT history provider does not declare MultiTransform history.') const [baseInput] = input.inputs const baseStep = await exportHistoryInput(baseInput) this.nativeHistoryDocumentVersions.set(input.documentId, input.documentVersion) const stagedOperations = input.transforms?.map((step) => step.type === 'linear' ? 'linear-pattern' as const : step.type === 'polar' ? 'polar-pattern' as const : 'mirrored' as const) ?? [] if (input.transforms && input.transforms.length >= 2 && stagedOperations.every((operation) => nativeHistory.provider.capabilities().operations.includes(operation))) { const captures: NativeTopologyHistoryStageCaptureResult[] = [] let previousStep = baseStep.text let previousObjectId = baseInput.objectId let previousStageId: string | undefined let previousNamingEvidence = baseInput.namingEvidence const declaredResultObjectId = input.stages?.length ? input.stages[input.stages.length - 1].resultObjectId : undefined for (let index = 0; index < input.transforms.length; index += 1) { const step = input.transforms[index] const operation = step.type === 'linear' ? 'linear-pattern' as const : step.type === 'polar' ? 'polar-pattern' as const : 'mirrored' as const if (!nativeHistory.provider.capabilities().operations.includes(operation)) throw new Error(`Native OCCT history provider does not declare ${operation}.`) const stageId = `${input.operationId}:native-stage:${index}` const resultObjectId = index === input.transforms.length - 1 && declaredResultObjectId ? declaredResultObjectId : `${stageId}:result` const execution = await nativeHistory.coordinator.capture(nativeHistory.provider, { documentId: input.documentId, documentVersion: input.documentVersion, operationId: `${input.operationId}:native-stage:${index}`, operation, resultObjectId, ...(index === input.transforms.length - 1 && input.resultObjectTag !== undefined ? { resultObjectTag: input.resultObjectTag } : {}), inputs: [{ inputId: `${stageId}:input:0`, objectId: previousObjectId, role: 'object', stageId: previousStageId, step: previousStep, ...(index === 0 && baseInput.objectTag !== undefined ? { objectTag: baseInput.objectTag } : {}), namingEvidence: previousNamingEvidence }], stages: [{ stageId, operation, inputIds: [`${stageId}:input:0`], ordinal: index }], objectStep: previousStep, ...(step.type === 'linear' ? { direction: step.direction } : {}), ...(step.type === 'polar' ? { axisOrigin: step.axisOrigin, direction: step.direction, angle: step.angle } : {}), ...(step.type === 'mirrored' ? { axisOrigin: step.axisOrigin, direction: step.direction } : {}), }) if (execution.status !== 'completed' || !execution.response) throw new Error(`Native OCCT MultiTransform stage ${index} history ${execution.status}.`) const response = execution.response.history if (!response.resultStep?.startsWith('ISO-10303-21;')) throw new Error(`Native OCCT MultiTransform stage ${index} did not return its result STEP.`) const records = mapNativeOcctHistoryRecords(response, { object: previousObjectId, tool: previousObjectId }, { inputs: [{ objectId: previousObjectId, shape: baseInput.shape, ...(previousStageId ? { stageId: previousStageId } : {}) }], stages: [{ stageId, operation, inputObjectIds: [previousObjectId], resultObjectId, ordinal: index }], }) const stageShape = await this.importShape({ documentId: input.documentId, documentVersion: input.documentVersion, format: 'step', text: response.resultStep }) let topology: SubshapeTopology try { topology = await this.topology(stageShape, 0.05) } finally { await this.release(stageShape) } captures.push({ stageId, operation, inputObjectIds: [previousObjectId], resultObjectId, ordinal: index, topology, records, namingEvidence: namingEvidenceForResponse(response, stageId, resultObjectId) }) previousStep = response.resultStep previousObjectId = resultObjectId previousStageId = stageId previousNamingEvidence = namingEvidenceForResponse(response, stageId, resultObjectId) } const records = captures[captures.length - 1].records as NativeTopologyHistoryRecords Object.defineProperty(records, 'stageCaptures', { value: captures, enumerable: false, configurable: false, writable: false }) return records } const execution = await captureHistory({ documentId: input.documentId, documentVersion: input.documentVersion, operationId: input.operationId, operation: input.operation, objectStep: baseStep.text, transformKind: input.transformKind, transforms: input.transforms, axisOrigin: input.axisOrigin, direction: input.direction, angle: input.angle }) if (execution.status !== 'completed' || !execution.response) throw new Error(`Native OCCT history ${execution.status}.`) return mapHistoryRecords(execution.response.history, { object: baseInput.objectId, tool: baseInput.objectId }) } 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([ exportHistoryInput(objectInput), exportHistoryInput(toolInput), ]) const execution = await captureHistory({ 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 mapHistoryRecords(execution.response.history, { object: objectInput.objectId, tool: toolInput.objectId }) } async release(shape: ShapeHandle): Promise { const entry = this.shapes.get(shape.id) if (!entry) return this.assertHandle(shape, entry.handle) this.shapes.delete(shape.id) this.nativeKernelSummaries.delete(shape.id) this.releasedShapeCount += 1 const kernelReference = this.kernelReferences.get(entry.reference.hash) if (kernelReference && kernelReference.count > 1) { kernelReference.count -= 1 this.syncOwnershipMetrics() return } this.kernelReferences.delete(entry.reference.hash) this.syncOwnershipMetrics() const client = this.client if (client && this.capabilitiesState.status === 'ready' && this.shapes.size === 0) await client.occt.cleanAllCache() } dispose() { this.nativeHistory?.coordinator.cancel() this.cancelInitialization?.() this.shapes.clear() this.kernelReferences.clear() this.releasedShapeCount = 0 this.peakShapeCount = 0 this.peakKernelReferenceCount = 0 this.syncOwnershipMetrics() this.client?.occtWorkerManager.cleanPromisesMade() this.worker?.terminate() this.client = null this.worker = null this.initialization = null this.cancelInitialization = null this.nativeHistoryDocumentVersions.clear() this.nativeKernelSummaries.clear() this.nativeHistory = null this.capabilitiesState = unavailableCapabilities() } private async readyClient() { const capabilities = await this.initialize() if (capabilities.status !== 'ready' || !this.client) throw new Error(capabilities.reason || 'Bitbybit OCCT is unavailable.') return this.client } private async createProfileFace(client: BitByBitOCCT, profile: PlanarProfile): Promise { const regions = [{ outer: profile.outer, holes: profile.holes }, ...(profile.additionalRegions ?? [])] const faces = await Promise.all(regions.map(async (region) => { const rings = [normalizedRing(region.outer), ...(region.holes ?? []).map(normalizedRing)] const outerNormal = ringNormal(rings[0]) as Point3 const orientedRings = rings.map((ring, index) => index > 0 && dot(outerNormal, ringNormal(ring) as Point3) > 0 ? [...ring].reverse() : ring) const wires = await Promise.all(orientedRings.map((points) => client.occt.shapes.wire.createPolygonWire({ points }))) return client.occt.shapes.face.createFaceFromWires({ shapes: wires, planar: true }) })) return faces.length === 1 ? faces[0] : client.occt.shapes.compound.makeCompound({ shapes: faces }) } private async createExtrusion(client: BitByBitOCCT, input: LinearFeatureParameters): Promise { const direction = input.direction ?? [0, 1, 0] const directionLength = magnitude(direction) const sign = input.reversed ? -1 : 1 const extrusion: Point3 = direction.map((coordinate) => coordinate / directionLength * input.length * sign) as Point3 const taperAngle = input.taperAngle ?? 0 if (taperAngle !== 0) { const inset = -input.length * Math.tan(taperAngle * Math.PI / 180) const baseRing = normalizedRing(input.profile.outer) const topRing = offsetProfileRing(baseRing, extrusion, inset) const wires = await Promise.all([baseRing, topRing].map((points) => client.occt.shapes.wire.createPolygonWire({ points }))) return client.occt.operations.loftAdvanced({ shapes: wires, makeSolid: true, closed: false, periodic: false, straight: true, nrPeriodicSections: 10, useSmoothing: false, maxUDegree: 3, tolerance: 1e-7, parType: 'approxCentripetal' as Inputs.OCCT.approxParametrizationTypeEnum, }) } const regions = [{ outer: input.profile.outer, holes: input.profile.holes }, ...(input.profile.additionalRegions ?? [])] let faces = await Promise.all(regions.map((region) => this.createProfileFace(client, region))) if (input.symmetricToPlane) { const translation = extrusion.map((coordinate) => -coordinate / 2) as Point3 faces = await Promise.all(faces.map((face) => client.occt.transforms.translate({ shape: face, translation }))) } const solids = await Promise.all(faces.map((face) => client.occt.operations.extrude({ shape: face, direction: extrusion }))) return solids.length === 1 ? solids[0] : client.occt.shapes.compound.makeCompound({ shapes: solids }) } private async throughAllLength(client: BitByBitOCCT, shape: KernelShapeReference, direction: Point3) { const mesh = await client.occt.shapeToMesh({ shape, precision: 0.1, adjustYtoZ: false }) const directionLength = magnitude(direction) const normalized = direction.map((coordinate) => coordinate / directionLength) as Point3 let minimum = Infinity let maximum = -Infinity for (const face of mesh.faceList) { for (let index = 0; index < face.vertexCoord.length; index += 3) { const point: Point3 = [face.vertexCoord[index], face.vertexCoord[index + 1], face.vertexCoord[index + 2]] const projection = dot(point, normalized) minimum = Math.min(minimum, projection) maximum = Math.max(maximum, projection) } } if (!Number.isFinite(minimum) || !Number.isFinite(maximum)) throw new Error('Through-all pocket cannot determine the base Shape extent.') const span = Math.max(maximum - minimum, 1) return 2 * (span + Math.max(1, span * 0.05)) } private registerShape(reference: KernelShapeReference, documentId: string, documentVersion: number) { const handle: ShapeHandle = { id: `shape-${Date.now().toString(36)}-${(++this.sequence).toString(36)}`, kernel: 'bitbybit-occt', kind: 'solid', documentId, documentVersion, } const kernelReference = this.kernelReferences.get(reference.hash) if (kernelReference) kernelReference.count += 1 else this.kernelReferences.set(reference.hash, { count: 1, reference }) this.shapes.set(handle.id, { handle, reference }) this.peakShapeCount = Math.max(this.peakShapeCount, this.shapes.size) this.peakKernelReferenceCount = Math.max(this.peakKernelReferenceCount, this.kernelReferences.size) this.syncOwnershipMetrics() return handle } private syncOwnershipMetrics() { this.capabilitiesState = { ...this.capabilitiesState, shapeCount: this.shapes.size, kernelReferenceCount: this.kernelReferences.size, releasedShapeCount: this.releasedShapeCount, peakShapeCount: this.peakShapeCount, peakKernelReferenceCount: this.peakKernelReferenceCount, } } private resolveShape(shape: ShapeHandle) { if (shape.kernel !== 'bitbybit-occt') throw new Error(`Unsupported geometry kernel: ${shape.kernel}`) const entry = this.shapes.get(shape.id) if (!entry) throw new Error(`Shape handle is unknown or has been released: ${shape.id}`) this.assertHandle(shape, entry.handle) return entry } private assertHandle(actual: ShapeHandle, expected: ShapeHandle) { assertShapeHandleIntegrity(actual, expected) } }