480 lines
24 KiB
TypeScript
480 lines
24 KiB
TypeScript
import { BitByBitOCCT, OccStateEnum } from '@bitbybit-dev/occt-worker'
|
|
import type { Inputs } from '@bitbybit-dev/occt'
|
|
import type { ApplyPlacementInput, BooleanCutInput, BooleanIntersectionInput, BooleanUnionInput, CreateBoxInput, CreateConeInput, CreateCylinderInput, CreateSphereInput, GeometryCapabilities, GeometryDocumentContext, LinearFeatureParameters, MeshAsset, PadInput, PlanarProfile, PocketInput, Point3, RevolutionInput, ShapeHandle, SubshapeRef } from './types'
|
|
import { createSubshapeRefs } from './topologyNaming'
|
|
|
|
type KernelShapeReference = Inputs.OCCT.TopoDSShapePointer
|
|
type KernelMesh = Inputs.OCCT.DecomposedMeshDto
|
|
type ShapeEntry = { handle: ShapeHandle; reference: KernelShapeReference }
|
|
type KernelReferenceEntry = { count: number; reference: KernelShapeReference }
|
|
|
|
const unavailableCapabilities = (): GeometryCapabilities => ({
|
|
provider: 'Bitbybit OCCT',
|
|
version: '1.1.1',
|
|
status: typeof Worker === 'undefined' ? 'unavailable' : 'idle',
|
|
worker: typeof Worker !== 'undefined',
|
|
wasm: typeof WebAssembly !== 'undefined',
|
|
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.`)
|
|
}
|
|
|
|
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 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 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)
|
|
}
|
|
|
|
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 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 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
|
|
}
|
|
|
|
export const validatePlanarProfile = (profile: PlanarProfile) => {
|
|
const rings = [normalizedRing(profile.outer), ...(profile.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)
|
|
}
|
|
|
|
export const validatePadInput = (input: PadInput) => 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')
|
|
}
|
|
|
|
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
|
|
positions.push(...face.vertexCoord)
|
|
if (face.normalCoord.length === face.vertexCoord.length) normals.push(...face.normalCoord)
|
|
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): MeshAsset => {
|
|
const positions: number[] = []
|
|
const normals: number[] = []
|
|
const indices: number[] = []
|
|
mesh.faceList.forEach((face) => appendFace(face, positions, normals, indices))
|
|
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])
|
|
}
|
|
}
|
|
const subshapes: SubshapeRef[] = createSubshapeRefs(shape.id, shape.documentVersion, mesh.faceList.map((face) => ({ vertexCoord: face.vertexCoord, normalCoord: face.normalCoord, triIndexes: face.triIndexes })), tolerance).refs
|
|
return {
|
|
shapeId: shape.id,
|
|
topologyVersion: shape.documentVersion,
|
|
positions: new Float32Array(positions),
|
|
normals: new Float32Array(normals),
|
|
indices: new Uint32Array(indices),
|
|
subshapes,
|
|
bounds: { min, max },
|
|
}
|
|
}
|
|
|
|
export class BitbybitGeometryRuntime {
|
|
private capabilitiesState = unavailableCapabilities()
|
|
private client: BitByBitOCCT | null = null
|
|
private worker: Worker | null = null
|
|
private initialization: Promise<GeometryCapabilities> | null = null
|
|
private cancelInitialization: (() => void) | null = null
|
|
private sequence = 0
|
|
private readonly shapes = new Map<string, ShapeEntry>()
|
|
private readonly kernelReferences = new Map<number, KernelReferenceEntry>()
|
|
|
|
capabilities(): GeometryCapabilities { return { ...this.capabilitiesState } }
|
|
|
|
initialize(): Promise<GeometryCapabilities> {
|
|
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<GeometryCapabilities>((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.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<ShapeHandle> {
|
|
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<ShapeHandle> {
|
|
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<ShapeHandle> {
|
|
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 createCone(input: CreateConeInput): Promise<ShapeHandle> {
|
|
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 applyPlacement(input: ApplyPlacementInput): Promise<ShapeHandle> {
|
|
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 union(input: BooleanUnionInput): Promise<ShapeHandle> {
|
|
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<ShapeHandle> {
|
|
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<ShapeHandle> {
|
|
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 pad(input: PadInput): Promise<ShapeHandle> {
|
|
validatePadInput(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<ShapeHandle> {
|
|
validatePocketInput(input)
|
|
const base = this.resolveShape(input.base).reference
|
|
const client = await this.readyClient()
|
|
const tool = await this.createExtrusion(client, input)
|
|
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<ShapeHandle> {
|
|
validateRevolutionInput(input)
|
|
const client = await this.readyClient()
|
|
const face = await this.createProfileFace(client, input.profile)
|
|
const kernelShape = await client.occtWorkerManager.genericCallToWorkerPromise('plugins.feature.revolution', {
|
|
shape: face,
|
|
axisOrigin: input.axisOrigin ?? [0, 0, 0],
|
|
axisDirection: input.axisDirection ?? [0, 1, 0],
|
|
angle: input.angle ?? 360,
|
|
}) as KernelShapeReference
|
|
return this.registerShape(kernelShape, input.documentId, input.documentVersion)
|
|
}
|
|
|
|
async mesh(shape: ShapeHandle, precision = 0.05): Promise<MeshAsset> {
|
|
finitePositive(precision, 'precision')
|
|
const entry = this.resolveShape(shape)
|
|
const client = await this.readyClient()
|
|
const mesh = await client.occt.shapeToMesh({ shape: entry.reference, precision, adjustYtoZ: false })
|
|
return normalizeBitbybitMesh(entry.handle, mesh, Math.max(1e-5, precision * 0.1))
|
|
}
|
|
|
|
async subshapes(shape: ShapeHandle, precision = 0.05): Promise<SubshapeRef[]> {
|
|
return (await this.mesh(shape, precision)).subshapes ?? []
|
|
}
|
|
|
|
async release(shape: ShapeHandle): Promise<void> {
|
|
const entry = this.shapes.get(shape.id)
|
|
if (!entry) return
|
|
this.assertHandle(shape, entry.handle)
|
|
this.shapes.delete(shape.id)
|
|
const kernelReference = this.kernelReferences.get(entry.reference.hash)
|
|
if (kernelReference && kernelReference.count > 1) {
|
|
kernelReference.count -= 1
|
|
return
|
|
}
|
|
this.kernelReferences.delete(entry.reference.hash)
|
|
const client = this.client
|
|
if (client && this.capabilitiesState.status === 'ready' && this.shapes.size === 0) await client.occt.cleanAllCache()
|
|
}
|
|
|
|
dispose() {
|
|
this.cancelInitialization?.()
|
|
this.shapes.clear()
|
|
this.kernelReferences.clear()
|
|
this.client?.occtWorkerManager.cleanPromisesMade()
|
|
this.worker?.terminate()
|
|
this.client = null
|
|
this.worker = null
|
|
this.initialization = null
|
|
this.cancelInitialization = 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<KernelShapeReference> {
|
|
const rings = [normalizedRing(profile.outer), ...(profile.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 })
|
|
}
|
|
|
|
private async createExtrusion(client: BitByBitOCCT, input: LinearFeatureParameters): Promise<KernelShapeReference> {
|
|
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
|
|
let face = await this.createProfileFace(client, input.profile)
|
|
if (input.symmetricToPlane) {
|
|
const translation = extrusion.map((coordinate) => -coordinate / 2) as Point3
|
|
face = await client.occt.transforms.translate({ shape: face, translation })
|
|
}
|
|
return client.occt.operations.extrude({ shape: face, direction: extrusion })
|
|
}
|
|
|
|
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 })
|
|
return handle
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|