mirror of
https://github.com/EsotericSoftware/spine-runtimes.git
synced 2026-02-05 06:44:56 +08:00
[c] Port type extractor to TypeScript and improve codegen
- Ported extract-spine-cpp-types.js to TypeScript in type-extractor.ts - Improved type interfaces with discriminated unions for better type safety - Added proper isConst tracking for const-qualified methods - Fixed exclusions to check method.isConst instead of return type - Removed special type mappings (utf8, spine_void) - primitives pass through unchanged - Made toCTypeName strict with proper error handling - Documented all conversion functions with examples - Excluded SpineObject from extraction (matches JS behavior) - Removed original JS extractor as it's now replaced by TypeScript version The TypeScript extractor produces identical output (107 files, 164 types) while providing better type information including isConst for methods and consistent isStatic fields. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
0a33247f44
commit
22ea76db1b
@ -48,7 +48,7 @@ method: EventData::setAudioPath
|
||||
method: EventData::setVolume
|
||||
method: EventData::setBalance
|
||||
|
||||
# Vector<String> methods need special handling
|
||||
# Array<String> methods need special handling
|
||||
method: AttachmentTimeline.getAttachmentNames
|
||||
|
||||
# BoneLocal/BonePose setScale is overloaded in a confusing way
|
||||
|
||||
@ -18,18 +18,18 @@ export interface ArraySpecialization {
|
||||
export function scanArraySpecializations(typesJson: SpineTypes, exclusions: any[], enumTypes: Set<string>): ArraySpecialization[] {
|
||||
const arrayTypes = new Set<string>();
|
||||
const warnings: string[] = [];
|
||||
|
||||
|
||||
// Extract Array<T> from a type string
|
||||
function extractArrayTypes(typeStr: string | undefined) {
|
||||
if (!typeStr) return;
|
||||
|
||||
|
||||
const regex = /Array<([^>]+)>/g;
|
||||
let match;
|
||||
while ((match = regex.exec(typeStr)) !== null) {
|
||||
arrayTypes.add(match[0]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Process all types
|
||||
for (const header of Object.keys(typesJson)) {
|
||||
for (const type of typesJson[header]) {
|
||||
@ -37,62 +37,69 @@ export function scanArraySpecializations(typesJson: SpineTypes, exclusions: any[
|
||||
if (isTypeExcluded(type.name, exclusions) || type.isTemplate) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
if (!type.members) continue;
|
||||
|
||||
|
||||
for (const member of type.members) {
|
||||
extractArrayTypes(member.returnType);
|
||||
extractArrayTypes(member.type);
|
||||
|
||||
if (member.parameters) {
|
||||
for (const param of member.parameters) {
|
||||
extractArrayTypes(param.type);
|
||||
}
|
||||
switch (member.kind) {
|
||||
case 'method':
|
||||
extractArrayTypes(member.returnType);
|
||||
if (member.parameters) {
|
||||
for (const param of member.parameters) {
|
||||
extractArrayTypes(param.type);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'field':
|
||||
extractArrayTypes(member.type);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Convert to specializations
|
||||
const specializations: ArraySpecialization[] = [];
|
||||
|
||||
|
||||
for (const arrayType of arrayTypes) {
|
||||
const elementMatch = arrayType.match(/Array<(.+)>$/);
|
||||
if (!elementMatch) continue;
|
||||
|
||||
|
||||
const elementType = elementMatch[1].trim();
|
||||
|
||||
|
||||
// Skip template placeholders
|
||||
if (elementType === 'T' || elementType === 'K') {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
// Handle nested arrays - emit warning
|
||||
if (elementType.startsWith('Array<')) {
|
||||
warnings.push(`Skipping nested array: ${arrayType} - manual handling required`);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
// Handle String arrays - emit warning
|
||||
if (elementType === 'String') {
|
||||
warnings.push(`Skipping String array: ${arrayType} - should be fixed in spine-cpp`);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
// Determine type characteristics
|
||||
const isPointer = elementType.endsWith('*');
|
||||
let cleanElementType = isPointer ? elementType.slice(0, -1).trim() : elementType;
|
||||
|
||||
|
||||
// Remove "class " or "struct " prefix if present
|
||||
cleanElementType = cleanElementType.replace(/^(?:class|struct)\s+/, '');
|
||||
const isEnum = enumTypes.has(cleanElementType) || cleanElementType === 'PropertyId';
|
||||
const isPrimitive = !isPointer && !isEnum &&
|
||||
const isPrimitive = !isPointer && !isEnum &&
|
||||
['int', 'float', 'double', 'bool', 'char', 'unsigned short', 'size_t'].includes(cleanElementType);
|
||||
|
||||
|
||||
// Generate C type names
|
||||
let cTypeName: string;
|
||||
let cElementType: string;
|
||||
|
||||
|
||||
if (isPrimitive) {
|
||||
// Map primitive types
|
||||
const typeMap: { [key: string]: string } = {
|
||||
@ -127,7 +134,7 @@ export function scanArraySpecializations(typesJson: SpineTypes, exclusions: any[
|
||||
warnings.push(`Unknown array element type: ${elementType}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
specializations.push({
|
||||
cppType: arrayType,
|
||||
elementType: elementType,
|
||||
@ -138,7 +145,7 @@ export function scanArraySpecializations(typesJson: SpineTypes, exclusions: any[
|
||||
isPrimitive: isPrimitive
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Print warnings
|
||||
if (warnings.length > 0) {
|
||||
console.log('\nArray Generation Warnings:');
|
||||
@ -147,9 +154,9 @@ export function scanArraySpecializations(typesJson: SpineTypes, exclusions: any[
|
||||
}
|
||||
console.log('');
|
||||
}
|
||||
|
||||
|
||||
// Sort by C type name for consistent output
|
||||
specializations.sort((a, b) => a.cTypeName.localeCompare(b.cTypeName));
|
||||
|
||||
|
||||
return specializations;
|
||||
}
|
||||
@ -1,93 +0,0 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
// Load the spine-cpp types
|
||||
const typesJson = JSON.parse(fs.readFileSync(path.join(__dirname, '../spine-cpp-types.json'), 'utf8'));
|
||||
|
||||
// Set to store unique Array<T> types
|
||||
const arrayTypes = new Set<string>();
|
||||
|
||||
// Function to extract Array<T> from a type string
|
||||
function extractArrayTypes(typeStr: string | undefined) {
|
||||
if (!typeStr) return;
|
||||
|
||||
// Find all Array<...> patterns
|
||||
const regex = /Array<([^>]+)>/g;
|
||||
let match;
|
||||
while ((match = regex.exec(typeStr)) !== null) {
|
||||
arrayTypes.add(match[0]); // Full Array<T> string
|
||||
}
|
||||
}
|
||||
|
||||
// Process all types
|
||||
for (const header of Object.keys(typesJson)) {
|
||||
for (const type of typesJson[header]) {
|
||||
if (!type.members) continue;
|
||||
|
||||
for (const member of type.members) {
|
||||
// Check return types
|
||||
extractArrayTypes(member.returnType);
|
||||
|
||||
// Check field types
|
||||
extractArrayTypes(member.type);
|
||||
|
||||
// Check parameter types
|
||||
if (member.parameters) {
|
||||
for (const param of member.parameters) {
|
||||
extractArrayTypes(param.type);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort and display results
|
||||
const sortedArrayTypes = Array.from(arrayTypes).sort();
|
||||
|
||||
console.log(`Found ${sortedArrayTypes.length} Array specializations:\n`);
|
||||
|
||||
// Group by element type
|
||||
const primitiveArrays: string[] = [];
|
||||
const pointerArrays: string[] = [];
|
||||
|
||||
for (const arrayType of sortedArrayTypes) {
|
||||
const elementType = arrayType.match(/Array<(.+)>/)![1];
|
||||
|
||||
if (elementType.includes('*')) {
|
||||
pointerArrays.push(arrayType);
|
||||
} else {
|
||||
primitiveArrays.push(arrayType);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Primitive Arrays:');
|
||||
for (const type of primitiveArrays) {
|
||||
console.log(` ${type}`);
|
||||
}
|
||||
|
||||
console.log('\nPointer Arrays:');
|
||||
for (const type of pointerArrays) {
|
||||
console.log(` ${type}`);
|
||||
}
|
||||
|
||||
// Generate C type names
|
||||
console.log('\nC Type Names:');
|
||||
console.log('\nPrimitive Arrays:');
|
||||
for (const type of primitiveArrays) {
|
||||
const elementType = type.match(/Array<(.+)>/)![1];
|
||||
const cType = elementType === 'float' ? 'float' :
|
||||
elementType === 'int' ? 'int32_t' :
|
||||
elementType === 'unsigned short' ? 'uint16_t' :
|
||||
elementType;
|
||||
console.log(` ${type} -> spine_array_${cType.replace(/ /g, '_')}`);
|
||||
}
|
||||
|
||||
console.log('\nPointer Arrays:');
|
||||
for (const type of pointerArrays) {
|
||||
const elementType = type.match(/Array<(.+?)\s*\*/)![1].trim();
|
||||
// Remove 'class ' prefix if present
|
||||
const cleanType = elementType.replace(/^class\s+/, '');
|
||||
// Convert to snake case
|
||||
const snakeCase = cleanType.replace(/([A-Z])/g, '_$1').toLowerCase().replace(/^_/, '');
|
||||
console.log(` ${type} -> spine_array_${snakeCase}`);
|
||||
}
|
||||
@ -1,9 +1,9 @@
|
||||
import { Type, Member, toSnakeCase, toCTypeName } from '../types';
|
||||
import { Type, Member, toSnakeCase, Method } from '../types';
|
||||
import { ArraySpecialization } from '../array-scanner';
|
||||
|
||||
export class ArrayGenerator {
|
||||
private arrayType: Type | undefined;
|
||||
|
||||
|
||||
constructor(private typesJson: any) {
|
||||
// Find the Array type definition
|
||||
for (const header of Object.keys(typesJson)) {
|
||||
@ -14,19 +14,19 @@ export class ArrayGenerator {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Generates arrays.h and arrays.cpp content
|
||||
*/
|
||||
generate(specializations: ArraySpecialization[]): { header: string[], source: string[] } {
|
||||
const header: string[] = [];
|
||||
const source: string[] = [];
|
||||
|
||||
|
||||
if (!this.arrayType) {
|
||||
console.error('ERROR: Array type not found in spine-cpp types');
|
||||
return { header, source };
|
||||
}
|
||||
|
||||
|
||||
// Header file
|
||||
header.push('#ifndef SPINE_C_ARRAYS_H');
|
||||
header.push('#define SPINE_C_ARRAYS_H');
|
||||
@ -38,7 +38,7 @@ export class ArrayGenerator {
|
||||
header.push('extern "C" {');
|
||||
header.push('#endif');
|
||||
header.push('');
|
||||
|
||||
|
||||
// Source file
|
||||
source.push('#include "arrays.h"');
|
||||
source.push('#include <spine/Array.h>');
|
||||
@ -46,50 +46,48 @@ export class ArrayGenerator {
|
||||
source.push('');
|
||||
source.push('using namespace spine;');
|
||||
source.push('');
|
||||
|
||||
|
||||
// Generate for each specialization
|
||||
for (const spec of specializations) {
|
||||
console.log(`Generating array specialization: ${spec.cTypeName}`);
|
||||
this.generateSpecialization(spec, header, source);
|
||||
}
|
||||
|
||||
|
||||
// Close header
|
||||
header.push('#ifdef __cplusplus');
|
||||
header.push('}');
|
||||
header.push('#endif');
|
||||
header.push('');
|
||||
header.push('#endif // SPINE_C_ARRAYS_H');
|
||||
|
||||
|
||||
return { header, source };
|
||||
}
|
||||
|
||||
|
||||
private generateSpecialization(spec: ArraySpecialization, header: string[], source: string[]) {
|
||||
// Opaque type declaration
|
||||
header.push(`// ${spec.cppType}`);
|
||||
header.push(`SPINE_OPAQUE_TYPE(${spec.cTypeName})`);
|
||||
header.push('');
|
||||
|
||||
|
||||
// Get Array methods to wrap
|
||||
const methods = this.arrayType!.members?.filter(m =>
|
||||
m.kind === 'method' &&
|
||||
!m.isStatic &&
|
||||
!m.name.includes('operator')
|
||||
) || [];
|
||||
|
||||
const methods = this.arrayType!.members?.filter(m =>
|
||||
m.kind === 'method'
|
||||
).filter(m => !m.isStatic && !m.name.includes('operator')) || [];
|
||||
|
||||
// Generate create method (constructor)
|
||||
header.push(`SPINE_C_EXPORT ${spec.cTypeName} ${spec.cTypeName}_create();`);
|
||||
source.push(`${spec.cTypeName} ${spec.cTypeName}_create() {`);
|
||||
source.push(` return (${spec.cTypeName}) new (__FILE__, __LINE__) ${spec.cppType}();`);
|
||||
source.push('}');
|
||||
source.push('');
|
||||
|
||||
|
||||
// Generate create with capacity
|
||||
header.push(`SPINE_C_EXPORT ${spec.cTypeName} ${spec.cTypeName}_create_with_capacity(int32_t capacity);`);
|
||||
source.push(`${spec.cTypeName} ${spec.cTypeName}_create_with_capacity(int32_t capacity) {`);
|
||||
source.push(` return (${spec.cTypeName}) new (__FILE__, __LINE__) ${spec.cppType}(capacity);`);
|
||||
source.push('}');
|
||||
source.push('');
|
||||
|
||||
|
||||
// Generate dispose
|
||||
header.push(`SPINE_C_EXPORT void ${spec.cTypeName}_dispose(${spec.cTypeName} array);`);
|
||||
source.push(`void ${spec.cTypeName}_dispose(${spec.cTypeName} array) {`);
|
||||
@ -97,7 +95,7 @@ export class ArrayGenerator {
|
||||
source.push(` delete (${spec.cppType}*) array;`);
|
||||
source.push('}');
|
||||
source.push('');
|
||||
|
||||
|
||||
// Generate hardcoded get/set methods
|
||||
header.push(`SPINE_C_EXPORT ${spec.cElementType} ${spec.cTypeName}_get(${spec.cTypeName} array, int32_t index);`);
|
||||
source.push(`${spec.cElementType} ${spec.cTypeName}_get(${spec.cTypeName} array, int32_t index) {`);
|
||||
@ -106,7 +104,7 @@ export class ArrayGenerator {
|
||||
source.push(` return ${this.convertFromCpp(spec, '(*_array)[index]')};`);
|
||||
source.push('}');
|
||||
source.push('');
|
||||
|
||||
|
||||
header.push(`SPINE_C_EXPORT void ${spec.cTypeName}_set(${spec.cTypeName} array, int32_t index, ${spec.cElementType} value);`);
|
||||
source.push(`void ${spec.cTypeName}_set(${spec.cTypeName} array, int32_t index, ${spec.cElementType} value) {`);
|
||||
source.push(` if (!array) return;`);
|
||||
@ -114,22 +112,22 @@ export class ArrayGenerator {
|
||||
source.push(` (*_array)[index] = ${this.convertToCpp(spec, 'value')};`);
|
||||
source.push('}');
|
||||
source.push('');
|
||||
|
||||
|
||||
// Generate wrapper for each Array method
|
||||
for (const method of methods) {
|
||||
this.generateMethodWrapper(spec, method, header, source);
|
||||
}
|
||||
|
||||
|
||||
header.push('');
|
||||
}
|
||||
|
||||
private generateMethodWrapper(spec: ArraySpecialization, method: Member, header: string[], source: string[]) {
|
||||
|
||||
private generateMethodWrapper(spec: ArraySpecialization, method: Method, header: string[], source: string[]) {
|
||||
// Skip constructors and destructors
|
||||
if (method.name === 'Array' || method.name === '~Array') return;
|
||||
|
||||
|
||||
// Build C function name
|
||||
const cFuncName = `${spec.cTypeName}_${toSnakeCase(method.name)}`;
|
||||
|
||||
|
||||
// Convert return type
|
||||
let returnType = 'void';
|
||||
let hasReturn = false;
|
||||
@ -154,14 +152,14 @@ export class ArrayGenerator {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Build parameter list
|
||||
const cParams: string[] = [`${spec.cTypeName} array`];
|
||||
const cppArgs: string[] = [];
|
||||
|
||||
|
||||
if (method.parameters) {
|
||||
for (const param of method.parameters) {
|
||||
if (param.type === 'T' || param.type === spec.elementType ||
|
||||
if (param.type === 'T' || param.type === spec.elementType ||
|
||||
param.type === 'const T &' || param.type === `const ${spec.elementType} &`) {
|
||||
cParams.push(`${spec.cElementType} ${param.name}`);
|
||||
cppArgs.push(this.convertToCpp(spec, param.name));
|
||||
@ -178,15 +176,15 @@ export class ArrayGenerator {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Generate declaration
|
||||
header.push(`SPINE_C_EXPORT ${returnType} ${cFuncName}(${cParams.join(', ')});`);
|
||||
|
||||
|
||||
// Generate implementation
|
||||
source.push(`${returnType} ${cFuncName}(${cParams.join(', ')}) {`);
|
||||
source.push(` if (!array) return${hasReturn ? ' ' + this.getDefaultReturn(returnType, spec) : ''};`);
|
||||
source.push(` ${spec.cppType} *_array = (${spec.cppType}*) array;`);
|
||||
|
||||
|
||||
const call = `_array->${method.name}(${cppArgs.join(', ')})`;
|
||||
if (hasReturn) {
|
||||
if (returnType === spec.cElementType) {
|
||||
@ -197,11 +195,11 @@ export class ArrayGenerator {
|
||||
} else {
|
||||
source.push(` ${call};`);
|
||||
}
|
||||
|
||||
|
||||
source.push('}');
|
||||
source.push('');
|
||||
}
|
||||
|
||||
|
||||
private getDefaultValue(spec: ArraySpecialization): string {
|
||||
if (spec.isPointer) return 'nullptr';
|
||||
if (spec.isPrimitive) {
|
||||
@ -211,7 +209,7 @@ export class ArrayGenerator {
|
||||
if (spec.isEnum) return '0';
|
||||
return '0';
|
||||
}
|
||||
|
||||
|
||||
private getDefaultReturn(returnType: string, spec: ArraySpecialization): string {
|
||||
if (returnType === 'bool') return 'false';
|
||||
if (returnType === 'size_t' || returnType === 'int32_t') return '0';
|
||||
@ -219,7 +217,7 @@ export class ArrayGenerator {
|
||||
if (returnType === spec.cTypeName) return 'nullptr';
|
||||
return '0';
|
||||
}
|
||||
|
||||
|
||||
private convertFromCpp(spec: ArraySpecialization, expr: string): string {
|
||||
if (spec.isPointer) {
|
||||
return `(${spec.cElementType}) ${expr}`;
|
||||
@ -229,7 +227,7 @@ export class ArrayGenerator {
|
||||
}
|
||||
return expr;
|
||||
}
|
||||
|
||||
|
||||
private convertToCpp(spec: ArraySpecialization, expr: string): string {
|
||||
if (spec.isPointer) {
|
||||
return `(${spec.elementType}) ${expr}`;
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { Type, Member, toSnakeCase, toCFunctionName, toCTypeName } from '../types';
|
||||
import { Type, Member, toSnakeCase, toCFunctionName, toCTypeName, Constructor } from '../types';
|
||||
|
||||
export interface GeneratorResult {
|
||||
declarations: string[];
|
||||
@ -6,15 +6,17 @@ export interface GeneratorResult {
|
||||
}
|
||||
|
||||
export class ConstructorGenerator {
|
||||
constructor(private validTypes: Set<string>) {}
|
||||
|
||||
generate(type: Type): GeneratorResult {
|
||||
const declarations: string[] = [];
|
||||
const implementations: string[] = [];
|
||||
|
||||
|
||||
if (!type.members) return { declarations, implementations };
|
||||
|
||||
|
||||
const constructors = type.members.filter(m => m.kind === 'constructor');
|
||||
const cTypeName = `spine_${toSnakeCase(type.name)}`;
|
||||
|
||||
|
||||
// Skip constructor generation for abstract types
|
||||
if (!type.isAbstract) {
|
||||
// Generate create functions for each constructor
|
||||
@ -22,79 +24,79 @@ export class ConstructorGenerator {
|
||||
for (const constructor of constructors) {
|
||||
const funcName = this.getCreateFunctionName(type.name, constructor, constructorIndex);
|
||||
const params = this.generateParameters(constructor);
|
||||
|
||||
|
||||
// Declaration
|
||||
declarations.push(`SPINE_C_EXPORT ${cTypeName} ${funcName}(${params.declaration});`);
|
||||
|
||||
|
||||
// Implementation
|
||||
implementations.push(`${cTypeName} ${funcName}(${params.declaration}) {`);
|
||||
implementations.push(` ${type.name} *obj = new (__FILE__, __LINE__) ${type.name}(${params.call});`);
|
||||
implementations.push(` return (${cTypeName}) obj;`);
|
||||
implementations.push(`}`);
|
||||
implementations.push('');
|
||||
|
||||
|
||||
constructorIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Always generate dispose function
|
||||
declarations.push(`SPINE_C_EXPORT void ${cTypeName}_dispose(${cTypeName} obj);`);
|
||||
|
||||
|
||||
implementations.push(`void ${cTypeName}_dispose(${cTypeName} obj) {`);
|
||||
implementations.push(` if (!obj) return;`);
|
||||
implementations.push(` delete (${type.name} *) obj;`);
|
||||
implementations.push(`}`);
|
||||
implementations.push('');
|
||||
|
||||
|
||||
return { declarations, implementations };
|
||||
}
|
||||
|
||||
private getCreateFunctionName(typeName: string, constructor: Member, index: number): string {
|
||||
|
||||
private getCreateFunctionName(typeName: string, constructor: Constructor, index: number): string {
|
||||
const baseName = `spine_${toSnakeCase(typeName)}_create`;
|
||||
|
||||
|
||||
if (!constructor.parameters || constructor.parameters.length === 0) {
|
||||
return baseName;
|
||||
}
|
||||
|
||||
|
||||
if (index === 0) {
|
||||
return baseName;
|
||||
}
|
||||
|
||||
|
||||
// Generate name based on parameter types
|
||||
const paramNames = constructor.parameters
|
||||
.map(p => this.getParamTypeSuffix(p.type))
|
||||
.join('_');
|
||||
|
||||
|
||||
return `${baseName}_with_${paramNames}`;
|
||||
}
|
||||
|
||||
|
||||
private getParamTypeSuffix(type: string): string {
|
||||
if (type.includes('float')) return 'float';
|
||||
if (type.includes('int')) return 'int';
|
||||
if (type.includes('bool')) return 'bool';
|
||||
if (type.includes('String') || type.includes('char')) return 'string';
|
||||
|
||||
|
||||
// Extract class name from pointers/references
|
||||
const match = type.match(/(?:const\s+)?(\w+)(?:\s*[*&])?/);
|
||||
if (match) {
|
||||
return toSnakeCase(match[1]);
|
||||
}
|
||||
|
||||
|
||||
return 'param';
|
||||
}
|
||||
|
||||
private generateParameters(constructor: Member): { declaration: string; call: string } {
|
||||
|
||||
private generateParameters(constructor: Constructor): { declaration: string; call: string } {
|
||||
if (!constructor.parameters || constructor.parameters.length === 0) {
|
||||
return { declaration: 'void', call: '' };
|
||||
}
|
||||
|
||||
|
||||
const declParts: string[] = [];
|
||||
const callParts: string[] = [];
|
||||
|
||||
|
||||
for (const param of constructor.parameters) {
|
||||
const cType = toCTypeName(param.type);
|
||||
const cType = toCTypeName(param.type, this.validTypes);
|
||||
declParts.push(`${cType} ${param.name}`);
|
||||
|
||||
|
||||
// Convert C type back to C++ for the call
|
||||
let callExpr = param.name;
|
||||
if (param.type === 'const String &' || param.type === 'String') {
|
||||
@ -106,10 +108,10 @@ export class ConstructorGenerator {
|
||||
const baseType = param.type.replace(/^(?:const\s+)?(.+?)\s*&$/, '$1').trim();
|
||||
callExpr = `*(${baseType}*) ${param.name}`;
|
||||
}
|
||||
|
||||
|
||||
callParts.push(callExpr);
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
declaration: declParts.join(', '),
|
||||
call: callParts.join(', ')
|
||||
|
||||
@ -1,24 +1,23 @@
|
||||
import { Type, Member, toSnakeCase, toCFunctionName, toCTypeName, Exclusion } from '../types';
|
||||
import { Type, Member, toSnakeCase, toCFunctionName, toCTypeName, Exclusion, Method } from '../types';
|
||||
import { isMethodExcluded } from '../exclusions';
|
||||
import { GeneratorResult } from './constructor-generator';
|
||||
|
||||
export class MethodGenerator {
|
||||
constructor(private exclusions: Exclusion[]) {}
|
||||
|
||||
constructor(private exclusions: Exclusion[], private validTypes: Set<string>) {}
|
||||
|
||||
generate(type: Type): GeneratorResult {
|
||||
const declarations: string[] = [];
|
||||
const implementations: string[] = [];
|
||||
|
||||
|
||||
if (!type.members) return { declarations, implementations };
|
||||
|
||||
const methods = type.members.filter(m =>
|
||||
m.kind === 'method' &&
|
||||
!m.isStatic &&
|
||||
!isMethodExcluded(type.name, m.name, this.exclusions, m)
|
||||
);
|
||||
|
||||
|
||||
const methods = type.members.filter(m =>
|
||||
m.kind === 'method'
|
||||
).filter(m => !isMethodExcluded(type.name, m.name, this.exclusions, m))
|
||||
.filter(m => !m.isStatic);
|
||||
|
||||
// Check for const/non-const method pairs
|
||||
const methodGroups = new Map<string, Member[]>();
|
||||
const methodGroups = new Map<string, Method[]>();
|
||||
for (const method of methods) {
|
||||
const key = method.name + '(' + (method.parameters?.map(p => p.type).join(',') || '') + ')';
|
||||
if (!methodGroups.has(key)) {
|
||||
@ -26,10 +25,10 @@ export class MethodGenerator {
|
||||
}
|
||||
methodGroups.get(key)!.push(method);
|
||||
}
|
||||
|
||||
|
||||
// Collect all errors before failing
|
||||
const errors: string[] = [];
|
||||
|
||||
|
||||
// Report errors for duplicate methods with different signatures
|
||||
for (const [signature, group] of methodGroups) {
|
||||
if (group.length > 1) {
|
||||
@ -38,7 +37,7 @@ export class MethodGenerator {
|
||||
if (returnTypes.size > 1) {
|
||||
let error = `\nERROR: Type '${type.name}' has multiple versions of method '${group[0].name}' with different return types:\n`;
|
||||
error += `This is likely a const/non-const overload pattern in C++ which cannot be represented in C.\n\n`;
|
||||
|
||||
|
||||
for (const method of group) {
|
||||
const source = method.fromSupertype ? ` (inherited from ${method.fromSupertype})` : '';
|
||||
error += ` - ${method.returnType || 'void'} ${method.name}()${source}\n`;
|
||||
@ -51,12 +50,12 @@ export class MethodGenerator {
|
||||
error += ` 1. Only exposing the const version in the C API\n`;
|
||||
error += ` 2. Renaming one of the methods in C++\n`;
|
||||
error += ` 3. Adding the method to exclusions.txt\n`;
|
||||
|
||||
|
||||
errors.push(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// If we have errors, throw them all at once
|
||||
if (errors.length > 0) {
|
||||
console.error("=".repeat(80));
|
||||
@ -70,53 +69,53 @@ export class MethodGenerator {
|
||||
console.error("=".repeat(80));
|
||||
throw new Error(`Cannot generate C API due to ${errors.length} const/non-const overloaded method conflicts.`);
|
||||
}
|
||||
|
||||
|
||||
// For now, continue with the unique methods
|
||||
const uniqueMethods = methods;
|
||||
|
||||
|
||||
const cTypeName = `spine_${toSnakeCase(type.name)}`;
|
||||
|
||||
|
||||
// Track method names to detect overloads
|
||||
const methodCounts = new Map<string, number>();
|
||||
for (const method of uniqueMethods) {
|
||||
const count = methodCounts.get(method.name) || 0;
|
||||
methodCounts.set(method.name, count + 1);
|
||||
}
|
||||
|
||||
|
||||
// Track how many times we've seen each method name
|
||||
const methodSeenCounts = new Map<string, number>();
|
||||
|
||||
|
||||
for (const method of uniqueMethods) {
|
||||
// Handle getters
|
||||
if (method.name.startsWith('get') && (!method.parameters || method.parameters.length === 0)) {
|
||||
const propName = method.name.substring(3);
|
||||
|
||||
// Check if return type is Vector
|
||||
if (method.returnType && method.returnType.includes('Vector<')) {
|
||||
// For Vector types, only generate collection accessors
|
||||
|
||||
// Check if return type is Array
|
||||
if (method.returnType && method.returnType.includes('Array<')) {
|
||||
// For Array types, only generate collection accessors
|
||||
this.generateCollectionAccessors(type, method, propName, declarations, implementations);
|
||||
} else if (method.name === 'getRTTI') {
|
||||
// Special handling for getRTTI - make it static
|
||||
const funcName = toCFunctionName(type.name, 'get_rtti');
|
||||
const returnType = 'spine_rtti';
|
||||
|
||||
|
||||
declarations.push(`SPINE_C_EXPORT ${returnType} ${funcName}();`);
|
||||
|
||||
|
||||
implementations.push(`${returnType} ${funcName}() {`);
|
||||
implementations.push(` return (spine_rtti) &${type.name}::rtti;`);
|
||||
implementations.push(`}`);
|
||||
implementations.push('');
|
||||
} else {
|
||||
// For non-Vector types, generate regular getter
|
||||
// For non-Array types, generate regular getter
|
||||
const funcName = toCFunctionName(type.name, method.name);
|
||||
const returnType = toCTypeName(method.returnType || 'void');
|
||||
|
||||
const returnType = toCTypeName(method.returnType || 'void', this.validTypes);
|
||||
|
||||
declarations.push(`SPINE_C_EXPORT ${returnType} ${funcName}(${cTypeName} obj);`);
|
||||
|
||||
|
||||
implementations.push(`${returnType} ${funcName}(${cTypeName} obj) {`);
|
||||
implementations.push(` if (!obj) return ${this.getDefaultReturn(returnType)};`);
|
||||
implementations.push(` ${type.name} *_obj = (${type.name} *) obj;`);
|
||||
|
||||
|
||||
const callExpr = this.generateMethodCall('_obj', method);
|
||||
implementations.push(` return ${callExpr};`);
|
||||
implementations.push(`}`);
|
||||
@ -126,14 +125,14 @@ export class MethodGenerator {
|
||||
// Handle setters
|
||||
else if (method.name.startsWith('set') && method.parameters && method.parameters.length === 1) {
|
||||
const funcName = toCFunctionName(type.name, method.name);
|
||||
const paramType = toCTypeName(method.parameters[0].type);
|
||||
|
||||
const paramType = toCTypeName(method.parameters[0].type, this.validTypes);
|
||||
|
||||
declarations.push(`SPINE_C_EXPORT void ${funcName}(${cTypeName} obj, ${paramType} value);`);
|
||||
|
||||
|
||||
implementations.push(`void ${funcName}(${cTypeName} obj, ${paramType} value) {`);
|
||||
implementations.push(` if (!obj) return;`);
|
||||
implementations.push(` ${type.name} *_obj = (${type.name} *) obj;`);
|
||||
|
||||
|
||||
const callExpr = this.generateSetterCall('_obj', method, 'value');
|
||||
implementations.push(` ${callExpr};`);
|
||||
implementations.push(`}`);
|
||||
@ -145,31 +144,31 @@ export class MethodGenerator {
|
||||
const isOverloaded = (methodCounts.get(method.name) || 0) > 1;
|
||||
const seenCount = methodSeenCounts.get(method.name) || 0;
|
||||
methodSeenCounts.set(method.name, seenCount + 1);
|
||||
|
||||
|
||||
// Generate function name with suffix for overloads
|
||||
let funcName = toCFunctionName(type.name, method.name);
|
||||
|
||||
|
||||
// Check for naming conflicts with type names
|
||||
if (method.name === 'pose' && type.name === 'Bone') {
|
||||
// Rename bone_pose() method to avoid conflict with spine_bone_pose type
|
||||
funcName = toCFunctionName(type.name, 'update_pose');
|
||||
}
|
||||
|
||||
|
||||
if (isOverloaded && seenCount > 0) {
|
||||
// Add parameter count suffix for overloaded methods
|
||||
const paramCount = method.parameters ? method.parameters.length : 0;
|
||||
funcName = `${funcName}_${paramCount}`;
|
||||
}
|
||||
|
||||
const returnType = toCTypeName(method.returnType || 'void');
|
||||
|
||||
const returnType = toCTypeName(method.returnType || 'void', this.validTypes);
|
||||
const params = this.generateMethodParameters(cTypeName, method);
|
||||
|
||||
|
||||
declarations.push(`SPINE_C_EXPORT ${returnType} ${funcName}(${params.declaration});`);
|
||||
|
||||
|
||||
implementations.push(`${returnType} ${funcName}(${params.declaration}) {`);
|
||||
implementations.push(` if (!obj) return ${this.getDefaultReturn(returnType)};`);
|
||||
implementations.push(` ${type.name} *_obj = (${type.name} *) obj;`);
|
||||
|
||||
|
||||
const callExpr = this.generateMethodCall('_obj', method, params.call);
|
||||
if (returnType === 'void') {
|
||||
implementations.push(` ${callExpr};`);
|
||||
@ -180,18 +179,18 @@ export class MethodGenerator {
|
||||
implementations.push('');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return { declarations, implementations };
|
||||
}
|
||||
|
||||
private generateCollectionAccessors(type: Type, method: Member, propName: string,
|
||||
|
||||
private generateCollectionAccessors(type: Type, method: Method, propName: string,
|
||||
declarations: string[], implementations: string[]) {
|
||||
const cTypeName = `spine_${toSnakeCase(type.name)}`;
|
||||
const propSnake = toSnakeCase(propName);
|
||||
const vectorMatch = method.returnType!.match(/Vector<(.+?)>/);
|
||||
if (!vectorMatch) return;
|
||||
|
||||
const elementType = vectorMatch[1].trim().replace(/\s*\*$/, '');
|
||||
const arrayMatch = method.returnType!.match(/Array<(.+?)>/);
|
||||
if (!arrayMatch) return;
|
||||
|
||||
const elementType = arrayMatch[1].trim().replace(/\s*\*$/, '');
|
||||
let cElementType: string;
|
||||
if (elementType === 'int') {
|
||||
cElementType = 'int32_t';
|
||||
@ -206,43 +205,43 @@ export class MethodGenerator {
|
||||
} else {
|
||||
cElementType = `spine_${toSnakeCase(elementType)}`;
|
||||
}
|
||||
|
||||
|
||||
// Get count function
|
||||
const getCountFunc = `spine_${toSnakeCase(type.name)}_get_num_${propSnake}`;
|
||||
declarations.push(`SPINE_C_EXPORT int32_t ${getCountFunc}(${cTypeName} obj);`);
|
||||
|
||||
|
||||
implementations.push(`int32_t ${getCountFunc}(${cTypeName} obj) {`);
|
||||
implementations.push(` if (!obj) return 0;`);
|
||||
implementations.push(` ${type.name} *_obj = (${type.name} *) obj;`);
|
||||
implementations.push(` return (int32_t) _obj->get${propName}().size();`);
|
||||
implementations.push(`}`);
|
||||
implementations.push('');
|
||||
|
||||
|
||||
// Get array function
|
||||
const getArrayFunc = `spine_${toSnakeCase(type.name)}_get_${propSnake}`;
|
||||
declarations.push(`SPINE_C_EXPORT ${cElementType} *${getArrayFunc}(${cTypeName} obj);`);
|
||||
|
||||
|
||||
implementations.push(`${cElementType} *${getArrayFunc}(${cTypeName} obj) {`);
|
||||
implementations.push(` if (!obj) return nullptr;`);
|
||||
implementations.push(` ${type.name} *_obj = (${type.name} *) obj;`);
|
||||
|
||||
// Handle const vs non-const vectors
|
||||
|
||||
// Handle const vs non-const arrays
|
||||
if (method.isConst || method.returnType!.includes('const')) {
|
||||
// For const vectors, we need to copy the data or use data() method
|
||||
// For const arrays, we need to copy the data or use data() method
|
||||
implementations.push(` auto& vec = _obj->get${propName}();`);
|
||||
implementations.push(` if (vec.size() == 0) return nullptr;`);
|
||||
implementations.push(` return (${cElementType} *) &vec[0];`);
|
||||
} else {
|
||||
implementations.push(` return (${cElementType} *) _obj->get${propName}().buffer();`);
|
||||
}
|
||||
|
||||
|
||||
implementations.push(`}`);
|
||||
implementations.push('');
|
||||
}
|
||||
|
||||
private generateMethodCall(objName: string, method: Member, args?: string): string {
|
||||
|
||||
private generateMethodCall(objName: string, method: Method, args?: string): string {
|
||||
let call = `${objName}->${method.name}(${args || ''})`;
|
||||
|
||||
|
||||
// Handle return type conversions
|
||||
if (method.returnType) {
|
||||
if (method.returnType === 'const String &' || method.returnType === 'String') {
|
||||
@ -251,37 +250,37 @@ export class MethodGenerator {
|
||||
// RTTI needs special handling - return as opaque pointer
|
||||
call = `(spine_rtti) &${call}`;
|
||||
} else if (method.returnType.includes('*')) {
|
||||
const cType = toCTypeName(method.returnType);
|
||||
const cType = toCTypeName(method.returnType, this.validTypes);
|
||||
call = `(${cType}) ${call}`;
|
||||
} else if (method.returnType === 'Color &' || method.returnType === 'const Color &') {
|
||||
call = `(spine_color) &${call}`;
|
||||
} else if (method.returnType.includes('&')) {
|
||||
// For reference returns, take the address
|
||||
const cType = toCTypeName(method.returnType);
|
||||
const cType = toCTypeName(method.returnType, this.validTypes);
|
||||
call = `(${cType}) &${call}`;
|
||||
} else if (!this.isPrimitiveType(method.returnType) && !this.isEnumType(method.returnType)) {
|
||||
// For non-primitive value returns (e.g., BoneLocal), take the address
|
||||
const cType = toCTypeName(method.returnType);
|
||||
const cType = toCTypeName(method.returnType, this.validTypes);
|
||||
call = `(${cType}) &${call}`;
|
||||
} else if (this.isEnumType(method.returnType)) {
|
||||
// Cast enum return values
|
||||
const cType = toCTypeName(method.returnType);
|
||||
const cType = toCTypeName(method.returnType, this.validTypes);
|
||||
call = `(${cType}) ${call}`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return call;
|
||||
}
|
||||
|
||||
private generateSetterCall(objName: string, method: Member, valueName: string): string {
|
||||
|
||||
private generateSetterCall(objName: string, method: Method, valueName: string): string {
|
||||
const param = method.parameters![0];
|
||||
let value = valueName;
|
||||
|
||||
|
||||
// Convert from C type to C++
|
||||
if (param.type === 'const String &' || param.type === 'String') {
|
||||
value = `String(${valueName})`;
|
||||
} else if (param.type.includes('Vector<')) {
|
||||
// Vector types are passed as void* and need to be cast back
|
||||
} else if (param.type.includes('Array<')) {
|
||||
// Array types are passed as void* and need to be cast back
|
||||
value = `(${param.type}) ${valueName}`;
|
||||
} else if (param.type.includes('*')) {
|
||||
value = `(${param.type}) ${valueName}`;
|
||||
@ -293,25 +292,25 @@ export class MethodGenerator {
|
||||
// Cast enum types
|
||||
value = `(${param.type}) ${valueName}`;
|
||||
}
|
||||
|
||||
|
||||
return `${objName}->${method.name}(${value})`;
|
||||
}
|
||||
|
||||
private generateMethodParameters(objTypeName: string, method: Member): { declaration: string; call: string } {
|
||||
|
||||
private generateMethodParameters(objTypeName: string, method: Method): { declaration: string; call: string } {
|
||||
const declParts = [`${objTypeName} obj`];
|
||||
const callParts: string[] = [];
|
||||
|
||||
|
||||
if (method.parameters) {
|
||||
for (const param of method.parameters) {
|
||||
const cType = toCTypeName(param.type);
|
||||
const cType = toCTypeName(param.type, this.validTypes);
|
||||
declParts.push(`${cType} ${param.name}`);
|
||||
|
||||
|
||||
// Convert C type back to C++ for the call
|
||||
let callExpr = param.name;
|
||||
if (param.type === 'const String &' || param.type === 'String') {
|
||||
callExpr = `String(${param.name})`;
|
||||
} else if (param.type.includes('Vector<')) {
|
||||
// Vector types are passed as void* and need to be cast back
|
||||
} else if (param.type.includes('Array<')) {
|
||||
// Array types are passed as void* and need to be cast back
|
||||
callExpr = `(${param.type}) ${param.name}`;
|
||||
} else if (param.type.includes('*')) {
|
||||
callExpr = `(${param.type}) ${param.name}`;
|
||||
@ -319,7 +318,7 @@ export class MethodGenerator {
|
||||
// Handle reference types
|
||||
const isConst = param.type.startsWith('const');
|
||||
const baseType = param.type.replace(/^(?:const\s+)?(.+?)\s*&$/, '$1').trim();
|
||||
|
||||
|
||||
// Non-const references to primitive types are output parameters - just pass the pointer
|
||||
if (!isConst && ['float', 'int', 'double', 'bool'].includes(baseType)) {
|
||||
callExpr = `*${param.name}`;
|
||||
@ -331,17 +330,17 @@ export class MethodGenerator {
|
||||
// Cast enum types
|
||||
callExpr = `(${param.type}) ${param.name}`;
|
||||
}
|
||||
|
||||
|
||||
callParts.push(callExpr);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
declaration: declParts.join(', '),
|
||||
call: callParts.join(', ')
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
private getDefaultReturn(returnType: string): string {
|
||||
if (returnType === 'void') return '';
|
||||
if (returnType === 'bool') return 'false';
|
||||
@ -353,7 +352,7 @@ export class MethodGenerator {
|
||||
}
|
||||
return 'nullptr';
|
||||
}
|
||||
|
||||
|
||||
private isEnumType(type: string): boolean {
|
||||
// List of known enum types in spine-cpp
|
||||
const enumTypes = [
|
||||
@ -364,9 +363,9 @@ export class MethodGenerator {
|
||||
];
|
||||
return enumTypes.includes(type);
|
||||
}
|
||||
|
||||
|
||||
private isPrimitiveType(type: string): boolean {
|
||||
return ['int', 'float', 'double', 'bool', 'size_t', 'int32_t', 'uint32_t',
|
||||
return ['int', 'float', 'double', 'bool', 'size_t', 'int32_t', 'uint32_t',
|
||||
'int16_t', 'uint16_t', 'uint8_t', 'void'].includes(type);
|
||||
}
|
||||
}
|
||||
@ -1,8 +0,0 @@
|
||||
import { Type, toSnakeCase } from '../types';
|
||||
|
||||
export class OpaqueTypeGenerator {
|
||||
generate(type: Type): string {
|
||||
const cName = `spine_${toSnakeCase(type.name)}`;
|
||||
return `SPINE_OPAQUE_TYPE(${cName})`;
|
||||
}
|
||||
}
|
||||
@ -30,20 +30,21 @@ function checkConstNonConstConflicts(classes: Type[], exclusions: Exclusion[]):
|
||||
const conflicts: Array<{ type: string, method: string }> = [];
|
||||
|
||||
for (const type of classes) {
|
||||
if (type.members === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get all non-static methods first
|
||||
const allMethods = type.members?.filter(m =>
|
||||
m.kind === 'method' &&
|
||||
!m.isStatic
|
||||
);
|
||||
const allMethods = type.members?.filter(m => m.kind === 'method').filter(m => !m.isStatic);
|
||||
|
||||
if (allMethods) {
|
||||
const methodGroups = new Map<string, Member[]>();
|
||||
const methodGroups = new Map<string, Array<Member & { kind: 'method' }>>();
|
||||
for (const method of allMethods) {
|
||||
if (method.name === 'getSetupPose') {
|
||||
console.log(`Skipping excluded method: ${type.name}::${method.name}${method.isConst ? ' const' : ''}`);
|
||||
}
|
||||
// Skip if this specific const/non-const version is excluded
|
||||
if (isMethodExcluded(type.name, method.name, exclusions, method)) {
|
||||
if (method.name === 'getSetupPose') {
|
||||
console.log(`Skipping excluded method: ${type.name}::${method.name}${method.isConst ? ' const' : ''}`);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
const key = method.name + '(' + (method.parameters?.map(p => p.type).join(',') || '') + ')';
|
||||
@ -133,9 +134,12 @@ async function main() {
|
||||
// Check for const/non-const conflicts
|
||||
checkConstNonConstConflicts(classes, exclusions);
|
||||
|
||||
// Create a set of valid type names for type checking
|
||||
const validTypes = new Set<string>(includedTypes.map(t => t.name));
|
||||
|
||||
// Initialize generators
|
||||
const constructorGen = new ConstructorGenerator();
|
||||
const methodGen = new MethodGenerator(exclusions);
|
||||
const constructorGen = new ConstructorGenerator(validTypes);
|
||||
const methodGen = new MethodGenerator(exclusions, validTypes);
|
||||
const enumGen = new EnumGenerator();
|
||||
const fileWriter = new FileWriter(path.join(__dirname, '../../src/generated'));
|
||||
|
||||
|
||||
@ -1,11 +1,547 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { execSync } from 'child_process';
|
||||
import { Type, Member, Method, Field, Constructor, Destructor, Parameter, EnumValue, SpineTypes } from './types';
|
||||
|
||||
const SPINE_CPP_PATH = path.join(__dirname, '../../../spine-cpp');
|
||||
const EXTRACTOR_SCRIPT = path.join(SPINE_CPP_PATH, 'extract-spine-cpp-types.js');
|
||||
const SPINE_INCLUDE_DIR = path.join(SPINE_CPP_PATH, 'spine-cpp/include');
|
||||
const OUTPUT_FILE = path.join(__dirname, '../spine-cpp-types.json');
|
||||
const HEADERS_DIR = path.join(SPINE_CPP_PATH, 'spine-cpp/include/spine');
|
||||
|
||||
/**
|
||||
* Extracts the value of an enum constant from source code
|
||||
*/
|
||||
function extractEnumValueFromSource(
|
||||
enumConstNode: any,
|
||||
sourceLines: string[]
|
||||
): string | null | undefined {
|
||||
if (!enumConstNode.loc) return undefined;
|
||||
|
||||
const line = sourceLines[enumConstNode.loc.line - 1];
|
||||
if (!line) return undefined;
|
||||
|
||||
// Find enum name and check for '='
|
||||
const nameMatch = line.match(new RegExp(`\\b${enumConstNode.name}\\b`));
|
||||
if (!nameMatch) return undefined;
|
||||
|
||||
const afterName = line.substring(nameMatch.index! + enumConstNode.name.length);
|
||||
const equalIndex = afterName.indexOf('=');
|
||||
if (equalIndex === -1) return null; // No explicit value
|
||||
|
||||
// Extract value expression
|
||||
let valueText = afterName.substring(equalIndex + 1);
|
||||
|
||||
// Handle multi-line values
|
||||
let currentLine = enumConstNode.loc.line;
|
||||
while (currentLine < sourceLines.length && !valueText.match(/[,}]/)) {
|
||||
valueText += '\n' + sourceLines[currentLine++];
|
||||
}
|
||||
|
||||
// Extract up to comma or brace
|
||||
const endMatch = valueText.match(/^(.*?)([,}])/s);
|
||||
if (endMatch) valueText = endMatch[1];
|
||||
|
||||
// Clean up
|
||||
return valueText
|
||||
.replace(/\/\/.*$/gm, '') // Remove single-line comments
|
||||
.replace(/\/\*[\s\S]*?\*\//g, '') // Remove multi-line comments
|
||||
.replace(/\s+/g, ' ') // Normalize whitespace
|
||||
.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts return type from a method node
|
||||
*/
|
||||
function extractReturnType(methodNode: any): string {
|
||||
const fullType = methodNode.type?.qualType || '';
|
||||
const match = fullType.match(/^(.+?)\s*\(/);
|
||||
return match ? match[1].trim() : 'void';
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts parameters from a method node
|
||||
*/
|
||||
function extractParameters(methodNode: any): Parameter[] {
|
||||
return (methodNode.inner || [])
|
||||
.filter((n: any) => n.kind === 'ParmVarDecl')
|
||||
.map((n: any) => ({
|
||||
name: n.name || '',
|
||||
type: n.type?.qualType || ''
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a node is in the target file
|
||||
*/
|
||||
function isInTargetFile(node: any, targetPath: string): boolean {
|
||||
if (!node.loc) return false;
|
||||
|
||||
const loc = node.loc;
|
||||
|
||||
// Check direct file location
|
||||
if (loc.file) return path.resolve(loc.file) === targetPath;
|
||||
|
||||
// Check macro locations
|
||||
for (const locType of ['spellingLoc', 'expansionLoc']) {
|
||||
if (loc[locType]) {
|
||||
if (loc[locType].includedFrom) return false;
|
||||
if (loc[locType].file) return path.resolve(loc[locType].file) === targetPath;
|
||||
}
|
||||
}
|
||||
|
||||
// If included from another file, reject
|
||||
if (loc.includedFrom) return false;
|
||||
|
||||
// No location info - assume it's in the main file
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts member information from an AST node
|
||||
*/
|
||||
function extractMember(inner: any, parent: any): Member | null {
|
||||
if (inner.isImplicit) return null;
|
||||
|
||||
switch (inner.kind) {
|
||||
case 'FieldDecl':
|
||||
const field: Field = {
|
||||
kind: 'field',
|
||||
name: inner.name || '',
|
||||
type: inner.type?.qualType || '',
|
||||
isStatic: inner.storageClass === 'static'
|
||||
};
|
||||
return field;
|
||||
|
||||
case 'CXXMethodDecl':
|
||||
if (!inner.name) return null;
|
||||
// Skip operators - not needed for C wrapper generation
|
||||
if (inner.name.startsWith('operator')) return null;
|
||||
|
||||
const method: Method = {
|
||||
kind: 'method',
|
||||
name: inner.name,
|
||||
returnType: extractReturnType(inner),
|
||||
parameters: extractParameters(inner),
|
||||
isStatic: inner.storageClass === 'static',
|
||||
isVirtual: inner.virtual || false,
|
||||
isPure: inner.pure || false,
|
||||
isConst: inner.constQualifier || false
|
||||
};
|
||||
return method;
|
||||
|
||||
case 'CXXConstructorDecl':
|
||||
const constructor: Constructor = {
|
||||
kind: 'constructor',
|
||||
name: inner.name || parent.name || '',
|
||||
parameters: extractParameters(inner)
|
||||
};
|
||||
return constructor;
|
||||
|
||||
case 'CXXDestructorDecl':
|
||||
// Include destructors for completeness
|
||||
const destructor: Destructor = {
|
||||
kind: 'destructor',
|
||||
name: inner.name || `~${parent.name}`,
|
||||
isVirtual: inner.virtual || false,
|
||||
isPure: inner.pure || false
|
||||
};
|
||||
return destructor;
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts type information from an AST node
|
||||
*/
|
||||
function extractTypeInfo(node: any, sourceLines: string[]): Type {
|
||||
const info: Type = {
|
||||
name: node.name || '',
|
||||
kind: node.kind === 'EnumDecl' ? 'enum' : (node.tagUsed || 'class') as 'class' | 'struct' | 'enum',
|
||||
loc: {
|
||||
line: node.loc?.line || 0,
|
||||
col: node.loc?.col || 0
|
||||
}
|
||||
};
|
||||
|
||||
// Extract base classes
|
||||
if (node.bases?.length > 0) {
|
||||
info.superTypes = node.bases.map((b: any) => b.type?.qualType || '').filter(Boolean);
|
||||
}
|
||||
|
||||
// For enums, extract the values
|
||||
if (node.kind === 'EnumDecl') {
|
||||
info.values = (node.inner || [])
|
||||
.filter((n: any) => n.kind === 'EnumConstantDecl')
|
||||
.map((n: any) => {
|
||||
const enumValue: EnumValue = { name: n.name || '' };
|
||||
const sourceValue = extractEnumValueFromSource(n, sourceLines);
|
||||
|
||||
if (sourceValue === null) {
|
||||
// Implicit value - no value property
|
||||
} else if (sourceValue) {
|
||||
enumValue.value = sourceValue;
|
||||
} else if (n.inner?.length > 0) {
|
||||
enumValue.value = "<<extraction failed>>";
|
||||
}
|
||||
|
||||
return enumValue;
|
||||
});
|
||||
return info;
|
||||
}
|
||||
|
||||
// For classes/structs, extract public members
|
||||
info.members = [];
|
||||
let currentAccess = node.tagUsed === 'struct' ? 'public' : 'private';
|
||||
let hasPureVirtual = false;
|
||||
|
||||
for (const inner of node.inner || []) {
|
||||
if (inner.kind === 'AccessSpecDecl') {
|
||||
currentAccess = inner.access || 'private';
|
||||
continue;
|
||||
}
|
||||
|
||||
if (inner.kind === 'FriendDecl' || currentAccess !== 'public') continue;
|
||||
|
||||
const member = extractMember(inner, node);
|
||||
if (member) {
|
||||
info.members.push(member);
|
||||
// Check if this is a pure virtual method
|
||||
if (member.kind === 'method' && member.isPure) {
|
||||
hasPureVirtual = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Always set isAbstract to a boolean value
|
||||
info.isAbstract = hasPureVirtual;
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes an AST node to extract types
|
||||
*/
|
||||
function processNode(
|
||||
node: any,
|
||||
types: Type[],
|
||||
targetPath: string,
|
||||
sourceLines: string[],
|
||||
inSpineNamespace: boolean = false
|
||||
): void {
|
||||
if (!node || typeof node !== 'object') return;
|
||||
|
||||
// Handle spine namespace
|
||||
if (node.kind === 'NamespaceDecl' && node.name === 'spine') {
|
||||
(node.inner || []).forEach((n: any) => processNode(n, types, targetPath, sourceLines, true));
|
||||
return;
|
||||
}
|
||||
|
||||
// Recurse to find spine namespace
|
||||
if (!inSpineNamespace) {
|
||||
(node.inner || []).forEach((n: any) => processNode(n, types, targetPath, sourceLines, false));
|
||||
return;
|
||||
}
|
||||
|
||||
// Process type declarations
|
||||
const typeKinds = ['CXXRecordDecl', 'ClassTemplateDecl', 'EnumDecl', 'TypedefDecl', 'TypeAliasDecl'];
|
||||
if (!typeKinds.includes(node.kind)) return;
|
||||
|
||||
// Skip if not in target file or invalid
|
||||
if (!isInTargetFile(node, targetPath) ||
|
||||
node.isImplicit ||
|
||||
!node.name ||
|
||||
node.name.startsWith('_') ||
|
||||
node.name.includes('<')) return;
|
||||
|
||||
// Skip forward declarations
|
||||
if (node.previousDecl && (!node.inner || node.inner.length === 0)) return;
|
||||
|
||||
// Extract type info
|
||||
if (node.kind === 'ClassTemplateDecl') {
|
||||
const classNode = (node.inner || []).find((n: any) => n.kind === 'CXXRecordDecl');
|
||||
if (classNode) {
|
||||
const typeInfo = extractTypeInfo(classNode, sourceLines);
|
||||
typeInfo.isTemplate = true;
|
||||
|
||||
// Extract template parameters
|
||||
const templateParams: string[] = [];
|
||||
for (const inner of node.inner || []) {
|
||||
if (inner.kind === 'TemplateTypeParmDecl' && inner.name) {
|
||||
templateParams.push(inner.name);
|
||||
}
|
||||
}
|
||||
if (templateParams.length > 0) {
|
||||
typeInfo.templateParams = templateParams;
|
||||
}
|
||||
|
||||
types.push(typeInfo);
|
||||
}
|
||||
} else if (node.kind === 'CXXRecordDecl' && node.inner?.length > 0) {
|
||||
const typeInfo = extractTypeInfo(node, sourceLines);
|
||||
// Ensure isTemplate is always set for non-template classes
|
||||
if (typeInfo.isTemplate === undefined) {
|
||||
typeInfo.isTemplate = false;
|
||||
}
|
||||
types.push(typeInfo);
|
||||
} else if (node.kind === 'EnumDecl') {
|
||||
types.push(extractTypeInfo(node, sourceLines));
|
||||
} else if (node.kind === 'TypedefDecl' || node.kind === 'TypeAliasDecl') {
|
||||
types.push(extractTypeInfo(node, sourceLines));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts types from a single header file
|
||||
*/
|
||||
function extractLocalTypes(headerFile: string, typeMap: Map<string, Type> | null = null): Type[] {
|
||||
const absHeaderPath = path.resolve(headerFile);
|
||||
const sourceContent = fs.readFileSync(absHeaderPath, 'utf8');
|
||||
const sourceLines = sourceContent.split('\n');
|
||||
|
||||
// Get AST from clang
|
||||
const cmd = `clang++ -Xclang -ast-dump=json -fsyntax-only -std=c++11 -I "${SPINE_INCLUDE_DIR}" "${absHeaderPath}" 2>/dev/null`;
|
||||
const maxBuffer = headerFile.includes('Debug.h') ? 500 : 200; // MB
|
||||
|
||||
let astJson: any;
|
||||
try {
|
||||
const output = execSync(cmd, { encoding: 'utf8', maxBuffer: maxBuffer * 1024 * 1024 });
|
||||
astJson = JSON.parse(output);
|
||||
} catch (error: any) {
|
||||
throw new Error(error.code === 'ENOBUFS'
|
||||
? `AST output too large (>${maxBuffer}MB)`
|
||||
: error.message);
|
||||
}
|
||||
|
||||
const types: Type[] = [];
|
||||
processNode(astJson, types, absHeaderPath, sourceLines);
|
||||
|
||||
// Filter out forward declarations and SpineObject
|
||||
const filteredTypes = types
|
||||
.filter(t => {
|
||||
// Skip types with no members (forward declarations)
|
||||
if (t.members && t.members.length === 0) return false;
|
||||
|
||||
// Skip SpineObject - it's not needed for C wrapper generation
|
||||
if (t.name === 'SpineObject') return false;
|
||||
|
||||
return true;
|
||||
})
|
||||
.sort((a, b) => (a.loc?.line || 0) - (b.loc?.line || 0));
|
||||
|
||||
// Add inherited methods if we have a type map
|
||||
if (typeMap) {
|
||||
for (const type of filteredTypes) {
|
||||
if (type.superTypes && type.members) {
|
||||
addInheritedMethods(type, typeMap);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return filteredTypes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a method signature for comparison
|
||||
*/
|
||||
function getMethodSignature(method: Method): string {
|
||||
let sig = method.name;
|
||||
if (method.parameters && method.parameters.length > 0) {
|
||||
sig += '(' + method.parameters.map(p => p.type).join(',') + ')';
|
||||
} else {
|
||||
sig += '()';
|
||||
}
|
||||
|
||||
// Add const qualifier if present
|
||||
if (method.isConst) {
|
||||
sig += ' const';
|
||||
}
|
||||
|
||||
return sig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Substitutes template parameters in a type string
|
||||
*/
|
||||
function substituteTemplateParams(typeStr: string, paramMap: Map<string, string>): string {
|
||||
let result = typeStr;
|
||||
|
||||
// Replace template parameters in order of length (longest first)
|
||||
// to avoid replacing substrings (e.g., V before V1)
|
||||
const sortedParams = Array.from(paramMap.keys()).sort((a, b) => b.length - a.length);
|
||||
|
||||
for (const param of sortedParams) {
|
||||
const regex = new RegExp(`\\b${param}\\b`, 'g');
|
||||
result = result.replace(regex, paramMap.get(param)!);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds methods inherited from template supertypes
|
||||
*/
|
||||
function addTemplateInheritedMethods(
|
||||
_type: Type,
|
||||
templateType: Type,
|
||||
templateClassName: string,
|
||||
templateArgs: string,
|
||||
inheritedMethods: Member[],
|
||||
ownMethodSignatures: Set<string>
|
||||
): void {
|
||||
// Parse template arguments (handle multiple args)
|
||||
const argsList: string[] = [];
|
||||
let depth = 0;
|
||||
let currentArg = '';
|
||||
|
||||
for (const char of templateArgs) {
|
||||
if (char === '<') depth++;
|
||||
else if (char === '>') depth--;
|
||||
|
||||
if (char === ',' && depth === 0) {
|
||||
argsList.push(currentArg.trim());
|
||||
currentArg = '';
|
||||
} else {
|
||||
currentArg += char;
|
||||
}
|
||||
}
|
||||
if (currentArg.trim()) {
|
||||
argsList.push(currentArg.trim());
|
||||
}
|
||||
|
||||
// Build a mapping of template params to actual types
|
||||
const paramMap = new Map<string, string>();
|
||||
|
||||
// Use the actual template parameters if we have them
|
||||
if (templateType.templateParams && templateType.templateParams.length === argsList.length) {
|
||||
templateType.templateParams.forEach((param, i) => {
|
||||
paramMap.set(param, argsList[i]);
|
||||
});
|
||||
} else {
|
||||
// Fallback: if we don't have template param info, skip substitution
|
||||
console.error(`Warning: Template ${templateClassName} missing parameter info, skipping substitution`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Process each member of the template
|
||||
for (const member of templateType.members || []) {
|
||||
if (member.kind === 'method') {
|
||||
const method = member as Method;
|
||||
// Skip template constructors - they have weird names like "Pose<P>"
|
||||
if (method.name.includes('<')) continue;
|
||||
|
||||
// Clone the member and substitute template parameters
|
||||
const inheritedMember: Method = JSON.parse(JSON.stringify(method));
|
||||
inheritedMember.fromSupertype = `${templateClassName}<${templateArgs}>`;
|
||||
|
||||
// Replace template parameters in return type
|
||||
if (inheritedMember.returnType) {
|
||||
inheritedMember.returnType = substituteTemplateParams(
|
||||
inheritedMember.returnType, paramMap
|
||||
);
|
||||
}
|
||||
|
||||
// Replace template parameters in parameters
|
||||
if (inheritedMember.parameters) {
|
||||
for (const param of inheritedMember.parameters) {
|
||||
param.type = substituteTemplateParams(param.type, paramMap);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if this method is overridden
|
||||
const sig = getMethodSignature(inheritedMember);
|
||||
if (!ownMethodSignatures.has(sig)) {
|
||||
inheritedMethods.push(inheritedMember);
|
||||
ownMethodSignatures.add(sig);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds inherited methods to a type
|
||||
*/
|
||||
function addInheritedMethods(type: Type, typeMap: Map<string, Type>): void {
|
||||
const inheritedMethods: Member[] = [];
|
||||
const ownMethodSignatures = new Set<string>();
|
||||
|
||||
// Build a set of method signatures from this type
|
||||
for (const member of type.members || []) {
|
||||
if (member.kind === 'method') {
|
||||
const sig = getMethodSignature(member as Method);
|
||||
ownMethodSignatures.add(sig);
|
||||
}
|
||||
}
|
||||
|
||||
// Process each supertype
|
||||
for (const superTypeName of type.superTypes || []) {
|
||||
// Clean up the supertype name (remove namespaces, etc)
|
||||
const cleanName = superTypeName.replace(/^.*::/, '');
|
||||
|
||||
// Skip SpineObject inheritance - it's just noise
|
||||
if (cleanName === 'SpineObject') continue;
|
||||
|
||||
// Check if this is a template supertype
|
||||
const templateMatch = cleanName.match(/^([^<]+)<(.+)>$/);
|
||||
if (templateMatch) {
|
||||
const templateClassName = templateMatch[1];
|
||||
const templateArgs = templateMatch[2];
|
||||
|
||||
const templateType = typeMap.get(templateClassName);
|
||||
if (templateType && templateType.members) {
|
||||
// Process template inheritance
|
||||
addTemplateInheritedMethods(
|
||||
type, templateType, templateClassName, templateArgs,
|
||||
inheritedMethods, ownMethodSignatures
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Non-template supertype
|
||||
const superType = typeMap.get(cleanName);
|
||||
|
||||
if (!superType || !superType.members) continue;
|
||||
|
||||
// Add non-overridden methods from supertype
|
||||
for (const member of superType.members) {
|
||||
if (member.kind === 'method') {
|
||||
const method = member as Method;
|
||||
const sig = getMethodSignature(method);
|
||||
if (!ownMethodSignatures.has(sig)) {
|
||||
const inheritedMember = { ...method, fromSupertype: cleanName };
|
||||
inheritedMethods.push(inheritedMember);
|
||||
ownMethodSignatures.add(sig); // Prevent duplicates from multiple inheritance
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add inherited methods to the type
|
||||
if (type.members) {
|
||||
type.members.push(...inheritedMethods);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds all header files in the spine include directory
|
||||
*/
|
||||
function findAllHeaderFiles(): string[] {
|
||||
const headers: string[] = [];
|
||||
|
||||
function walkDir(dir: string): void {
|
||||
fs.readdirSync(dir).forEach(file => {
|
||||
const fullPath = path.join(dir, file);
|
||||
if (fs.statSync(fullPath).isDirectory()) {
|
||||
walkDir(fullPath);
|
||||
} else if (file.endsWith('.h') && file !== 'spine.h') {
|
||||
headers.push(fullPath);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
walkDir(SPINE_INCLUDE_DIR);
|
||||
return headers.sort();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if type extraction is needed based on file timestamps
|
||||
@ -16,20 +552,21 @@ function isExtractionNeeded(): boolean {
|
||||
console.log('spine-cpp-types.json not found, extraction needed');
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// Get output file timestamp
|
||||
const outputStats = fs.statSync(OUTPUT_FILE);
|
||||
const outputTime = outputStats.mtime.getTime();
|
||||
|
||||
// Check all header files
|
||||
const headerFiles = fs.readdirSync(HEADERS_DIR)
|
||||
.filter(f => f.endsWith('.h'))
|
||||
.map(f => path.join(HEADERS_DIR, f));
|
||||
|
||||
|
||||
// Check all header files in the spine subdirectory
|
||||
const spineDir = path.join(SPINE_INCLUDE_DIR, 'spine');
|
||||
const headerFiles = fs.readdirSync(spineDir, { recursive: true })
|
||||
.filter((f: string | Buffer<ArrayBufferLike>) => f.toString().endsWith('.h'))
|
||||
.map((f: string | Buffer<ArrayBufferLike>) => path.join(spineDir, f.toString()));
|
||||
|
||||
// Find newest header modification time
|
||||
let newestHeaderTime = 0;
|
||||
let newestHeader = '';
|
||||
|
||||
|
||||
for (const headerFile of headerFiles) {
|
||||
const stats = fs.statSync(headerFile);
|
||||
if (stats.mtime.getTime() > newestHeaderTime) {
|
||||
@ -37,38 +574,88 @@ function isExtractionNeeded(): boolean {
|
||||
newestHeader = path.basename(headerFile);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// If any header is newer than output, we need to extract
|
||||
if (newestHeaderTime > outputTime) {
|
||||
console.log(`Header ${newestHeader} is newer than spine-cpp-types.json, extraction needed`);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
console.log('spine-cpp-types.json is up to date');
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the extract-spine-cpp-types.js script to generate type information
|
||||
* Runs the type extraction process and generates the output file
|
||||
*/
|
||||
export function extractTypes(): void {
|
||||
if (!isExtractionNeeded()) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
console.log('Running type extraction...');
|
||||
|
||||
|
||||
try {
|
||||
// Run the extractor script
|
||||
const output = execSync(`node "${EXTRACTOR_SCRIPT}"`, {
|
||||
cwd: SPINE_CPP_PATH,
|
||||
encoding: 'utf8',
|
||||
maxBuffer: 10 * 1024 * 1024 // 10MB buffer for large output
|
||||
});
|
||||
|
||||
const allHeaders = findAllHeaderFiles();
|
||||
const allTypes: SpineTypes = {};
|
||||
let processed = 0, errors = 0;
|
||||
|
||||
console.error(`Processing ${allHeaders.length} header files...`);
|
||||
|
||||
// First pass: extract all types without inheritance
|
||||
const typeMap = new Map<string, Type>();
|
||||
|
||||
for (const headerFile of allHeaders) {
|
||||
const relPath = path.relative(SPINE_INCLUDE_DIR, headerFile);
|
||||
process.stderr.write(`\r\x1b[K Pass 1 - Processing ${++processed}/${allHeaders.length}: ${relPath}...`);
|
||||
|
||||
try {
|
||||
const types = extractLocalTypes(headerFile);
|
||||
if (types.length > 0) {
|
||||
allTypes[relPath] = types;
|
||||
// Build type map
|
||||
for (const type of types) {
|
||||
typeMap.set(type.name, type);
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
errors++;
|
||||
console.error(`\n ERROR processing ${relPath}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Second pass: add inherited methods
|
||||
console.error(`\n Pass 2 - Adding inherited methods...`);
|
||||
processed = 0;
|
||||
|
||||
for (const headerFile of allHeaders) {
|
||||
const relPath = path.relative(SPINE_INCLUDE_DIR, headerFile);
|
||||
if (!allTypes[relPath]) continue;
|
||||
|
||||
process.stderr.write(`\r\x1b[K Pass 2 - Processing ${++processed}/${Object.keys(allTypes).length}: ${relPath}...`);
|
||||
|
||||
for (const type of allTypes[relPath]) {
|
||||
if (type.superTypes && type.members) {
|
||||
addInheritedMethods(type, typeMap);
|
||||
|
||||
// Check if any inherited methods are pure virtual
|
||||
// If so, and the class doesn't override them, it's abstract
|
||||
if (!type.isAbstract) {
|
||||
const hasPureVirtual = type.members.some(m =>
|
||||
m.kind === 'method' && m.isPure === true
|
||||
);
|
||||
if (hasPureVirtual) {
|
||||
type.isAbstract = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.error(`\n Completed: ${Object.keys(allTypes).length} files processed, ${errors} errors`);
|
||||
|
||||
// Write to output file
|
||||
fs.writeFileSync(OUTPUT_FILE, output);
|
||||
|
||||
fs.writeFileSync(OUTPUT_FILE, JSON.stringify(allTypes, null, 2));
|
||||
console.log(`Type extraction complete, wrote ${OUTPUT_FILE}`);
|
||||
} catch (error: any) {
|
||||
console.error('Failed to extract types:', error.message);
|
||||
@ -79,10 +666,10 @@ export function extractTypes(): void {
|
||||
/**
|
||||
* Loads the extracted type information
|
||||
*/
|
||||
export function loadTypes(): any {
|
||||
export function loadTypes(): SpineTypes {
|
||||
if (!fs.existsSync(OUTPUT_FILE)) {
|
||||
throw new Error(`Type information not found at ${OUTPUT_FILE}. Run extraction first.`);
|
||||
}
|
||||
|
||||
|
||||
return JSON.parse(fs.readFileSync(OUTPUT_FILE, 'utf8'));
|
||||
}
|
||||
@ -1,22 +1,49 @@
|
||||
export interface Parameter {
|
||||
name: string;
|
||||
type: string;
|
||||
defaultValue?: string;
|
||||
}
|
||||
|
||||
export interface Member {
|
||||
kind: 'field' | 'method' | 'constructor' | 'destructor';
|
||||
export type Field = {
|
||||
kind: 'field';
|
||||
name: string;
|
||||
type?: string; // For fields
|
||||
returnType?: string; // For methods
|
||||
type: string;
|
||||
isStatic?: boolean;
|
||||
fromSupertype?: string;
|
||||
}
|
||||
|
||||
export type Method = {
|
||||
kind: 'method';
|
||||
name: string;
|
||||
returnType: string;
|
||||
parameters?: Parameter[];
|
||||
isStatic?: boolean;
|
||||
isVirtual?: boolean;
|
||||
isPure?: boolean;
|
||||
isConst?: boolean;
|
||||
fromSupertype?: string; // Indicates this member was inherited
|
||||
fromSupertype?: string;
|
||||
}
|
||||
|
||||
export type Constructor = {
|
||||
kind: 'constructor';
|
||||
name: string;
|
||||
parameters?: Parameter[];
|
||||
fromSupertype?: string;
|
||||
}
|
||||
|
||||
export type Destructor = {
|
||||
kind: 'destructor';
|
||||
name: string;
|
||||
isVirtual?: boolean;
|
||||
isPure?: boolean;
|
||||
fromSupertype?: string;
|
||||
};
|
||||
|
||||
export type Member =
|
||||
| Field
|
||||
| Method
|
||||
| Constructor
|
||||
| Destructor
|
||||
|
||||
export interface EnumValue {
|
||||
name: string;
|
||||
value?: string;
|
||||
@ -41,7 +68,7 @@ export interface SpineTypes {
|
||||
[header: string]: Type[];
|
||||
}
|
||||
|
||||
export type Exclusion =
|
||||
export type Exclusion =
|
||||
| {
|
||||
kind: 'type';
|
||||
typeName: string;
|
||||
@ -53,6 +80,17 @@ export type Exclusion =
|
||||
isConst?: boolean; // Whether the method is const (e.g., void foo() const), NOT whether return type is const
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts a PascalCase or camelCase name to snake_case.
|
||||
*
|
||||
* @param name The name to convert
|
||||
* @returns The snake_case version
|
||||
*
|
||||
* Examples:
|
||||
* - "AnimationState" → "animation_state"
|
||||
* - "getRTTI" → "get_rtti"
|
||||
* - "IKConstraint" → "ik_constraint"
|
||||
*/
|
||||
export function toSnakeCase(name: string): string {
|
||||
// Handle acronyms and consecutive capitals
|
||||
let result = name.replace(/([A-Z]+)([A-Z][a-z])/g, '$1_$2');
|
||||
@ -61,140 +99,135 @@ export function toSnakeCase(name: string): string {
|
||||
return result.toLowerCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a C function name from a type and method name.
|
||||
*
|
||||
* @param typeName The C++ class name
|
||||
* @param methodName The method name
|
||||
* @returns The C function name
|
||||
*
|
||||
* Examples:
|
||||
* - ("Skeleton", "updateCache") → "spine_skeleton_update_cache"
|
||||
* - ("AnimationState", "apply") → "spine_animation_state_apply"
|
||||
*/
|
||||
export function toCFunctionName(typeName: string, methodName: string): string {
|
||||
return `spine_${toSnakeCase(typeName)}_${toSnakeCase(methodName)}`;
|
||||
}
|
||||
|
||||
export function toCTypeName(cppType: string): string {
|
||||
// Remove any spine:: namespace prefix first
|
||||
cppType = cppType.replace(/^spine::/, '');
|
||||
|
||||
// Category 1: Primitives (including void)
|
||||
const primitiveMap: { [key: string]: string } = {
|
||||
'void': 'void',
|
||||
'bool': 'bool',
|
||||
'char': 'char',
|
||||
'int': 'int32_t',
|
||||
'int32_t': 'int32_t',
|
||||
'unsigned int': 'uint32_t',
|
||||
'uint32_t': 'uint32_t',
|
||||
'short': 'int16_t',
|
||||
'int16_t': 'int16_t',
|
||||
'unsigned short': 'uint16_t',
|
||||
'uint16_t': 'uint16_t',
|
||||
'long long': 'int64_t',
|
||||
'int64_t': 'int64_t',
|
||||
'unsigned long long': 'uint64_t',
|
||||
'uint64_t': 'uint64_t',
|
||||
'float': 'float',
|
||||
'double': 'double',
|
||||
'size_t': 'size_t',
|
||||
'uint8_t': 'uint8_t'
|
||||
};
|
||||
|
||||
if (primitiveMap[cppType]) {
|
||||
return primitiveMap[cppType];
|
||||
/**
|
||||
* Checks if a type is a primitive by tokenizing and checking if ALL tokens start with lowercase.
|
||||
* Examples:
|
||||
* - "int" → true
|
||||
* - "const char*" → true (all tokens: "const", "char*" start lowercase)
|
||||
* - "unsigned int" → true (all tokens start lowercase)
|
||||
* - "Array<float>" → false (starts uppercase)
|
||||
* - "const Array<float>&" → false ("Array" starts uppercase)
|
||||
*/
|
||||
function isPrimitive(cppType: string): boolean {
|
||||
const tokens = cppType.split(/\s+/);
|
||||
return tokens.every(token => {
|
||||
// Remove any trailing punctuation like *, &
|
||||
const cleanToken = token.replace(/[*&]+$/, '');
|
||||
return cleanToken.length > 0 && /^[a-z]/.test(cleanToken);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a C++ type to its corresponding C type.
|
||||
*
|
||||
* @param cppType The C++ type to convert
|
||||
* @param validTypes Set of valid type names (classes and enums) from filtered types
|
||||
* @returns The C type
|
||||
* @throws Error if the type is not recognized
|
||||
*
|
||||
* Examples:
|
||||
* - Primitives: "int" → "int", "const float*" → "const float*"
|
||||
* - String types: "String" → "const char*", "const String&" → "const char*"
|
||||
* - Arrays: "Array<float>" → "spine_array_float"
|
||||
* - Class pointers: "Bone*" → "spine_bone"
|
||||
* - Class references: "const Color&" → "spine_color"
|
||||
* - Non-const primitive refs: "float&" → "float*" (output parameter)
|
||||
*/
|
||||
export function toCTypeName(cppType: string, validTypes: Set<string>): string {
|
||||
// Remove extra spaces and normalize
|
||||
const normalizedType = cppType.replace(/\s+/g, ' ').trim();
|
||||
|
||||
// Primitives - pass through unchanged
|
||||
if (isPrimitive(normalizedType)) {
|
||||
return normalizedType;
|
||||
}
|
||||
|
||||
// Category 2: Special types
|
||||
if (cppType === 'String' || cppType === 'const String' || cppType === 'const char *') {
|
||||
return 'const utf8 *';
|
||||
|
||||
// Special type: String
|
||||
if (normalizedType === 'String' || normalizedType === 'const String' ||
|
||||
normalizedType === 'String&' || normalizedType === 'const String&') {
|
||||
return 'const char*';
|
||||
}
|
||||
if (cppType === 'void *') {
|
||||
return 'spine_void';
|
||||
|
||||
// PropertyId is a typedef
|
||||
if (normalizedType === 'PropertyId') {
|
||||
return 'int64_t';
|
||||
}
|
||||
if (cppType === 'DisposeRendererObject') {
|
||||
return 'spine_dispose_renderer_object';
|
||||
}
|
||||
if (cppType === 'TextureLoader' || cppType === 'TextureLoader *') {
|
||||
return 'spine_texture_loader';
|
||||
}
|
||||
if (cppType === 'PropertyId') {
|
||||
return 'int64_t'; // PropertyId is typedef'd to long long
|
||||
}
|
||||
|
||||
// Category 3: Arrays - must check before pointers/references
|
||||
const arrayMatch = cppType.match(/^(?:const\s+)?Array<(.+?)>\s*(?:&|\*)?$/);
|
||||
|
||||
// Arrays - must check before pointers/references
|
||||
const arrayMatch = normalizedType.match(/^(?:const\s+)?Array<(.+?)>\s*(?:&|\*)?$/);
|
||||
if (arrayMatch) {
|
||||
const elementType = arrayMatch[1].trim();
|
||||
|
||||
// Map element types to C array type suffixes
|
||||
let typeSuffix: string;
|
||||
|
||||
// Handle primitives
|
||||
if (elementType === 'float') typeSuffix = 'float';
|
||||
else if (elementType === 'int') typeSuffix = 'int32';
|
||||
else if (elementType === 'unsigned short') typeSuffix = 'uint16';
|
||||
else if (elementType === 'bool') typeSuffix = 'bool';
|
||||
else if (elementType === 'char') typeSuffix = 'char';
|
||||
else if (elementType === 'size_t') typeSuffix = 'size';
|
||||
else if (elementType === 'PropertyId') typeSuffix = 'property_id';
|
||||
// Handle pointer types - remove * and convert
|
||||
else if (elementType.endsWith('*')) {
|
||||
const cleanType = elementType.slice(0, -1).trim();
|
||||
typeSuffix = toSnakeCase(cleanType);
|
||||
|
||||
// For primitive element types, use the type name with spaces replaced by underscores
|
||||
if (isPrimitive(elementType)) {
|
||||
return `spine_array_${elementType.replace(/\s+/g, '_')}`;
|
||||
}
|
||||
// Handle everything else (enums, classes)
|
||||
else {
|
||||
typeSuffix = toSnakeCase(elementType);
|
||||
|
||||
// For pointer types, remove the * and convert
|
||||
if (elementType.endsWith('*')) {
|
||||
const baseType = elementType.slice(0, -1).trim();
|
||||
return `spine_array_${toSnakeCase(baseType)}`;
|
||||
}
|
||||
|
||||
return `spine_array_${typeSuffix}`;
|
||||
|
||||
// For class/enum types
|
||||
return `spine_array_${toSnakeCase(elementType)}`;
|
||||
}
|
||||
|
||||
// Category 4: Pointers
|
||||
const pointerMatch = cppType.match(/^(.+?)\s*\*$/);
|
||||
|
||||
// Pointers
|
||||
const pointerMatch = normalizedType.match(/^(.+?)\s*\*$/);
|
||||
if (pointerMatch) {
|
||||
const baseType = pointerMatch[1].trim();
|
||||
|
||||
|
||||
// Primitive pointers stay as-is
|
||||
if (primitiveMap[baseType]) {
|
||||
const mappedType = primitiveMap[baseType];
|
||||
// For numeric types, use the mapped type
|
||||
return mappedType === 'void' ? 'void *' : `${mappedType} *`;
|
||||
if (isPrimitive(baseType)) {
|
||||
return normalizedType;
|
||||
}
|
||||
|
||||
// char* becomes utf8*
|
||||
if (baseType === 'char' || baseType === 'const char') {
|
||||
return 'utf8 *';
|
||||
}
|
||||
|
||||
// Class pointers
|
||||
|
||||
// Class pointers become opaque types
|
||||
return `spine_${toSnakeCase(baseType)}`;
|
||||
}
|
||||
|
||||
// Category 5: References
|
||||
const refMatch = cppType.match(/^(?:const\s+)?(.+?)\s*&$/);
|
||||
|
||||
// References
|
||||
const refMatch = normalizedType.match(/^((?:const\s+)?(.+?))\s*&$/);
|
||||
if (refMatch) {
|
||||
const baseType = refMatch[1].trim();
|
||||
const isConst = cppType.includes('const ');
|
||||
|
||||
// Special cases
|
||||
if (baseType === 'String') return 'const utf8 *';
|
||||
if (baseType === 'RTTI') return 'spine_rtti';
|
||||
|
||||
const fullBaseType = refMatch[1].trim();
|
||||
const baseType = refMatch[2].trim();
|
||||
const isConst = fullBaseType.startsWith('const ');
|
||||
|
||||
// Non-const references to primitives become pointers (output parameters)
|
||||
if (!isConst && primitiveMap[baseType]) {
|
||||
const mappedType = primitiveMap[baseType];
|
||||
return mappedType === 'void' ? 'void *' : `${mappedType} *`;
|
||||
if (!isConst && isPrimitive(baseType)) {
|
||||
return `${baseType}*`;
|
||||
}
|
||||
|
||||
|
||||
// Const references and class references - recurse without the reference
|
||||
return toCTypeName(baseType);
|
||||
return toCTypeName(baseType, validTypes);
|
||||
}
|
||||
|
||||
// Category 6: Known enums
|
||||
const knownEnums = [
|
||||
'MixBlend', 'MixDirection', 'BlendMode', 'AttachmentType', 'EventType',
|
||||
'Format', 'TextureFilter', 'TextureWrap', 'Inherit', 'Physics',
|
||||
'PositionMode', 'Property', 'RotateMode', 'SequenceMode', 'SpacingMode'
|
||||
];
|
||||
|
||||
if (knownEnums.includes(cppType)) {
|
||||
return `spine_${toSnakeCase(cppType)}`;
|
||||
|
||||
// Function pointers - for now, just error
|
||||
if (normalizedType.includes('(') && normalizedType.includes(')')) {
|
||||
throw new Error(`Function pointer types not yet supported: ${normalizedType}`);
|
||||
}
|
||||
|
||||
// Category 7: Classes (default case)
|
||||
// Assume any remaining type is a spine class
|
||||
return `spine_${toSnakeCase(cppType)}`;
|
||||
|
||||
// Everything else should be a class or enum type
|
||||
// Check if it's a valid type
|
||||
if (!validTypes.has(normalizedType)) {
|
||||
throw new Error(`Unknown type: ${normalizedType}. Not a primitive and not in the list of valid types.`);
|
||||
}
|
||||
|
||||
return `spine_${toSnakeCase(normalizedType)}`;
|
||||
}
|
||||
@ -1,529 +0,0 @@
|
||||
/******************************************************************************
|
||||
* Spine Runtimes License Agreement
|
||||
* Last updated April 5, 2025. Replaces all prior versions.
|
||||
*
|
||||
* Copyright (c) 2013-2025, Esoteric Software LLC
|
||||
*
|
||||
* Integration of the Spine Runtimes into software or otherwise creating
|
||||
* derivative works of the Spine Runtimes is permitted under the terms and
|
||||
* conditions of Section 2 of the Spine Editor License Agreement:
|
||||
* http://esotericsoftware.com/spine-editor-license
|
||||
*
|
||||
* Otherwise, it is permitted to integrate the Spine Runtimes into software
|
||||
* or otherwise create derivative works of the Spine Runtimes (collectively,
|
||||
* "Products"), provided that each user of the Products must obtain their own
|
||||
* Spine Editor license and redistribution of the Products in any form must
|
||||
* include this license and copyright notice.
|
||||
*
|
||||
* THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
|
||||
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
|
||||
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
|
||||
* BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
|
||||
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
|
||||
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*****************************************************************************/
|
||||
|
||||
#include "custom.h"
|
||||
#include <spine/spine.h>
|
||||
#include <spine/Version.h>
|
||||
#include <spine/Debug.h>
|
||||
#include <spine/SkeletonRenderer.h>
|
||||
#include <spine/AnimationState.h>
|
||||
#include <cstring>
|
||||
|
||||
using namespace spine;
|
||||
|
||||
// Internal structures
|
||||
struct _spine_atlas {
|
||||
void *atlas;
|
||||
utf8 **imagePaths;
|
||||
int32_t numImagePaths;
|
||||
utf8 *error;
|
||||
};
|
||||
|
||||
struct _spine_skeleton_data_result {
|
||||
spine_skeleton_data skeletonData;
|
||||
utf8 *error;
|
||||
};
|
||||
|
||||
struct _spine_bounds {
|
||||
float x, y, width, height;
|
||||
};
|
||||
|
||||
struct _spine_vector {
|
||||
float x, y;
|
||||
};
|
||||
|
||||
struct _spine_skeleton_drawable : public SpineObject {
|
||||
spine_skeleton skeleton;
|
||||
spine_animation_state animationState;
|
||||
spine_animation_state_data animationStateData;
|
||||
spine_animation_state_events animationStateEvents;
|
||||
SkeletonRenderer *renderer;
|
||||
};
|
||||
|
||||
struct _spine_skin_entry {
|
||||
int32_t slotIndex;
|
||||
utf8 *name;
|
||||
spine_attachment attachment;
|
||||
};
|
||||
|
||||
struct _spine_skin_entries {
|
||||
int32_t numEntries;
|
||||
_spine_skin_entry *entries;
|
||||
};
|
||||
|
||||
// Animation state event tracking
|
||||
struct AnimationStateEvent {
|
||||
EventType type;
|
||||
TrackEntry *entry;
|
||||
Event *event;
|
||||
AnimationStateEvent(EventType type, TrackEntry *entry, Event *event) : type(type), entry(entry), event(event){};
|
||||
};
|
||||
|
||||
class EventListener : public AnimationStateListenerObject, public SpineObject {
|
||||
public:
|
||||
Vector<AnimationStateEvent> events;
|
||||
|
||||
void callback(AnimationState *state, EventType type, TrackEntry *entry, Event *event) override {
|
||||
events.add(AnimationStateEvent(type, entry, event));
|
||||
SP_UNUSED(state);
|
||||
}
|
||||
};
|
||||
|
||||
// Static variables
|
||||
static Color NULL_COLOR(0, 0, 0, 0);
|
||||
static SpineExtension *defaultExtension = nullptr;
|
||||
static DebugExtension *debugExtension = nullptr;
|
||||
|
||||
static void initExtensions() {
|
||||
if (defaultExtension == nullptr) {
|
||||
defaultExtension = new DefaultSpineExtension();
|
||||
debugExtension = new DebugExtension(defaultExtension);
|
||||
}
|
||||
}
|
||||
|
||||
namespace spine {
|
||||
SpineExtension *getDefaultExtension() {
|
||||
initExtensions();
|
||||
return defaultExtension;
|
||||
}
|
||||
}
|
||||
|
||||
// Version functions
|
||||
int32_t spine_major_version() {
|
||||
return SPINE_MAJOR_VERSION;
|
||||
}
|
||||
|
||||
int32_t spine_minor_version() {
|
||||
return SPINE_MINOR_VERSION;
|
||||
}
|
||||
|
||||
void spine_enable_debug_extension(spine_bool enable) {
|
||||
initExtensions();
|
||||
SpineExtension::setInstance(enable ? debugExtension : defaultExtension);
|
||||
}
|
||||
|
||||
void spine_report_leaks() {
|
||||
initExtensions();
|
||||
debugExtension->reportLeaks();
|
||||
fflush(stdout);
|
||||
}
|
||||
|
||||
// Color functions
|
||||
float spine_color_get_r(spine_color color) {
|
||||
if (!color) return 0;
|
||||
return ((Color *) color)->r;
|
||||
}
|
||||
|
||||
float spine_color_get_g(spine_color color) {
|
||||
if (!color) return 0;
|
||||
return ((Color *) color)->g;
|
||||
}
|
||||
|
||||
float spine_color_get_b(spine_color color) {
|
||||
if (!color) return 0;
|
||||
return ((Color *) color)->b;
|
||||
}
|
||||
|
||||
float spine_color_get_a(spine_color color) {
|
||||
if (!color) return 0;
|
||||
return ((Color *) color)->a;
|
||||
}
|
||||
|
||||
// Bounds functions
|
||||
float spine_bounds_get_x(spine_bounds bounds) {
|
||||
if (!bounds) return 0;
|
||||
return ((_spine_bounds *) bounds)->x;
|
||||
}
|
||||
|
||||
float spine_bounds_get_y(spine_bounds bounds) {
|
||||
if (!bounds) return 0;
|
||||
return ((_spine_bounds *) bounds)->y;
|
||||
}
|
||||
|
||||
float spine_bounds_get_width(spine_bounds bounds) {
|
||||
if (!bounds) return 0;
|
||||
return ((_spine_bounds *) bounds)->width;
|
||||
}
|
||||
|
||||
float spine_bounds_get_height(spine_bounds bounds) {
|
||||
if (!bounds) return 0;
|
||||
return ((_spine_bounds *) bounds)->height;
|
||||
}
|
||||
|
||||
// Vector functions
|
||||
float spine_vector_get_x(spine_vector vector) {
|
||||
if (!vector) return 0;
|
||||
return ((_spine_vector *) vector)->x;
|
||||
}
|
||||
|
||||
float spine_vector_get_y(spine_vector vector) {
|
||||
if (!vector) return 0;
|
||||
return ((_spine_vector *) vector)->y;
|
||||
}
|
||||
|
||||
// Atlas functions
|
||||
class LiteTextureLoad : public TextureLoader {
|
||||
void load(AtlasPage &page, const String &path) {
|
||||
page.texture = (void *) (intptr_t) page.index;
|
||||
}
|
||||
|
||||
void unload(void *texture) {
|
||||
}
|
||||
};
|
||||
static LiteTextureLoad liteLoader;
|
||||
|
||||
spine_atlas spine_atlas_load(const utf8 *atlasData) {
|
||||
if (!atlasData) return nullptr;
|
||||
int32_t length = (int32_t) strlen(atlasData);
|
||||
auto atlas = new (__FILE__, __LINE__) Atlas(atlasData, length, "", &liteLoader, true);
|
||||
_spine_atlas *result = SpineExtension::calloc<_spine_atlas>(1, __FILE__, __LINE__);
|
||||
result->atlas = atlas;
|
||||
result->numImagePaths = (int32_t) atlas->getPages().size();
|
||||
result->imagePaths = SpineExtension::calloc<utf8 *>(result->numImagePaths, __FILE__, __LINE__);
|
||||
for (int i = 0; i < result->numImagePaths; i++) {
|
||||
result->imagePaths[i] = (utf8 *) strdup(atlas->getPages()[i]->texturePath.buffer());
|
||||
}
|
||||
return (spine_atlas) result;
|
||||
}
|
||||
|
||||
class CallbackTextureLoad : public TextureLoader {
|
||||
spine_texture_loader_load_func loadCb;
|
||||
spine_texture_loader_unload_func unloadCb;
|
||||
|
||||
public:
|
||||
CallbackTextureLoad() : loadCb(nullptr), unloadCb(nullptr) {}
|
||||
|
||||
void setCallbacks(spine_texture_loader_load_func load, spine_texture_loader_unload_func unload) {
|
||||
loadCb = load;
|
||||
unloadCb = unload;
|
||||
}
|
||||
|
||||
void load(AtlasPage &page, const String &path) {
|
||||
page.texture = this->loadCb(path.buffer());
|
||||
}
|
||||
|
||||
void unload(void *texture) {
|
||||
this->unloadCb(texture);
|
||||
}
|
||||
};
|
||||
static CallbackTextureLoad callbackLoader;
|
||||
|
||||
spine_atlas spine_atlas_load_callback(const utf8 *atlasData, const utf8 *atlasDir,
|
||||
spine_texture_loader_load_func load,
|
||||
spine_texture_loader_unload_func unload) {
|
||||
if (!atlasData) return nullptr;
|
||||
int32_t length = (int32_t) strlen(atlasData);
|
||||
callbackLoader.setCallbacks(load, unload);
|
||||
auto atlas = new (__FILE__, __LINE__) Atlas(atlasData, length, (const char *) atlasDir, &callbackLoader, true);
|
||||
_spine_atlas *result = SpineExtension::calloc<_spine_atlas>(1, __FILE__, __LINE__);
|
||||
result->atlas = atlas;
|
||||
result->numImagePaths = (int32_t) atlas->getPages().size();
|
||||
result->imagePaths = SpineExtension::calloc<utf8 *>(result->numImagePaths, __FILE__, __LINE__);
|
||||
for (int i = 0; i < result->numImagePaths; i++) {
|
||||
result->imagePaths[i] = (utf8 *) strdup(atlas->getPages()[i]->texturePath.buffer());
|
||||
}
|
||||
return (spine_atlas) result;
|
||||
}
|
||||
|
||||
int32_t spine_atlas_get_num_image_paths(spine_atlas atlas) {
|
||||
if (!atlas) return 0;
|
||||
return ((_spine_atlas *) atlas)->numImagePaths;
|
||||
}
|
||||
|
||||
utf8 *spine_atlas_get_image_path(spine_atlas atlas, int32_t index) {
|
||||
if (!atlas) return nullptr;
|
||||
_spine_atlas *_atlas = (_spine_atlas *) atlas;
|
||||
if (index < 0 || index >= _atlas->numImagePaths) return nullptr;
|
||||
return _atlas->imagePaths[index];
|
||||
}
|
||||
|
||||
spine_bool spine_atlas_is_pma(spine_atlas atlas) {
|
||||
if (!atlas) return 0;
|
||||
Atlas *_atlas = (Atlas *) ((_spine_atlas *) atlas)->atlas;
|
||||
for (size_t i = 0; i < _atlas->getPages().size(); i++) {
|
||||
AtlasPage *page = _atlas->getPages()[i];
|
||||
if (page->pma) return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
utf8 *spine_atlas_get_error(spine_atlas atlas) {
|
||||
if (!atlas) return nullptr;
|
||||
return ((_spine_atlas *) atlas)->error;
|
||||
}
|
||||
|
||||
void spine_atlas_dispose(spine_atlas atlas) {
|
||||
if (!atlas) return;
|
||||
_spine_atlas *_atlas = (_spine_atlas *) atlas;
|
||||
if (_atlas->atlas) {
|
||||
delete (Atlas *) _atlas->atlas;
|
||||
}
|
||||
if (_atlas->imagePaths) {
|
||||
for (int i = 0; i < _atlas->numImagePaths; i++) {
|
||||
if (_atlas->imagePaths[i]) {
|
||||
SpineExtension::free(_atlas->imagePaths[i], __FILE__, __LINE__);
|
||||
}
|
||||
}
|
||||
SpineExtension::free(_atlas->imagePaths, __FILE__, __LINE__);
|
||||
}
|
||||
if (_atlas->error) {
|
||||
SpineExtension::free(_atlas->error, __FILE__, __LINE__);
|
||||
}
|
||||
SpineExtension::free(_atlas, __FILE__, __LINE__);
|
||||
}
|
||||
|
||||
// Skeleton data loading
|
||||
spine_skeleton_data_result spine_skeleton_data_load_json(spine_atlas atlas, const utf8 *skeletonData) {
|
||||
if (!atlas || !skeletonData) return nullptr;
|
||||
_spine_skeleton_data_result *result = SpineExtension::calloc<_spine_skeleton_data_result>(1, __FILE__, __LINE__);
|
||||
SkeletonJson json((Atlas *) ((_spine_atlas *) atlas)->atlas);
|
||||
json.setScale(1);
|
||||
|
||||
SkeletonData *data = json.readSkeletonData(skeletonData);
|
||||
if (!data) {
|
||||
result->error = (utf8 *) strdup("Failed to load skeleton data");
|
||||
return (spine_skeleton_data_result) result;
|
||||
}
|
||||
|
||||
result->skeletonData = (spine_skeleton_data) data;
|
||||
return (spine_skeleton_data_result) result;
|
||||
}
|
||||
|
||||
spine_skeleton_data_result spine_skeleton_data_load_binary(spine_atlas atlas, const uint8_t *skeletonData, int32_t length) {
|
||||
if (!atlas || !skeletonData) return nullptr;
|
||||
_spine_skeleton_data_result *result = SpineExtension::calloc<_spine_skeleton_data_result>(1, __FILE__, __LINE__);
|
||||
SkeletonBinary binary((Atlas *) ((_spine_atlas *) atlas)->atlas);
|
||||
binary.setScale(1);
|
||||
|
||||
SkeletonData *data = binary.readSkeletonData((const unsigned char *) skeletonData, length);
|
||||
if (!data) {
|
||||
result->error = (utf8 *) strdup("Failed to load skeleton data");
|
||||
return (spine_skeleton_data_result) result;
|
||||
}
|
||||
|
||||
result->skeletonData = (spine_skeleton_data) data;
|
||||
return (spine_skeleton_data_result) result;
|
||||
}
|
||||
|
||||
utf8 *spine_skeleton_data_result_get_error(spine_skeleton_data_result result) {
|
||||
if (!result) return nullptr;
|
||||
return ((_spine_skeleton_data_result *) result)->error;
|
||||
}
|
||||
|
||||
spine_skeleton_data spine_skeleton_data_result_get_data(spine_skeleton_data_result result) {
|
||||
if (!result) return nullptr;
|
||||
return ((_spine_skeleton_data_result *) result)->skeletonData;
|
||||
}
|
||||
|
||||
void spine_skeleton_data_result_dispose(spine_skeleton_data_result result) {
|
||||
if (!result) return;
|
||||
_spine_skeleton_data_result *_result = (_spine_skeleton_data_result *) result;
|
||||
if (_result->error) {
|
||||
SpineExtension::free(_result->error, __FILE__, __LINE__);
|
||||
}
|
||||
SpineExtension::free(_result, __FILE__, __LINE__);
|
||||
}
|
||||
|
||||
// Skeleton drawable
|
||||
spine_skeleton_drawable spine_skeleton_drawable_create(spine_skeleton_data skeletonData) {
|
||||
if (!skeletonData) return nullptr;
|
||||
_spine_skeleton_drawable *drawable = new (__FILE__, __LINE__) _spine_skeleton_drawable();
|
||||
|
||||
Skeleton *skeleton = new (__FILE__, __LINE__) Skeleton(*((SkeletonData *) skeletonData));
|
||||
AnimationStateData *stateData = new (__FILE__, __LINE__) AnimationStateData((SkeletonData *) skeletonData);
|
||||
AnimationState *state = new (__FILE__, __LINE__) AnimationState(stateData);
|
||||
EventListener *listener = new (__FILE__, __LINE__) EventListener();
|
||||
state->setListener(listener);
|
||||
|
||||
drawable->skeleton = (spine_skeleton) skeleton;
|
||||
drawable->animationStateData = (spine_animation_state_data) stateData;
|
||||
drawable->animationState = (spine_animation_state) state;
|
||||
drawable->animationStateEvents = (spine_animation_state_events) listener;
|
||||
drawable->renderer = new (__FILE__, __LINE__) SkeletonRenderer();
|
||||
|
||||
return (spine_skeleton_drawable) drawable;
|
||||
}
|
||||
|
||||
spine_render_command spine_skeleton_drawable_render(spine_skeleton_drawable drawable) {
|
||||
if (!drawable) return nullptr;
|
||||
_spine_skeleton_drawable *_drawable = (_spine_skeleton_drawable *) drawable;
|
||||
Skeleton *skeleton = (Skeleton *) _drawable->skeleton;
|
||||
SkeletonRenderer *renderer = _drawable->renderer;
|
||||
|
||||
RenderCommand *commands = renderer->render(*skeleton);
|
||||
return (spine_render_command) commands;
|
||||
}
|
||||
|
||||
void spine_skeleton_drawable_dispose(spine_skeleton_drawable drawable) {
|
||||
if (!drawable) return;
|
||||
_spine_skeleton_drawable *_drawable = (_spine_skeleton_drawable *) drawable;
|
||||
|
||||
if (_drawable->renderer) {
|
||||
delete _drawable->renderer;
|
||||
}
|
||||
if (_drawable->animationState) {
|
||||
delete (AnimationState *) _drawable->animationState;
|
||||
}
|
||||
if (_drawable->animationStateData) {
|
||||
delete (AnimationStateData *) _drawable->animationStateData;
|
||||
}
|
||||
if (_drawable->skeleton) {
|
||||
delete (Skeleton *) _drawable->skeleton;
|
||||
}
|
||||
if (_drawable->animationStateEvents) {
|
||||
delete (EventListener *) _drawable->animationStateEvents;
|
||||
}
|
||||
|
||||
delete _drawable;
|
||||
}
|
||||
|
||||
spine_skeleton spine_skeleton_drawable_get_skeleton(spine_skeleton_drawable drawable) {
|
||||
if (!drawable) return nullptr;
|
||||
return ((_spine_skeleton_drawable *) drawable)->skeleton;
|
||||
}
|
||||
|
||||
spine_animation_state spine_skeleton_drawable_get_animation_state(spine_skeleton_drawable drawable) {
|
||||
if (!drawable) return nullptr;
|
||||
return ((_spine_skeleton_drawable *) drawable)->animationState;
|
||||
}
|
||||
|
||||
spine_animation_state_data spine_skeleton_drawable_get_animation_state_data(spine_skeleton_drawable drawable) {
|
||||
if (!drawable) return nullptr;
|
||||
return ((_spine_skeleton_drawable *) drawable)->animationStateData;
|
||||
}
|
||||
|
||||
spine_animation_state_events spine_skeleton_drawable_get_animation_state_events(spine_skeleton_drawable drawable) {
|
||||
if (!drawable) return nullptr;
|
||||
return ((_spine_skeleton_drawable *) drawable)->animationStateEvents;
|
||||
}
|
||||
|
||||
// Render command functions
|
||||
float *spine_render_command_get_positions(spine_render_command command) {
|
||||
if (!command) return nullptr;
|
||||
return ((RenderCommand *) command)->positions;
|
||||
}
|
||||
|
||||
float *spine_render_command_get_uvs(spine_render_command command) {
|
||||
if (!command) return nullptr;
|
||||
return ((RenderCommand *) command)->uvs;
|
||||
}
|
||||
|
||||
int32_t *spine_render_command_get_colors(spine_render_command command) {
|
||||
if (!command) return nullptr;
|
||||
return (int32_t *) ((RenderCommand *) command)->colors;
|
||||
}
|
||||
|
||||
int32_t *spine_render_command_get_dark_colors(spine_render_command command) {
|
||||
if (!command) return nullptr;
|
||||
return (int32_t *) ((RenderCommand *) command)->darkColors;
|
||||
}
|
||||
|
||||
int32_t spine_render_command_get_num_vertices(spine_render_command command) {
|
||||
if (!command) return 0;
|
||||
return ((RenderCommand *) command)->numVertices;
|
||||
}
|
||||
|
||||
uint16_t *spine_render_command_get_indices(spine_render_command command) {
|
||||
if (!command) return nullptr;
|
||||
return ((RenderCommand *) command)->indices;
|
||||
}
|
||||
|
||||
int32_t spine_render_command_get_num_indices(spine_render_command command) {
|
||||
if (!command) return 0;
|
||||
return ((RenderCommand *) command)->numIndices;
|
||||
}
|
||||
|
||||
int32_t spine_render_command_get_atlas_page(spine_render_command command) {
|
||||
if (!command) return 0;
|
||||
return (int32_t) (intptr_t) ((RenderCommand *) command)->texture;
|
||||
}
|
||||
|
||||
spine_blend_mode spine_render_command_get_blend_mode(spine_render_command command) {
|
||||
if (!command) return SPINE_BLEND_MODE_NORMAL;
|
||||
BlendMode mode = ((RenderCommand *) command)->blendMode;
|
||||
switch (mode) {
|
||||
case BlendMode_Normal: return SPINE_BLEND_MODE_NORMAL;
|
||||
case BlendMode_Additive: return SPINE_BLEND_MODE_ADDITIVE;
|
||||
case BlendMode_Multiply: return SPINE_BLEND_MODE_MULTIPLY;
|
||||
case BlendMode_Screen: return SPINE_BLEND_MODE_SCREEN;
|
||||
default: return SPINE_BLEND_MODE_NORMAL;
|
||||
}
|
||||
}
|
||||
|
||||
spine_render_command spine_render_command_get_next(spine_render_command command) {
|
||||
if (!command) return nullptr;
|
||||
return (spine_render_command) ((RenderCommand *) command)->next;
|
||||
}
|
||||
|
||||
// Skin entries
|
||||
spine_skin_entries spine_skin_entries_create() {
|
||||
_spine_skin_entries *entries = SpineExtension::calloc<_spine_skin_entries>(1, __FILE__, __LINE__);
|
||||
return (spine_skin_entries) entries;
|
||||
}
|
||||
|
||||
void spine_skin_entries_dispose(spine_skin_entries entries) {
|
||||
if (!entries) return;
|
||||
_spine_skin_entries *_entries = (_spine_skin_entries *) entries;
|
||||
if (_entries->entries) {
|
||||
for (int i = 0; i < _entries->numEntries; i++) {
|
||||
if (_entries->entries[i].name) {
|
||||
SpineExtension::free(_entries->entries[i].name, __FILE__, __LINE__);
|
||||
}
|
||||
}
|
||||
SpineExtension::free(_entries->entries, __FILE__, __LINE__);
|
||||
}
|
||||
SpineExtension::free(_entries, __FILE__, __LINE__);
|
||||
}
|
||||
|
||||
int32_t spine_skin_entries_get_num_entries(spine_skin_entries entries) {
|
||||
if (!entries) return 0;
|
||||
return ((_spine_skin_entries *) entries)->numEntries;
|
||||
}
|
||||
|
||||
spine_skin_entry spine_skin_entries_get_entry(spine_skin_entries entries, int32_t index) {
|
||||
if (!entries) return nullptr;
|
||||
_spine_skin_entries *_entries = (_spine_skin_entries *) entries;
|
||||
if (index < 0 || index >= _entries->numEntries) return nullptr;
|
||||
return (spine_skin_entry) &_entries->entries[index];
|
||||
}
|
||||
|
||||
int32_t spine_skin_entry_get_slot_index(spine_skin_entry entry) {
|
||||
if (!entry) return 0;
|
||||
return ((_spine_skin_entry *) entry)->slotIndex;
|
||||
}
|
||||
|
||||
const utf8 *spine_skin_entry_get_name(spine_skin_entry entry) {
|
||||
if (!entry) return nullptr;
|
||||
return ((_spine_skin_entry *) entry)->name;
|
||||
}
|
||||
|
||||
spine_attachment spine_skin_entry_get_attachment(spine_skin_entry entry) {
|
||||
if (!entry) return nullptr;
|
||||
return ((_spine_skin_entry *) entry)->attachment;
|
||||
}
|
||||
@ -1,194 +0,0 @@
|
||||
/******************************************************************************
|
||||
* Spine Runtimes License Agreement
|
||||
* Last updated April 5, 2025. Replaces all prior versions.
|
||||
*
|
||||
* Copyright (c) 2013-2025, Esoteric Software LLC
|
||||
*
|
||||
* Integration of the Spine Runtimes into software or otherwise creating
|
||||
* derivative works of the Spine Runtimes is permitted under the terms and
|
||||
* conditions of Section 2 of the Spine Editor License Agreement:
|
||||
* http://esotericsoftware.com/spine-editor-license
|
||||
*
|
||||
* Otherwise, it is permitted to integrate the Spine Runtimes into software
|
||||
* or otherwise create derivative works of the Spine Runtimes (collectively,
|
||||
* "Products"), provided that each user of the Products must obtain their own
|
||||
* Spine Editor license and redistribution of the Products in any form must
|
||||
* include this license and copyright notice.
|
||||
*
|
||||
* THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
|
||||
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
|
||||
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
|
||||
* BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
|
||||
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
|
||||
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*****************************************************************************/
|
||||
|
||||
#ifndef SPINE_C_NEW_CUSTOM_H
|
||||
#define SPINE_C_NEW_CUSTOM_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
#if _WIN32
|
||||
#define SPINE_C_EXPORT extern "C" __declspec(dllexport)
|
||||
#else
|
||||
#ifdef __EMSCRIPTEN__
|
||||
#define SPINE_C_EXPORT extern "C" __attribute__((used))
|
||||
#else
|
||||
#define SPINE_C_EXPORT extern "C"
|
||||
#endif
|
||||
#endif
|
||||
#else
|
||||
#if _WIN32
|
||||
#define SPINE_C_EXPORT __declspec(dllexport)
|
||||
#else
|
||||
#ifdef __EMSCRIPTEN__
|
||||
#define SPINE_C_EXPORT __attribute__((used))
|
||||
#else
|
||||
#define SPINE_C_EXPORT
|
||||
#endif
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#define SPINE_OPAQUE_TYPE(name) \
|
||||
typedef struct name##_wrapper { \
|
||||
} name##_wrapper; \
|
||||
typedef name##_wrapper *name;
|
||||
|
||||
typedef char utf8;
|
||||
typedef int32_t spine_bool;
|
||||
typedef size_t spine_size_t;
|
||||
|
||||
// Custom types for spine-c-new
|
||||
SPINE_OPAQUE_TYPE(spine_atlas)
|
||||
SPINE_OPAQUE_TYPE(spine_skeleton_data_result)
|
||||
SPINE_OPAQUE_TYPE(spine_bounds)
|
||||
SPINE_OPAQUE_TYPE(spine_vector)
|
||||
SPINE_OPAQUE_TYPE(spine_color)
|
||||
SPINE_OPAQUE_TYPE(spine_skeleton_drawable)
|
||||
SPINE_OPAQUE_TYPE(spine_render_command)
|
||||
SPINE_OPAQUE_TYPE(spine_skin_entry)
|
||||
SPINE_OPAQUE_TYPE(spine_skin_entries)
|
||||
SPINE_OPAQUE_TYPE(spine_rtti)
|
||||
|
||||
// Texture loader callbacks
|
||||
typedef void* (*spine_texture_loader_load_func)(const char *path);
|
||||
typedef void (*spine_texture_loader_unload_func)(void *texture);
|
||||
|
||||
// Version functions
|
||||
SPINE_C_EXPORT int32_t spine_major_version();
|
||||
SPINE_C_EXPORT int32_t spine_minor_version();
|
||||
SPINE_C_EXPORT void spine_enable_debug_extension(spine_bool enable);
|
||||
SPINE_C_EXPORT void spine_report_leaks();
|
||||
|
||||
// Color functions
|
||||
SPINE_C_EXPORT float spine_color_get_r(spine_color color);
|
||||
SPINE_C_EXPORT float spine_color_get_g(spine_color color);
|
||||
SPINE_C_EXPORT float spine_color_get_b(spine_color color);
|
||||
SPINE_C_EXPORT float spine_color_get_a(spine_color color);
|
||||
|
||||
// Bounds functions
|
||||
SPINE_C_EXPORT float spine_bounds_get_x(spine_bounds bounds);
|
||||
SPINE_C_EXPORT float spine_bounds_get_y(spine_bounds bounds);
|
||||
SPINE_C_EXPORT float spine_bounds_get_width(spine_bounds bounds);
|
||||
SPINE_C_EXPORT float spine_bounds_get_height(spine_bounds bounds);
|
||||
|
||||
// Vector functions
|
||||
SPINE_C_EXPORT float spine_vector_get_x(spine_vector vector);
|
||||
SPINE_C_EXPORT float spine_vector_get_y(spine_vector vector);
|
||||
|
||||
// Atlas functions
|
||||
SPINE_C_EXPORT spine_atlas spine_atlas_load(const utf8 *atlasData);
|
||||
SPINE_C_EXPORT spine_atlas spine_atlas_load_callback(const utf8 *atlasData, const utf8 *atlasDir,
|
||||
spine_texture_loader_load_func load,
|
||||
spine_texture_loader_unload_func unload);
|
||||
SPINE_C_EXPORT int32_t spine_atlas_get_num_image_paths(spine_atlas atlas);
|
||||
SPINE_C_EXPORT utf8 *spine_atlas_get_image_path(spine_atlas atlas, int32_t index);
|
||||
SPINE_C_EXPORT spine_bool spine_atlas_is_pma(spine_atlas atlas);
|
||||
SPINE_C_EXPORT utf8 *spine_atlas_get_error(spine_atlas atlas);
|
||||
SPINE_C_EXPORT void spine_atlas_dispose(spine_atlas atlas);
|
||||
|
||||
// Forward declarations for types used in generated headers
|
||||
struct spine_skeleton_data_wrapper;
|
||||
typedef struct spine_skeleton_data_wrapper *spine_skeleton_data;
|
||||
struct spine_timeline_wrapper;
|
||||
typedef struct spine_timeline_wrapper *spine_timeline;
|
||||
struct spine_skeleton_wrapper;
|
||||
typedef struct spine_skeleton_wrapper *spine_skeleton;
|
||||
struct spine_event_wrapper;
|
||||
typedef struct spine_event_wrapper *spine_event;
|
||||
struct spine_skin_wrapper;
|
||||
typedef struct spine_skin_wrapper *spine_skin;
|
||||
struct spine_sequence_wrapper;
|
||||
typedef struct spine_sequence_wrapper *spine_sequence;
|
||||
struct spine_attachment_wrapper;
|
||||
typedef struct spine_attachment_wrapper *spine_attachment;
|
||||
|
||||
SPINE_C_EXPORT spine_skeleton_data_result spine_skeleton_data_load_json(spine_atlas atlas, const utf8 *skeletonData);
|
||||
SPINE_C_EXPORT spine_skeleton_data_result spine_skeleton_data_load_binary(spine_atlas atlas, const uint8_t *skeletonData, int32_t length);
|
||||
SPINE_C_EXPORT utf8 *spine_skeleton_data_result_get_error(spine_skeleton_data_result result);
|
||||
SPINE_C_EXPORT spine_skeleton_data spine_skeleton_data_result_get_data(spine_skeleton_data_result result);
|
||||
SPINE_C_EXPORT void spine_skeleton_data_result_dispose(spine_skeleton_data_result result);
|
||||
|
||||
// Skeleton drawable - these need forward declarations
|
||||
struct spine_skeleton_wrapper;
|
||||
typedef struct spine_skeleton_wrapper *spine_skeleton;
|
||||
struct spine_animation_state_wrapper;
|
||||
typedef struct spine_animation_state_wrapper *spine_animation_state;
|
||||
struct spine_animation_state_data_wrapper;
|
||||
typedef struct spine_animation_state_data_wrapper *spine_animation_state_data;
|
||||
struct spine_animation_state_events_wrapper;
|
||||
typedef struct spine_animation_state_events_wrapper *spine_animation_state_events;
|
||||
|
||||
SPINE_C_EXPORT spine_skeleton_drawable spine_skeleton_drawable_create(spine_skeleton_data skeletonData);
|
||||
SPINE_C_EXPORT spine_render_command spine_skeleton_drawable_render(spine_skeleton_drawable drawable);
|
||||
SPINE_C_EXPORT void spine_skeleton_drawable_dispose(spine_skeleton_drawable drawable);
|
||||
SPINE_C_EXPORT spine_skeleton spine_skeleton_drawable_get_skeleton(spine_skeleton_drawable drawable);
|
||||
SPINE_C_EXPORT spine_animation_state spine_skeleton_drawable_get_animation_state(spine_skeleton_drawable drawable);
|
||||
SPINE_C_EXPORT spine_animation_state_data spine_skeleton_drawable_get_animation_state_data(spine_skeleton_drawable drawable);
|
||||
SPINE_C_EXPORT spine_animation_state_events spine_skeleton_drawable_get_animation_state_events(spine_skeleton_drawable drawable);
|
||||
|
||||
// Render command functions
|
||||
SPINE_C_EXPORT float *spine_render_command_get_positions(spine_render_command command);
|
||||
SPINE_C_EXPORT float *spine_render_command_get_uvs(spine_render_command command);
|
||||
SPINE_C_EXPORT int32_t *spine_render_command_get_colors(spine_render_command command);
|
||||
SPINE_C_EXPORT int32_t *spine_render_command_get_dark_colors(spine_render_command command);
|
||||
SPINE_C_EXPORT int32_t spine_render_command_get_num_vertices(spine_render_command command);
|
||||
SPINE_C_EXPORT uint16_t *spine_render_command_get_indices(spine_render_command command);
|
||||
SPINE_C_EXPORT int32_t spine_render_command_get_num_indices(spine_render_command command);
|
||||
SPINE_C_EXPORT int32_t spine_render_command_get_atlas_page(spine_render_command command);
|
||||
|
||||
// Forward declaration for spine_blend_mode enum
|
||||
typedef enum spine_blend_mode {
|
||||
SPINE_BLEND_MODE_NORMAL = 0,
|
||||
SPINE_BLEND_MODE_ADDITIVE,
|
||||
SPINE_BLEND_MODE_MULTIPLY,
|
||||
SPINE_BLEND_MODE_SCREEN
|
||||
} spine_blend_mode;
|
||||
|
||||
// Forward declarations for other enum types used in generated headers
|
||||
typedef enum spine_mix_blend spine_mix_blend;
|
||||
typedef enum spine_mix_direction spine_mix_direction;
|
||||
|
||||
SPINE_C_EXPORT spine_blend_mode spine_render_command_get_blend_mode(spine_render_command command);
|
||||
SPINE_C_EXPORT spine_render_command spine_render_command_get_next(spine_render_command command);
|
||||
|
||||
// Skin entries - these need forward declarations
|
||||
struct spine_attachment_wrapper;
|
||||
typedef struct spine_attachment_wrapper *spine_attachment;
|
||||
|
||||
SPINE_C_EXPORT spine_skin_entries spine_skin_entries_create();
|
||||
SPINE_C_EXPORT void spine_skin_entries_dispose(spine_skin_entries entries);
|
||||
SPINE_C_EXPORT int32_t spine_skin_entries_get_num_entries(spine_skin_entries entries);
|
||||
SPINE_C_EXPORT spine_skin_entry spine_skin_entries_get_entry(spine_skin_entries entries, int32_t index);
|
||||
|
||||
SPINE_C_EXPORT int32_t spine_skin_entry_get_slot_index(spine_skin_entry entry);
|
||||
SPINE_C_EXPORT const utf8 *spine_skin_entry_get_name(spine_skin_entry entry);
|
||||
SPINE_C_EXPORT spine_attachment spine_skin_entry_get_attachment(spine_skin_entry entry);
|
||||
|
||||
#endif // SPINE_C_NEW_CUSTOM_H
|
||||
@ -1,670 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const { execSync } = require('child_process');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const scriptDir = path.dirname(path.resolve(__filename));
|
||||
const spineIncludeDir = path.join(scriptDir, 'spine-cpp', 'include');
|
||||
|
||||
function showHelp() {
|
||||
console.log(`
|
||||
extract-spine-cpp-types.js - Extract type information from spine-cpp header files
|
||||
|
||||
USAGE:
|
||||
extract-spine-cpp-types.js [<header-file>]
|
||||
extract-spine-cpp-types.js --help
|
||||
|
||||
DESCRIPTION:
|
||||
Extracts classes, structs, enums, and their public members from C++ headers.
|
||||
Without arguments, processes all headers in spine-cpp/include.
|
||||
|
||||
In all-files mode, performs two passes:
|
||||
1. Extract all type definitions
|
||||
2. Add inherited methods from supertypes (including template instantiations)
|
||||
|
||||
OUTPUT FORMAT:
|
||||
Single file mode: Array of type definitions
|
||||
All files mode: Object with relative file paths as keys
|
||||
|
||||
TYPE DEFINITION STRUCTURE:
|
||||
{
|
||||
"name": "ClassName", // Type name
|
||||
"kind": "class" | "struct" | "enum", // Type kind
|
||||
"loc": { // Source location
|
||||
"line": 45,
|
||||
"col": 15
|
||||
},
|
||||
"superTypes": ["BaseClass", "Interface"], // Base classes (optional)
|
||||
"members": [...], // Public members (classes/structs)
|
||||
"values": [...], // Enum constants (enums only)
|
||||
"isTemplate": true, // Present if type is a template
|
||||
"templateParams": ["T", "U"], // Template parameter names (templates only)
|
||||
"isAbstract": true // Present if class has pure virtual methods
|
||||
}
|
||||
|
||||
MEMBER STRUCTURE:
|
||||
Fields:
|
||||
{
|
||||
"kind": "field",
|
||||
"name": "fieldName",
|
||||
"type": "int"
|
||||
}
|
||||
|
||||
Methods:
|
||||
{
|
||||
"kind": "method",
|
||||
"name": "methodName",
|
||||
"returnType": "void",
|
||||
"parameters": [
|
||||
{"name": "param1", "type": "int"},
|
||||
{"name": "param2", "type": "const String &"}
|
||||
],
|
||||
"isStatic": false,
|
||||
"isVirtual": true,
|
||||
"isPure": false,
|
||||
"fromSupertype": "BaseClass" // Present if inherited (all-files mode only)
|
||||
}
|
||||
|
||||
Constructors:
|
||||
{
|
||||
"kind": "constructor",
|
||||
"name": "ClassName",
|
||||
"parameters": [...]
|
||||
}
|
||||
|
||||
ENUM VALUES:
|
||||
{
|
||||
"name": "ENUM_VALUE",
|
||||
"value": "1 << 0" // Present only if explicitly initialized
|
||||
}
|
||||
|
||||
INHERITANCE (all-files mode only):
|
||||
- Methods inherited from non-template supertypes are included
|
||||
- Methods inherited from template supertypes have parameters substituted
|
||||
- Full inheritance hierarchy is preserved (grandparent methods appear via parent)
|
||||
- SpineObject methods are excluded (considered noise)
|
||||
- Methods are marked with "fromSupertype" field indicating immediate source
|
||||
- Template supertypes appear as "TemplateName<Type1, Type2>"
|
||||
- Example: If C inherits B inherits A, C gets all methods from A and B
|
||||
|
||||
NOTES:
|
||||
- Only public members are extracted
|
||||
- Forward declarations are excluded
|
||||
- Template instantiations (e.g., Vector<int>) are excluded
|
||||
- Friend declarations are excluded
|
||||
- Implicit/compiler-generated methods are excluded
|
||||
- Destructors and operator methods are excluded (not needed for C wrapper generation)
|
||||
|
||||
EXAMPLES:
|
||||
# Extract types from single file
|
||||
extract-spine-cpp-types.js spine-cpp/include/spine/Animation.h > animation.json
|
||||
|
||||
# Extract all types with inheritance resolution
|
||||
extract-spine-cpp-types.js > all-spine-types.json
|
||||
|
||||
# Query specific type
|
||||
extract-spine-cpp-types.js > types.json
|
||||
jq '."spine/Bone.h"[] | select(.name == "Bone")' types.json
|
||||
`);
|
||||
}
|
||||
|
||||
function extractLocalTypes(headerFile, typeMap = null) {
|
||||
const absHeaderPath = path.resolve(headerFile);
|
||||
const sourceContent = fs.readFileSync(absHeaderPath, 'utf8');
|
||||
const sourceLines = sourceContent.split('\n');
|
||||
|
||||
// Get AST from clang
|
||||
const cmd = `clang++ -Xclang -ast-dump=json -fsyntax-only -std=c++11 -I "${spineIncludeDir}" "${absHeaderPath}" 2>/dev/null`;
|
||||
const maxBuffer = headerFile.includes('Debug.h') ? 500 : 200; // MB
|
||||
|
||||
let astJson;
|
||||
try {
|
||||
const output = execSync(cmd, { encoding: 'utf8', maxBuffer: maxBuffer * 1024 * 1024 });
|
||||
astJson = JSON.parse(output);
|
||||
} catch (error) {
|
||||
throw new Error(error.code === 'ENOBUFS'
|
||||
? `AST output too large (>${maxBuffer}MB)`
|
||||
: error.message);
|
||||
}
|
||||
|
||||
const types = [];
|
||||
|
||||
function extractEnumValueFromSource(enumConstNode) {
|
||||
if (!enumConstNode.loc) return undefined;
|
||||
|
||||
const line = sourceLines[enumConstNode.loc.line - 1];
|
||||
if (!line) return undefined;
|
||||
|
||||
// Find enum name and check for '='
|
||||
const nameMatch = line.match(new RegExp(`\\b${enumConstNode.name}\\b`));
|
||||
if (!nameMatch) return undefined;
|
||||
|
||||
const afterName = line.substring(nameMatch.index + enumConstNode.name.length);
|
||||
const equalIndex = afterName.indexOf('=');
|
||||
if (equalIndex === -1) return null; // No explicit value
|
||||
|
||||
// Extract value expression
|
||||
let valueText = afterName.substring(equalIndex + 1);
|
||||
|
||||
// Handle multi-line values
|
||||
let currentLine = enumConstNode.loc.line;
|
||||
while (currentLine < sourceLines.length && !valueText.match(/[,}]/)) {
|
||||
valueText += '\n' + sourceLines[currentLine++];
|
||||
}
|
||||
|
||||
// Extract up to comma or brace
|
||||
const endMatch = valueText.match(/^(.*?)([,}])/s);
|
||||
if (endMatch) valueText = endMatch[1];
|
||||
|
||||
// Clean up
|
||||
return valueText
|
||||
.replace(/\/\/.*$/gm, '') // Remove single-line comments
|
||||
.replace(/\/\*[\s\S]*?\*\//g, '') // Remove multi-line comments
|
||||
.replace(/\s+/g, ' ') // Normalize whitespace
|
||||
.trim();
|
||||
}
|
||||
|
||||
function extractReturnType(methodNode) {
|
||||
const fullType = methodNode.type?.qualType || '';
|
||||
const match = fullType.match(/^(.+?)\s*\(/);
|
||||
return match ? match[1].trim() : 'void';
|
||||
}
|
||||
|
||||
function extractParameters(methodNode) {
|
||||
return (methodNode.inner || [])
|
||||
.filter(n => n.kind === 'ParmVarDecl')
|
||||
.map(n => ({
|
||||
name: n.name || '',
|
||||
type: n.type?.qualType || ''
|
||||
}));
|
||||
}
|
||||
|
||||
function isInTargetFile(node) {
|
||||
if (!node.loc) return false;
|
||||
|
||||
const targetPath = path.resolve(headerFile);
|
||||
const loc = node.loc;
|
||||
|
||||
// Check direct file location
|
||||
if (loc.file) return path.resolve(loc.file) === targetPath;
|
||||
|
||||
// Check macro locations
|
||||
for (const locType of ['spellingLoc', 'expansionLoc']) {
|
||||
if (loc[locType]) {
|
||||
if (loc[locType].includedFrom) return false;
|
||||
if (loc[locType].file) return path.resolve(loc[locType].file) === targetPath;
|
||||
}
|
||||
}
|
||||
|
||||
// If included from another file, reject
|
||||
if (loc.includedFrom) return false;
|
||||
|
||||
// No location info - assume it's in the main file
|
||||
return true;
|
||||
}
|
||||
|
||||
function extractTypeInfo(node) {
|
||||
const info = {
|
||||
name: node.name || '',
|
||||
kind: node.kind === 'EnumDecl' ? 'enum' : (node.tagUsed || ''),
|
||||
loc: {
|
||||
line: node.loc?.line || 0,
|
||||
col: node.loc?.col || 0
|
||||
}
|
||||
};
|
||||
|
||||
// Extract base classes
|
||||
if (node.bases?.length > 0) {
|
||||
info.superTypes = node.bases.map(b => b.type?.qualType || '').filter(Boolean);
|
||||
}
|
||||
|
||||
// For enums, extract the values
|
||||
if (node.kind === 'EnumDecl') {
|
||||
info.values = (node.inner || [])
|
||||
.filter(n => n.kind === 'EnumConstantDecl')
|
||||
.map(n => {
|
||||
const enumValue = { name: n.name || '' };
|
||||
const sourceValue = extractEnumValueFromSource(n);
|
||||
|
||||
if (sourceValue === null) {
|
||||
// Implicit value - no value property
|
||||
} else if (sourceValue) {
|
||||
enumValue.value = sourceValue;
|
||||
} else if (n.inner?.length > 0) {
|
||||
enumValue.value = "<<extraction failed>>";
|
||||
}
|
||||
|
||||
return enumValue;
|
||||
});
|
||||
return info;
|
||||
}
|
||||
|
||||
// For classes/structs, extract public members
|
||||
info.members = [];
|
||||
let currentAccess = node.tagUsed === 'struct' ? 'public' : 'private';
|
||||
let hasPureVirtual = false;
|
||||
|
||||
for (const inner of node.inner || []) {
|
||||
if (inner.kind === 'AccessSpecDecl') {
|
||||
currentAccess = inner.access || 'private';
|
||||
continue;
|
||||
}
|
||||
|
||||
if (inner.kind === 'FriendDecl' || currentAccess !== 'public') continue;
|
||||
|
||||
const member = extractMember(inner, node);
|
||||
if (member) {
|
||||
info.members.push(member);
|
||||
// Check if this is a pure virtual method
|
||||
if (member.kind === 'method' && member.isPure) {
|
||||
hasPureVirtual = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Always set isAbstract to a boolean value
|
||||
info.isAbstract = hasPureVirtual;
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
function extractMember(inner, parent) {
|
||||
if (inner.isImplicit) return null;
|
||||
|
||||
switch (inner.kind) {
|
||||
case 'FieldDecl':
|
||||
return {
|
||||
kind: 'field',
|
||||
name: inner.name || '',
|
||||
type: inner.type?.qualType || ''
|
||||
};
|
||||
|
||||
case 'CXXMethodDecl':
|
||||
if (!inner.name) return null;
|
||||
// Skip operators - not needed for C wrapper generation
|
||||
if (inner.name.startsWith('operator')) return null;
|
||||
|
||||
return {
|
||||
kind: 'method',
|
||||
name: inner.name,
|
||||
returnType: extractReturnType(inner),
|
||||
parameters: extractParameters(inner),
|
||||
isStatic: inner.storageClass === 'static',
|
||||
isVirtual: inner.virtual || false,
|
||||
isPure: inner.pure || false
|
||||
};
|
||||
|
||||
case 'CXXConstructorDecl':
|
||||
return {
|
||||
kind: 'constructor',
|
||||
name: inner.name || parent.name || '',
|
||||
parameters: extractParameters(inner)
|
||||
};
|
||||
|
||||
case 'CXXDestructorDecl':
|
||||
// Skip destructors - not needed for C wrapper generation
|
||||
return null;
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function processNode(node, inSpineNamespace = false) {
|
||||
if (!node || typeof node !== 'object') return;
|
||||
|
||||
// Handle spine namespace
|
||||
if (node.kind === 'NamespaceDecl' && node.name === 'spine') {
|
||||
(node.inner || []).forEach(n => processNode(n, true));
|
||||
return;
|
||||
}
|
||||
|
||||
// Recurse to find spine namespace
|
||||
if (!inSpineNamespace) {
|
||||
(node.inner || []).forEach(n => processNode(n, false));
|
||||
return;
|
||||
}
|
||||
|
||||
// Process type declarations
|
||||
const typeKinds = ['CXXRecordDecl', 'ClassTemplateDecl', 'EnumDecl', 'TypedefDecl', 'TypeAliasDecl'];
|
||||
if (!typeKinds.includes(node.kind)) return;
|
||||
|
||||
// Skip if not in target file or invalid
|
||||
if (!isInTargetFile(node) ||
|
||||
node.isImplicit ||
|
||||
!node.name ||
|
||||
node.name.startsWith('_') ||
|
||||
node.name.includes('<')) return;
|
||||
|
||||
// Skip forward declarations
|
||||
if (node.previousDecl && (!node.inner || node.inner.length === 0)) return;
|
||||
|
||||
// Extract type info
|
||||
if (node.kind === 'ClassTemplateDecl') {
|
||||
const classNode = (node.inner || []).find(n => n.kind === 'CXXRecordDecl');
|
||||
if (classNode) {
|
||||
const typeInfo = extractTypeInfo(classNode);
|
||||
typeInfo.isTemplate = true;
|
||||
|
||||
// Extract template parameters
|
||||
const templateParams = [];
|
||||
for (const inner of node.inner || []) {
|
||||
if (inner.kind === 'TemplateTypeParmDecl' && inner.name) {
|
||||
templateParams.push(inner.name);
|
||||
}
|
||||
}
|
||||
if (templateParams.length > 0) {
|
||||
typeInfo.templateParams = templateParams;
|
||||
}
|
||||
|
||||
types.push(typeInfo);
|
||||
}
|
||||
} else if (node.kind === 'CXXRecordDecl' && node.inner?.length > 0) {
|
||||
const typeInfo = extractTypeInfo(node);
|
||||
// Ensure isTemplate is always set for non-template classes
|
||||
if (typeInfo.isTemplate === undefined) {
|
||||
typeInfo.isTemplate = false;
|
||||
}
|
||||
types.push(typeInfo);
|
||||
} else if (node.kind === 'EnumDecl') {
|
||||
types.push(extractTypeInfo(node));
|
||||
} else if (node.kind === 'TypedefDecl' || node.kind === 'TypeAliasDecl') {
|
||||
types.push(extractTypeInfo(node));
|
||||
}
|
||||
}
|
||||
|
||||
processNode(astJson);
|
||||
|
||||
// Filter out forward declarations and sort by line number
|
||||
const filteredTypes = types
|
||||
.filter(t => !(t.members && t.members.length === 0))
|
||||
.sort((a, b) => a.loc.line - b.loc.line);
|
||||
|
||||
// Add inherited methods if we have a type map
|
||||
if (typeMap) {
|
||||
for (const type of filteredTypes) {
|
||||
if (type.superTypes && type.members) {
|
||||
addInheritedMethods(type, typeMap);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return filteredTypes;
|
||||
}
|
||||
|
||||
function addInheritedMethods(type, typeMap) {
|
||||
const inheritedMethods = [];
|
||||
const ownMethodSignatures = new Set();
|
||||
|
||||
// Build a set of method signatures from this type
|
||||
for (const member of type.members) {
|
||||
if (member.kind === 'method') {
|
||||
const sig = getMethodSignature(member);
|
||||
ownMethodSignatures.add(sig);
|
||||
}
|
||||
}
|
||||
|
||||
// Process each supertype
|
||||
for (const superTypeName of type.superTypes) {
|
||||
// Clean up the supertype name (remove namespaces, etc)
|
||||
const cleanName = superTypeName.replace(/^.*::/, '');
|
||||
|
||||
// Skip SpineObject inheritance - it's just noise
|
||||
if (cleanName === 'SpineObject') continue;
|
||||
|
||||
// Check if this is a template supertype
|
||||
const templateMatch = cleanName.match(/^([^<]+)<(.+)>$/);
|
||||
if (templateMatch) {
|
||||
const templateClassName = templateMatch[1];
|
||||
const templateArgs = templateMatch[2];
|
||||
|
||||
const templateType = typeMap.get(templateClassName);
|
||||
if (templateType && templateType.isTemplate && templateType.members) {
|
||||
// Process template inheritance
|
||||
addTemplateInheritedMethods(
|
||||
type, templateType, templateClassName, templateArgs,
|
||||
inheritedMethods, ownMethodSignatures
|
||||
);
|
||||
} else if (templateType && !templateType.isTemplate) {
|
||||
// Template might not be marked as isTemplate, try anyway
|
||||
addTemplateInheritedMethods(
|
||||
type, templateType, templateClassName, templateArgs,
|
||||
inheritedMethods, ownMethodSignatures
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Non-template supertype
|
||||
const superType = typeMap.get(cleanName);
|
||||
|
||||
if (!superType || !superType.members) continue;
|
||||
|
||||
// Add non-overridden methods from supertype
|
||||
for (const member of superType.members) {
|
||||
if (member.kind === 'method') {
|
||||
const sig = getMethodSignature(member);
|
||||
if (!ownMethodSignatures.has(sig)) {
|
||||
const inheritedMember = { ...member, fromSupertype: cleanName };
|
||||
inheritedMethods.push(inheritedMember);
|
||||
ownMethodSignatures.add(sig); // Prevent duplicates from multiple inheritance
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add inherited methods to the type
|
||||
type.members.push(...inheritedMethods);
|
||||
}
|
||||
|
||||
function addTemplateInheritedMethods(
|
||||
type, templateType, templateClassName, templateArgs,
|
||||
inheritedMethods, ownMethodSignatures
|
||||
) {
|
||||
// Parse template arguments (handle multiple args)
|
||||
const argsList = [];
|
||||
let depth = 0;
|
||||
let currentArg = '';
|
||||
|
||||
for (const char of templateArgs) {
|
||||
if (char === '<') depth++;
|
||||
else if (char === '>') depth--;
|
||||
|
||||
if (char === ',' && depth === 0) {
|
||||
argsList.push(currentArg.trim());
|
||||
currentArg = '';
|
||||
} else {
|
||||
currentArg += char;
|
||||
}
|
||||
}
|
||||
if (currentArg.trim()) {
|
||||
argsList.push(currentArg.trim());
|
||||
}
|
||||
|
||||
// Build a mapping of template params to actual types
|
||||
const paramMap = new Map();
|
||||
|
||||
// Use the actual template parameters if we have them
|
||||
if (templateType.templateParams && templateType.templateParams.length === argsList.length) {
|
||||
templateType.templateParams.forEach((param, i) => {
|
||||
paramMap.set(param, argsList[i]);
|
||||
});
|
||||
} else {
|
||||
// Fallback: if we don't have template param info, skip substitution
|
||||
console.error(`Warning: Template ${templateClassName} missing parameter info, skipping substitution`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Process each member of the template
|
||||
for (const member of templateType.members) {
|
||||
if (member.kind === 'method') {
|
||||
// Skip template constructors - they have weird names like "Pose<P>"
|
||||
if (member.name.includes('<')) continue;
|
||||
|
||||
// Clone the member and substitute template parameters
|
||||
const inheritedMember = JSON.parse(JSON.stringify(member));
|
||||
inheritedMember.fromSupertype = `${templateClassName}<${templateArgs}>`;
|
||||
|
||||
// Replace template parameters in return type
|
||||
if (inheritedMember.returnType) {
|
||||
inheritedMember.returnType = substituteTemplateParams(
|
||||
inheritedMember.returnType, paramMap
|
||||
);
|
||||
}
|
||||
|
||||
// Replace template parameters in parameters
|
||||
if (inheritedMember.parameters) {
|
||||
for (const param of inheritedMember.parameters) {
|
||||
param.type = substituteTemplateParams(param.type, paramMap);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if this method is overridden
|
||||
const sig = getMethodSignature(inheritedMember);
|
||||
if (!ownMethodSignatures.has(sig)) {
|
||||
inheritedMethods.push(inheritedMember);
|
||||
ownMethodSignatures.add(sig);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function substituteTemplateParams(typeStr, paramMap) {
|
||||
let result = typeStr;
|
||||
|
||||
// Replace template parameters in order of length (longest first)
|
||||
// to avoid replacing substrings (e.g., V before V1)
|
||||
const sortedParams = Array.from(paramMap.keys()).sort((a, b) => b.length - a.length);
|
||||
|
||||
for (const param of sortedParams) {
|
||||
const regex = new RegExp(`\\b${param}\\b`, 'g');
|
||||
result = result.replace(regex, paramMap.get(param));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function getMethodSignature(method) {
|
||||
// Create a signature that identifies method overrides
|
||||
// For virtual methods, just use the name
|
||||
// For non-virtual methods, include parameter types
|
||||
let sig = method.name;
|
||||
if (method.parameters && method.parameters.length > 0) {
|
||||
sig += '(' + method.parameters.map(p => p.type).join(',') + ')';
|
||||
} else {
|
||||
sig += '()';
|
||||
}
|
||||
|
||||
// Add const qualifier if present
|
||||
if (method.returnType && method.returnType.includes('const')) {
|
||||
sig += ' const';
|
||||
}
|
||||
|
||||
return sig;
|
||||
}
|
||||
|
||||
function findAllHeaderFiles() {
|
||||
const headers = [];
|
||||
|
||||
function walkDir(dir) {
|
||||
fs.readdirSync(dir).forEach(file => {
|
||||
const fullPath = path.join(dir, file);
|
||||
if (fs.statSync(fullPath).isDirectory()) {
|
||||
walkDir(fullPath);
|
||||
} else if (file.endsWith('.h') && file !== 'spine.h') {
|
||||
headers.push(fullPath);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
walkDir(spineIncludeDir);
|
||||
return headers.sort();
|
||||
}
|
||||
|
||||
// Main execution
|
||||
const arg = process.argv[2];
|
||||
|
||||
if (arg === '--help' || arg === '-h') {
|
||||
showHelp();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (arg) {
|
||||
// Single file mode - no inheritance support
|
||||
if (!fs.existsSync(arg)) {
|
||||
console.error(`Error: File not found: ${arg}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
try {
|
||||
const types = extractLocalTypes(arg);
|
||||
if (types.length === 0) {
|
||||
console.error('No types found in the specified file');
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(JSON.stringify(types, null, 2));
|
||||
} catch (error) {
|
||||
console.error(`Error: ${error.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
} else {
|
||||
// Process all files
|
||||
const allHeaders = findAllHeaderFiles();
|
||||
const allTypes = {};
|
||||
let processed = 0, errors = 0;
|
||||
|
||||
console.error(`Processing ${allHeaders.length} header files...`);
|
||||
|
||||
// First pass: extract all types without inheritance
|
||||
const typeMap = new Map();
|
||||
|
||||
for (const headerFile of allHeaders) {
|
||||
const relPath = path.relative(spineIncludeDir, headerFile);
|
||||
process.stderr.write(`\r\x1b[K Pass 1 - Processing ${++processed}/${allHeaders.length}: ${relPath}...`);
|
||||
|
||||
try {
|
||||
const types = extractLocalTypes(headerFile);
|
||||
if (types.length > 0) {
|
||||
allTypes[relPath] = types;
|
||||
// Build type map
|
||||
for (const type of types) {
|
||||
typeMap.set(type.name, type);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
errors++;
|
||||
console.error(`\n ERROR processing ${relPath}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Second pass: add inherited methods
|
||||
console.error(`\n Pass 2 - Adding inherited methods...`);
|
||||
processed = 0;
|
||||
|
||||
for (const headerFile of allHeaders) {
|
||||
const relPath = path.relative(spineIncludeDir, headerFile);
|
||||
if (!allTypes[relPath]) continue;
|
||||
|
||||
process.stderr.write(`\r\x1b[K Pass 2 - Processing ${++processed}/${Object.keys(allTypes).length}: ${relPath}...`);
|
||||
|
||||
for (const type of allTypes[relPath]) {
|
||||
if (type.superTypes && type.members) {
|
||||
addInheritedMethods(type, typeMap);
|
||||
|
||||
// Check if any inherited methods are pure virtual
|
||||
// If so, and the class doesn't override them, it's abstract
|
||||
if (!type.isAbstract) {
|
||||
const hasPureVirtual = type.members.some(m =>
|
||||
m.kind === 'method' && m.isPure === true
|
||||
);
|
||||
if (hasPureVirtual) {
|
||||
type.isAbstract = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.error(`\n Completed: ${Object.keys(allTypes).length} files processed, ${errors} errors`);
|
||||
console.log(JSON.stringify(allTypes, null, 2));
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user