@bitbybit-dev/occt-worker
Version:
Bit By Bit Developers CAD algorithms using OpenCascade Technology kernel adapted for WebWorker
237 lines (236 loc) • 10.3 kB
JavaScript
import { isShapeReference, isEntityReference, createShapeReference, createEntityReference } from "./constants";
/**
* ShapeResolver handles the recursive resolution of shape and document references in input objects.
*
* When shapes or documents are passed from the main thread to the worker, they are serialized as
* reference objects containing a hash. This class recursively traverses any
* data structure to find and replace these references with the actual cached objects.
*
* This solves the limitation of only resolving shapes at the top level of inputs,
* allowing shapes and documents to be nested at any depth within the input structure.
*/
export class ShapeResolver {
constructor(cacheHelper) {
this.cacheHelper = cacheHelper;
}
/**
* Recursively resolves all shape and document references in the given value.
*
* @param value - Any value that may contain shape/document references at any nesting level
* @returns The value with all references replaced by actual cached objects
* @throws Error if a reference hash is not found in cache
*/
resolveShapeReferences(value) {
return this.resolveRecursively(value);
}
/**
* Internal recursive resolution method.
* Handles all cases: primitives, shape references, document references, arrays, and objects.
*
* NOTE: This method is synchronous. File/Blob objects that slip through from the main thread
* cannot be converted here (would require async). They should be converted on the API side
* using prepareStepData() before being sent to the worker.
*/
resolveRecursively(value) {
// Handle null/undefined
if (value === null || value === undefined) {
return value;
}
// Handle primitives (string, number, boolean, etc.)
if (typeof value !== "object") {
return value;
}
// Preserve typed arrays (Uint8Array, Float32Array, ArrayBuffer, etc.) as-is
// These are used for binary data like STEP/glTF file contents
if (ArrayBuffer.isView(value) || value instanceof ArrayBuffer) {
return value;
}
// Detect File/Blob objects that were incorrectly passed to worker
// File extends Blob, so checking Blob covers both
if (typeof Blob !== "undefined" && value instanceof Blob) {
throw new Error("File/Blob objects cannot be passed directly to the worker. " +
"The data should have been converted to ArrayBuffer on the main thread. " +
"This is a bug in the API layer - please report it.");
}
// Check if this is a shape reference
if (isShapeReference(value)) {
return this.resolveFromCache(value.hash, "shape");
}
// Check if this is an entity reference (non-shape OCCT object)
if (isEntityReference(value)) {
return this.resolveFromCache(value.hash, "entity");
}
// Handle arrays - recursively resolve each element
if (Array.isArray(value)) {
return value.map(item => this.resolveRecursively(item));
}
// Handle plain objects - recursively resolve each property
const resolved = {};
for (const key of Object.keys(value)) {
resolved[key] = this.resolveRecursively(value[key]);
}
return resolved;
}
/**
* Retrieves an object from the cache by its hash.
*
* @param hash - The hash identifier of the cached object
* @param type - The type of object ("shape" or "entity")
* @returns The cached object
* @throws Error if the object is not found in cache
*/
resolveFromCache(hash, type) {
const cached = this.cacheHelper.checkCache(hash);
if (!cached) {
throw new Error(`${type === "shape" ? "Shape" : "Entity"} with hash ${hash} not found in cache. ` +
"The cache may have been cleaned. Please regenerate the object.");
}
return cached;
}
}
/**
* ResultSerializer handles the conversion of OCCT shapes and documents back to serializable references.
*
* When returning results from the worker, actual shape/document objects cannot be passed directly
* to the main thread. Instead, they are cached and a reference is returned.
*
* This class provides methods to serialize various result types:
* - Single shapes -> ShapeReference
* - Assembly documents -> DocumentReference
* - Arrays of shapes
* - ObjectDefinition structures (compound shapes with associated data)
* - Arbitrary nested structures containing shapes/documents
* - Non-shape values (passed through unchanged)
*
* The serialization is **recursive**, meaning shapes and documents nested at any depth within
* objects or arrays will be properly converted to references.
*/
export class ResultSerializer {
constructor(cacheHelper) {
this.cacheHelper = cacheHelper;
}
/**
* Serializes a result for transmission back to the main thread.
* Recursively traverses the result to find and serialize all OCCT shapes and documents.
*
* @param result - The result from an OCCT operation
* @returns A serializable version with references instead of actual objects
*/
serializeResult(result) {
return this.serializeRecursively(result);
}
/**
* Recursively serializes a value, converting OCCT objects to references.
*/
serializeRecursively(value) {
// Handle null/undefined
if (value === null || value === undefined) {
return value;
}
// Handle primitives (string, number, boolean, etc.)
if (typeof value !== "object") {
return value;
}
// Preserve typed arrays (Uint8Array, Float32Array, etc.) as-is
// These are used for binary data like STEP/glTF exports
if (ArrayBuffer.isView(value)) {
return value;
}
// Check if this is a shape (TopoDS_Shape - has ShapeType method)
if (this.cacheHelper.isShape(value) && !Array.isArray(value)) {
return createShapeReference(value.hash);
}
// Check if this is a non-shape OCCT entity (e.g., document handle)
if (this.cacheHelper.isEntityHandle(value) && !Array.isArray(value)) {
return createEntityReference(value.hash);
}
// Handle arrays - check if it's an array of OCCT shapes or mixed content
if (Array.isArray(value)) {
// Check if it's an array of shapes (all items have ShapeType)
if (value.length > 0 && this.cacheHelper.isShape(value[0])) {
return value.map(shape => createShapeReference(shape.hash));
}
// Otherwise recursively serialize each element
return value.map(item => this.serializeRecursively(item));
}
// Check for ObjectDefinition structure (compound + shapes + data)
// This is a special case that needs specific handling
if (this.isObjectDefinition(value)) {
return this.serializeObjectDefinition(value);
}
// Handle plain objects - recursively serialize each property
const serialized = {};
for (const key of Object.keys(value)) {
serialized[key] = this.serializeRecursively(value[key]);
}
return serialized;
}
/**
* Type guard to check if value is an ObjectDefinition.
* ObjectDefinition has compound, data, and shapes properties.
*/
isObjectDefinition(value) {
if (value === null || typeof value !== "object") {
return false;
}
const obj = value;
return ("compound" in obj &&
"data" in obj &&
"shapes" in obj &&
Array.isArray(obj.shapes) &&
obj.shapes.length > 0);
}
/**
* Serializes an ObjectDefinition structure.
* Converts the compound and individual shapes to references while preserving data.
*/
serializeObjectDefinition(objDef) {
return Object.assign(Object.assign({}, objDef), { compound: createShapeReference(objDef.compound.hash), shapes: objDef.shapes.map(s => ({
id: s.id,
shape: createShapeReference(s.shape.hash),
})) });
}
}
/**
* FunctionPathResolver handles calling functions on nested objects by path.
*
* Instead of hardcoded path depth checks, this resolver can handle any depth
* of nesting in the OpenCascade service object.
*
* @example
* // Calling "shapes.wire.createCircleWire"
* resolver.callFunction(openCascade, "shapes.wire.createCircleWire", inputs)
* // Equivalent to: openCascade.shapes.wire.createCircleWire(inputs)
*/
export class FunctionPathResolver {
/**
* Resolves a function path and calls it with the provided inputs.
*
* @param root - The root object (OpenCascade service)
* @param functionPath - Dot-separated path to the function (e.g., "shapes.wire.createCircleWire")
* @param inputs - The inputs to pass to the function
* @returns The result of calling the function
* @throws Error if the path cannot be resolved or the function doesn't exist
*/
callFunction(root, functionPath, inputs) {
const pathParts = functionPath.split(".");
// Navigate to the parent object containing the function
let current = root;
for (let i = 0; i < pathParts.length - 1; i++) {
if (current === null || current === undefined || typeof current !== "object") {
throw new Error(`Cannot resolve path "${functionPath}": "${pathParts[i]}" is not an object`);
}
current = current[pathParts[i]];
}
// Get and call the function
const functionName = pathParts[pathParts.length - 1];
if (current === null || current === undefined || typeof current !== "object") {
throw new Error(`Cannot resolve path "${functionPath}": parent is not an object`);
}
const fn = current[functionName];
if (typeof fn !== "function") {
throw new Error(`"${functionPath}" is not a function`);
}
return fn.call(current, inputs);
}
}