P3-03: add OCCT primitives and placement
This commit is contained in:
@@ -1,10 +1,11 @@
|
||||
import { BitByBitOCCT, OccStateEnum } from '@bitbybit-dev/occt-worker'
|
||||
import type { Inputs } from '@bitbybit-dev/occt'
|
||||
import type { CreateBoxInput, GeometryCapabilities, MeshAsset, ShapeHandle } from './types'
|
||||
import type { ApplyPlacementInput, CreateBoxInput, CreateConeInput, CreateCylinderInput, CreateSphereInput, GeometryCapabilities, MeshAsset, ShapeHandle } from './types'
|
||||
|
||||
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',
|
||||
@@ -19,13 +20,63 @@ 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 validateDocumentVersion = (value: number) => {
|
||||
if (!Number.isSafeInteger(value) || value < 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')
|
||||
if (!Number.isSafeInteger(input.documentVersion) || input.documentVersion < 0) throw new RangeError('documentVersion must be a non-negative safe integer.')
|
||||
validateDocumentVersion(input.documentVersion)
|
||||
const center = input.center ?? [0, 0, 0]
|
||||
if (center.length !== 3 || center.some((coordinate) => !Number.isFinite(coordinate))) throw new RangeError('center must contain three finite coordinates.')
|
||||
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')
|
||||
validateDocumentVersion(input.documentVersion)
|
||||
}
|
||||
|
||||
export const validateSphereInput = (input: CreateSphereInput) => {
|
||||
finitePositive(input.radius, 'radius')
|
||||
validateVector(input.center ?? [0, 0, 0], 'center')
|
||||
validateDocumentVersion(input.documentVersion)
|
||||
}
|
||||
|
||||
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')
|
||||
validateDocumentVersion(input.documentVersion)
|
||||
}
|
||||
|
||||
export const validatePlacementInput = (input: ApplyPlacementInput) => {
|
||||
validateVector(input.placement.translation, 'translation')
|
||||
validateVector(input.placement.rotationAxis, 'rotationAxis', false)
|
||||
validateAngle(input.placement.rotationAngle, 'rotationAngle', true)
|
||||
validateDocumentVersion(input.documentVersion)
|
||||
}
|
||||
|
||||
export const assertShapeHandleIntegrity = (actual: ShapeHandle, expected: ShapeHandle) => {
|
||||
@@ -78,6 +129,7 @@ export class BitbybitGeometryRuntime {
|
||||
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 } }
|
||||
|
||||
@@ -113,6 +165,7 @@ export class BitbybitGeometryRuntime {
|
||||
subscription.unsubscribe()
|
||||
worker.terminate()
|
||||
this.shapes.clear()
|
||||
this.kernelReferences.clear()
|
||||
this.worker = null
|
||||
this.client = null
|
||||
this.initialization = null
|
||||
@@ -141,20 +194,62 @@ export class BitbybitGeometryRuntime {
|
||||
center: input.center ?? [0, 0, 0],
|
||||
originOnCenter: input.originOnCenter ?? true,
|
||||
})
|
||||
const handle: ShapeHandle = {
|
||||
id: `shape-${Date.now().toString(36)}-${(++this.sequence).toString(36)}`,
|
||||
kernel: 'bitbybit-occt',
|
||||
kind: 'solid',
|
||||
documentVersion: input.documentVersion,
|
||||
}
|
||||
this.shapes.set(handle.id, { handle, reference: kernelShape })
|
||||
return handle
|
||||
return this.registerShape(kernelShape, 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.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.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.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.documentVersion)
|
||||
}
|
||||
|
||||
async mesh(shape: ShapeHandle, precision = 0.05): Promise<MeshAsset> {
|
||||
finitePositive(precision, 'precision')
|
||||
const client = await this.readyClient()
|
||||
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)
|
||||
}
|
||||
@@ -164,6 +259,12 @@ export class BitbybitGeometryRuntime {
|
||||
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') await client.occt.deleteShape({ shape: entry.reference })
|
||||
}
|
||||
@@ -171,6 +272,7 @@ export class BitbybitGeometryRuntime {
|
||||
dispose() {
|
||||
this.cancelInitialization?.()
|
||||
this.shapes.clear()
|
||||
this.kernelReferences.clear()
|
||||
this.client?.occtWorkerManager.cleanPromisesMade()
|
||||
this.worker?.terminate()
|
||||
this.client = null
|
||||
@@ -186,6 +288,20 @@ export class BitbybitGeometryRuntime {
|
||||
return this.client
|
||||
}
|
||||
|
||||
private registerShape(reference: KernelShapeReference, documentVersion: number) {
|
||||
const handle: ShapeHandle = {
|
||||
id: `shape-${Date.now().toString(36)}-${(++this.sequence).toString(36)}`,
|
||||
kernel: 'bitbybit-occt',
|
||||
kind: 'solid',
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user