mirror of
https://github.com/EsotericSoftware/spine-runtimes.git
synced 2026-02-11 17:48:45 +08:00
[cpp] Various fixes for c wrapper generation
This commit is contained in:
parent
e1577829dd
commit
b1e5ed1c25
@ -1,229 +0,0 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { Type, toSnakeCase } from './types';
|
||||
|
||||
const LICENSE_HEADER = `/******************************************************************************
|
||||
* 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.
|
||||
*****************************************************************************/`;
|
||||
|
||||
export class FileWriter {
|
||||
constructor(private outputDir: string) {
|
||||
// Clean and recreate output directory
|
||||
this.cleanOutputDirectory();
|
||||
}
|
||||
|
||||
private cleanOutputDirectory(): void {
|
||||
// Remove existing generated directory if it exists
|
||||
if (fs.existsSync(this.outputDir)) {
|
||||
console.log(`Cleaning ${this.outputDir}...`);
|
||||
fs.rmSync(this.outputDir, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
// Recreate the directory
|
||||
fs.mkdirSync(this.outputDir, { recursive: true });
|
||||
}
|
||||
|
||||
async writeType(typeName: string, headerContent: string[], sourceContent: string[]): Promise<void> {
|
||||
const fileName = toSnakeCase(typeName);
|
||||
|
||||
// Write header file
|
||||
const headerPath = path.join(this.outputDir, `${fileName}.h`);
|
||||
const headerGuard = `SPINE_C_${typeName.toUpperCase()}_H`;
|
||||
|
||||
// Check if the header content uses any custom types from extensions.h
|
||||
const headerString = headerContent.join('\n');
|
||||
const needsExtensions = headerString.includes('spine_void') ||
|
||||
headerString.includes('spine_dispose_renderer_object') ||
|
||||
headerString.includes('spine_texture_loader');
|
||||
|
||||
const headerLines = [
|
||||
LICENSE_HEADER,
|
||||
'',
|
||||
`#ifndef ${headerGuard}`,
|
||||
`#define ${headerGuard}`,
|
||||
'',
|
||||
'#ifdef __cplusplus',
|
||||
'extern "C" {',
|
||||
'#endif',
|
||||
'',
|
||||
'#include "types.h"',
|
||||
];
|
||||
|
||||
// Include extensions.h if needed
|
||||
if (needsExtensions) {
|
||||
headerLines.push('#include "../extensions.h"');
|
||||
}
|
||||
|
||||
headerLines.push(
|
||||
'',
|
||||
...headerContent,
|
||||
'',
|
||||
'#ifdef __cplusplus',
|
||||
'}',
|
||||
'#endif',
|
||||
'',
|
||||
`#endif // ${headerGuard}`
|
||||
);
|
||||
|
||||
fs.writeFileSync(headerPath, headerLines.join('\n'));
|
||||
|
||||
// Write source file (as .cpp since it contains C++ code)
|
||||
const sourcePath = path.join(this.outputDir, `${fileName}.cpp`);
|
||||
const sourceLines = [
|
||||
LICENSE_HEADER,
|
||||
'',
|
||||
...sourceContent
|
||||
];
|
||||
|
||||
fs.writeFileSync(sourcePath, sourceLines.join('\n'));
|
||||
}
|
||||
|
||||
async writeEnum(enumName: string, content: string[]): Promise<void> {
|
||||
const fileName = toSnakeCase(enumName);
|
||||
const headerPath = path.join(this.outputDir, `${fileName}.h`);
|
||||
const headerGuard = `SPINE_C_${enumName.toUpperCase()}_H`;
|
||||
|
||||
const headerLines = [
|
||||
LICENSE_HEADER,
|
||||
'',
|
||||
`#ifndef ${headerGuard}`,
|
||||
`#define ${headerGuard}`,
|
||||
'',
|
||||
'#include "../base.h"',
|
||||
'',
|
||||
'#ifdef __cplusplus',
|
||||
'extern "C" {',
|
||||
'#endif',
|
||||
'',
|
||||
...content,
|
||||
'',
|
||||
'#ifdef __cplusplus',
|
||||
'}',
|
||||
'#endif',
|
||||
'',
|
||||
`#endif // ${headerGuard}`
|
||||
];
|
||||
|
||||
fs.writeFileSync(headerPath, headerLines.join('\n'));
|
||||
}
|
||||
|
||||
async writeMainHeader(classes: Type[], enums: Type[]): Promise<void> {
|
||||
const mainHeaderPath = path.join(this.outputDir, '..', '..', 'include', 'spine-c.h');
|
||||
|
||||
// Ensure include directory exists
|
||||
const includeDir = path.dirname(mainHeaderPath);
|
||||
if (!fs.existsSync(includeDir)) {
|
||||
fs.mkdirSync(includeDir, { recursive: true });
|
||||
}
|
||||
|
||||
const lines = [
|
||||
LICENSE_HEADER,
|
||||
'',
|
||||
'#ifndef SPINE_C_H',
|
||||
'#define SPINE_C_H',
|
||||
'',
|
||||
'// Base definitions',
|
||||
'#include "../src/base.h"',
|
||||
'',
|
||||
'// All type declarations and enum includes',
|
||||
'#include "../src/generated/types.h"',
|
||||
'',
|
||||
'// Extension functions',
|
||||
'#include "../src/extensions.h"',
|
||||
'',
|
||||
'// Generated class types'
|
||||
];
|
||||
|
||||
// Add class includes (they contain the actual function declarations)
|
||||
for (const classType of classes) {
|
||||
lines.push(`#include "../src/generated/${toSnakeCase(classType.name)}.h"`);
|
||||
}
|
||||
|
||||
lines.push('');
|
||||
lines.push('#endif // SPINE_C_H');
|
||||
|
||||
fs.writeFileSync(mainHeaderPath, lines.join('\n'));
|
||||
}
|
||||
|
||||
async writeArrays(header: string[], source: string[]): Promise<void> {
|
||||
const headerPath = path.join(this.outputDir, 'arrays.h');
|
||||
const sourcePath = path.join(this.outputDir, 'arrays.cpp');
|
||||
|
||||
// Add license header to both files
|
||||
const headerLines = [LICENSE_HEADER, '', ...header];
|
||||
const sourceLines = [LICENSE_HEADER, '', ...source];
|
||||
|
||||
fs.writeFileSync(headerPath, headerLines.join('\n'));
|
||||
fs.writeFileSync(sourcePath, sourceLines.join('\n'));
|
||||
}
|
||||
|
||||
async writeTypesHeader(classes: Type[], enums: Type[]): Promise<void> {
|
||||
const headerPath = path.join(this.outputDir, 'types.h');
|
||||
const lines: string[] = [
|
||||
LICENSE_HEADER,
|
||||
'',
|
||||
'#ifndef SPINE_C_TYPES_H',
|
||||
'#define SPINE_C_TYPES_H',
|
||||
'',
|
||||
'#ifdef __cplusplus',
|
||||
'extern "C" {',
|
||||
'#endif',
|
||||
'',
|
||||
'#include "../base.h"',
|
||||
'',
|
||||
'// Forward declarations for all non-enum types'
|
||||
];
|
||||
|
||||
// Forward declare all class types
|
||||
for (const classType of classes) {
|
||||
const cTypeName = `spine_${toSnakeCase(classType.name)}`;
|
||||
lines.push(`SPINE_OPAQUE_TYPE(${cTypeName})`);
|
||||
}
|
||||
|
||||
lines.push('');
|
||||
lines.push('// Include all enum types (cannot be forward declared)');
|
||||
|
||||
// Include all enum headers
|
||||
for (const enumType of enums) {
|
||||
lines.push(`#include "${toSnakeCase(enumType.name)}.h"`);
|
||||
}
|
||||
|
||||
lines.push('');
|
||||
lines.push('// Array specializations');
|
||||
lines.push('#include "arrays.h"');
|
||||
|
||||
lines.push('');
|
||||
lines.push('#ifdef __cplusplus');
|
||||
lines.push('}');
|
||||
lines.push('#endif');
|
||||
lines.push('');
|
||||
lines.push('#endif // SPINE_C_TYPES_H');
|
||||
|
||||
fs.writeFileSync(headerPath, lines.join('\n'));
|
||||
}
|
||||
}
|
||||
@ -1,232 +0,0 @@
|
||||
import { ArraySpecialization, ClassOrStruct, Method, toSnakeCase, Type } from '../types';
|
||||
|
||||
export class ArrayGenerator {
|
||||
private arrayType: ClassOrStruct | undefined;
|
||||
|
||||
constructor(private typesJson: any) {
|
||||
// Find the Array type definition
|
||||
for (const header of Object.keys(typesJson)) {
|
||||
const arrayType = typesJson[header].find((t: Type) => t.kind === 'class' && t.name === 'Array');
|
||||
if (arrayType) {
|
||||
this.arrayType = arrayType;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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');
|
||||
header.push('');
|
||||
header.push('#include "../base.h"');
|
||||
header.push('#include "types.h"');
|
||||
header.push('');
|
||||
header.push('#ifdef __cplusplus');
|
||||
header.push('extern "C" {');
|
||||
header.push('#endif');
|
||||
header.push('');
|
||||
|
||||
// Source file
|
||||
source.push('#include "arrays.h"');
|
||||
source.push('#include <spine/Array.h>');
|
||||
source.push('#include <spine/spine.h>');
|
||||
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'
|
||||
).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 dispose
|
||||
header.push(`SPINE_C_EXPORT void ${spec.cTypeName}_dispose(${spec.cTypeName} array);`);
|
||||
source.push(`void ${spec.cTypeName}_dispose(${spec.cTypeName} array) {`);
|
||||
source.push(` if (!array) return;`);
|
||||
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, int index);`);
|
||||
source.push(`${spec.cElementType} ${spec.cTypeName}_get(${spec.cTypeName} array, int index) {`);
|
||||
source.push(` if (!array) return ${this.getDefaultValue(spec)};`);
|
||||
source.push(` ${spec.cppType} *_array = (${spec.cppType}*) array;`);
|
||||
source.push(` return ${this.convertFromCpp(spec, '(*_array)[index]')};`);
|
||||
source.push('}');
|
||||
source.push('');
|
||||
|
||||
header.push(`SPINE_C_EXPORT void ${spec.cTypeName}_set(${spec.cTypeName} array, int index, ${spec.cElementType} value);`);
|
||||
source.push(`void ${spec.cTypeName}_set(${spec.cTypeName} array, int index, ${spec.cElementType} value) {`);
|
||||
source.push(` if (!array) return;`);
|
||||
source.push(` ${spec.cppType} *_array = (${spec.cppType}*) array;`);
|
||||
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: 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;
|
||||
if (method.returnType && method.returnType !== 'void') {
|
||||
hasReturn = true;
|
||||
if (method.returnType === 'T &' || method.returnType === `${spec.elementType} &`) {
|
||||
// Return by reference becomes return by value in C
|
||||
returnType = spec.cElementType;
|
||||
} else if (method.returnType === 'T *' || method.returnType === `${spec.elementType} *`) {
|
||||
returnType = spec.cElementType;
|
||||
} else if (method.returnType === 'size_t') {
|
||||
returnType = 'size_t';
|
||||
} else if (method.returnType === 'int') {
|
||||
returnType = 'int';
|
||||
} else if (method.returnType === 'bool') {
|
||||
returnType = 'bool';
|
||||
} else if (method.returnType === `${spec.cppType} *`) {
|
||||
returnType = spec.cTypeName;
|
||||
} else {
|
||||
// Unknown return type - skip this method
|
||||
console.log(` Skipping Array method ${method.name} - unknown return type: ${method.returnType}`);
|
||||
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 ||
|
||||
param.type === 'const T &' || param.type === `const ${spec.elementType} &`) {
|
||||
cParams.push(`${spec.cElementType} ${param.name}`);
|
||||
cppArgs.push(this.convertToCpp(spec, param.name));
|
||||
} else if (param.type === 'size_t') {
|
||||
cParams.push(`size_t ${param.name}`);
|
||||
cppArgs.push(param.name);
|
||||
} else if (param.type === 'int') {
|
||||
cParams.push(`int ${param.name}`);
|
||||
cppArgs.push(param.name);
|
||||
} else {
|
||||
// Unknown parameter type - skip this method
|
||||
console.log(` Skipping Array method ${method.name} - unknown param type: ${param.type}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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) {
|
||||
source.push(` return ${this.convertFromCpp(spec, call)};`);
|
||||
} else {
|
||||
source.push(` return ${call};`);
|
||||
}
|
||||
} else {
|
||||
source.push(` ${call};`);
|
||||
}
|
||||
|
||||
source.push('}');
|
||||
source.push('');
|
||||
}
|
||||
|
||||
private getDefaultValue(spec: ArraySpecialization): string {
|
||||
if (spec.isPointer) return 'nullptr';
|
||||
if (spec.isPrimitive) {
|
||||
if (spec.cElementType === 'bool') return 'false';
|
||||
return '0';
|
||||
}
|
||||
if (spec.isEnum) return '0';
|
||||
return '0';
|
||||
}
|
||||
|
||||
private getDefaultReturn(returnType: string, spec: ArraySpecialization): string {
|
||||
if (returnType === 'bool') return 'false';
|
||||
if (returnType === 'size_t' || returnType === 'int') return '0';
|
||||
if (returnType === spec.cElementType) return this.getDefaultValue(spec);
|
||||
if (returnType === spec.cTypeName) return 'nullptr';
|
||||
return '0';
|
||||
}
|
||||
|
||||
private convertFromCpp(spec: ArraySpecialization, expr: string): string {
|
||||
if (spec.isPointer) {
|
||||
return `(${spec.cElementType}) ${expr}`;
|
||||
}
|
||||
if (spec.isEnum && spec.elementType !== 'PropertyId') {
|
||||
return `(${spec.cElementType}) ${expr}`;
|
||||
}
|
||||
return expr;
|
||||
}
|
||||
|
||||
private convertToCpp(spec: ArraySpecialization, expr: string): string {
|
||||
if (spec.isPointer) {
|
||||
return `(${spec.elementType}) ${expr}`;
|
||||
}
|
||||
if (spec.isEnum && spec.elementType !== 'PropertyId') {
|
||||
return `(${spec.elementType}) ${expr}`;
|
||||
}
|
||||
return expr;
|
||||
}
|
||||
}
|
||||
@ -1,120 +0,0 @@
|
||||
import { Type, Member, toSnakeCase, toCFunctionName, toCTypeName, Constructor, ClassOrStruct } from '../types';
|
||||
|
||||
export interface GeneratorResult {
|
||||
declarations: string[];
|
||||
implementations: string[];
|
||||
}
|
||||
|
||||
export class ConstructorGenerator {
|
||||
constructor(private validTypes: Set<string>) {}
|
||||
|
||||
generate(type: ClassOrStruct): 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
|
||||
let constructorIndex = 0;
|
||||
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: 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: 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, 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('*')) {
|
||||
callExpr = `(${param.type}) ${param.name}`;
|
||||
} else if (param.type.includes('&')) {
|
||||
// Handle reference types - need to dereference the pointer
|
||||
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,36 +0,0 @@
|
||||
import { Enum, toSnakeCase } from '../types';
|
||||
|
||||
export class EnumGenerator {
|
||||
generate(enumType: Enum): string[] {
|
||||
const lines: string[] = [];
|
||||
const enumName = `spine_${toSnakeCase(enumType.name)}`;
|
||||
|
||||
lines.push(`typedef enum ${enumName} {`);
|
||||
|
||||
if (enumType.values) {
|
||||
for (let i = 0; i < enumType.values.length; i++) {
|
||||
const value = enumType.values[i];
|
||||
// Remove redundant enum name prefix from value name if present
|
||||
let valueName = value.name;
|
||||
if (valueName.toLowerCase().startsWith(enumType.name.toLowerCase())) {
|
||||
valueName = valueName.substring(enumType.name.length);
|
||||
// Remove leading underscore if present after removing prefix
|
||||
if (valueName.startsWith('_')) {
|
||||
valueName = valueName.substring(1);
|
||||
}
|
||||
}
|
||||
const cName = `SPINE_${toSnakeCase(enumType.name).toUpperCase()}_${toSnakeCase(valueName).toUpperCase()}`;
|
||||
|
||||
if (value.value !== undefined) {
|
||||
lines.push(` ${cName} = ${value.value}${i < enumType.values.length - 1 ? ',' : ''}`);
|
||||
} else {
|
||||
lines.push(` ${cName}${i < enumType.values.length - 1 ? ',' : ''}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lines.push(`} ${enumName};`);
|
||||
|
||||
return lines;
|
||||
}
|
||||
}
|
||||
@ -1,378 +0,0 @@
|
||||
import { Type, Member, toSnakeCase, toCFunctionName, toCTypeName, Exclusion, Method, ClassOrStruct } from '../types';
|
||||
import { isMethodExcluded } from '../exclusions';
|
||||
import { GeneratorResult } from './constructor-generator';
|
||||
|
||||
export class MethodGenerator {
|
||||
constructor(private exclusions: Exclusion[], private validTypes: Set<string>) { }
|
||||
|
||||
generate(type: ClassOrStruct): GeneratorResult {
|
||||
const declarations: string[] = [];
|
||||
const implementations: string[] = [];
|
||||
|
||||
if (!type.members) return { declarations, implementations };
|
||||
|
||||
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, Method[]>();
|
||||
for (const method of methods) {
|
||||
const key = method.name + '(' + (method.parameters?.map(p => p.type).join(',') || '') + ')';
|
||||
if (!methodGroups.has(key)) {
|
||||
methodGroups.set(key, []);
|
||||
}
|
||||
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) {
|
||||
// Check if we have different return types (const vs non-const overloading)
|
||||
const returnTypes = new Set(group.map(m => m.returnType));
|
||||
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`;
|
||||
}
|
||||
error += `\nC++ pattern detected:\n`;
|
||||
error += ` T& ${group[0].name}() { return member; } // for non-const objects\n`;
|
||||
error += ` const T& ${group[0].name}() const { return member; } // for const objects\n`;
|
||||
error += `\nThis pattern provides const-correctness in C++ but cannot be represented in C.\n`;
|
||||
error += `Consider:\n`;
|
||||
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));
|
||||
console.error("CONST/NON-CONST METHOD CONFLICTS FOUND");
|
||||
console.error("=".repeat(80));
|
||||
for (const error of errors) {
|
||||
console.error(error);
|
||||
}
|
||||
console.error("=".repeat(80));
|
||||
console.error(`Total conflicts found: ${errors.length}`);
|
||||
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) {
|
||||
try {
|
||||
// Handle getters
|
||||
if (method.name.startsWith('get') && (!method.parameters || method.parameters.length === 0)) {
|
||||
const propName = method.name.substring(3);
|
||||
|
||||
// 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-Array types, generate regular getter
|
||||
const funcName = toCFunctionName(type.name, method.name);
|
||||
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(`}`);
|
||||
implementations.push('');
|
||||
}
|
||||
}
|
||||
// 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, 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(`}`);
|
||||
implementations.push('');
|
||||
}
|
||||
// Handle other methods
|
||||
else {
|
||||
// Check if this is an overloaded method
|
||||
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', 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};`);
|
||||
} else {
|
||||
implementations.push(` return ${callExpr};`);
|
||||
}
|
||||
implementations.push(`}`);
|
||||
implementations.push('');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error generating method for type ${type.name}::${method.name}:`);
|
||||
console.error(error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
return { declarations, implementations };
|
||||
|
||||
}
|
||||
|
||||
private generateCollectionAccessors(type: Type, method: Method, propName: string,
|
||||
declarations: string[], implementations: string[]) {
|
||||
const cTypeName = `spine_${toSnakeCase(type.name)}`;
|
||||
const propSnake = toSnakeCase(propName);
|
||||
const arrayMatch = method.returnType!.match(/Array<(.+?)>/);
|
||||
if (!arrayMatch) return;
|
||||
|
||||
const elementType = arrayMatch[1].trim().replace(/\s*\*$/, '');
|
||||
let cElementType: string;
|
||||
if (elementType === 'int') {
|
||||
cElementType = 'int';
|
||||
} else if (elementType === 'float') {
|
||||
cElementType = 'float';
|
||||
} else if (elementType === 'uint8_t') {
|
||||
cElementType = 'uint8_t';
|
||||
} else if (elementType === 'String') {
|
||||
cElementType = 'const char *';
|
||||
} else if (elementType === 'PropertyId') {
|
||||
cElementType = 'int64_t'; // PropertyId is just an int
|
||||
} 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 arrays
|
||||
if (method.isConst || method.returnType!.includes('const')) {
|
||||
// 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: 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') {
|
||||
call = `(const char *) ${call}.buffer()`;
|
||||
} else if (method.returnType === 'const RTTI &' || method.returnType === 'RTTI &') {
|
||||
// RTTI needs special handling - return as opaque pointer
|
||||
call = `(spine_rtti) &${call}`;
|
||||
} else if (method.returnType.includes('*')) {
|
||||
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, 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, this.validTypes);
|
||||
call = `(${cType}) &${call}`;
|
||||
} else if (this.isEnumType(method.returnType)) {
|
||||
// Cast enum return values
|
||||
const cType = toCTypeName(method.returnType, this.validTypes);
|
||||
call = `(${cType}) ${call}`;
|
||||
}
|
||||
}
|
||||
|
||||
return call;
|
||||
}
|
||||
|
||||
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('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}`;
|
||||
} else if (param.type.includes('&') && !param.type.includes('const')) {
|
||||
// Non-const reference parameters need to dereference the C pointer
|
||||
const baseType = param.type.replace(/\s*&\s*$/, '').trim();
|
||||
value = `*((${baseType}*) ${valueName})`;
|
||||
} else if (this.isEnumType(param.type)) {
|
||||
// Cast enum types
|
||||
value = `(${param.type}) ${valueName}`;
|
||||
}
|
||||
|
||||
return `${objName}->${method.name}(${value})`;
|
||||
}
|
||||
|
||||
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, 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('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}`;
|
||||
} else if (param.type.includes('&')) {
|
||||
// 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}`;
|
||||
} else {
|
||||
// Const references and non-primitive types need dereferencing
|
||||
callExpr = `*(${baseType}*) ${param.name}`;
|
||||
}
|
||||
} else if (this.isEnumType(param.type)) {
|
||||
// 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';
|
||||
if (returnType.includes('int') || returnType === 'float' || returnType === 'double' || returnType === 'size_t') return '0';
|
||||
// Check if it's an enum type (spine_* types that are not pointers)
|
||||
if (returnType.startsWith('spine_') && !returnType.includes('*')) {
|
||||
// Cast 0 to the enum type
|
||||
return `(${returnType}) 0`;
|
||||
}
|
||||
return 'nullptr';
|
||||
}
|
||||
|
||||
private isEnumType(type: string): boolean {
|
||||
// List of known enum types in spine-cpp
|
||||
const enumTypes = [
|
||||
'EventType', 'Format', 'TextureFilter', 'TextureWrap',
|
||||
'AttachmentType', 'BlendMode', 'Inherit', 'MixBlend',
|
||||
'MixDirection', 'Physics', 'PositionMode', 'Property',
|
||||
'RotateMode', 'SequenceMode', 'SpacingMode'
|
||||
];
|
||||
return enumTypes.includes(type);
|
||||
}
|
||||
|
||||
private isPrimitiveType(type: string): boolean {
|
||||
return ['int', 'float', 'double', 'bool', 'size_t', 'int32_t', 'uint32_t',
|
||||
'int16_t', 'uint16_t', 'uint8_t', 'void'].includes(type);
|
||||
}
|
||||
}
|
||||
@ -96,8 +96,8 @@ namespace spine {
|
||||
TextureWrap vWrap;
|
||||
int width, height;
|
||||
bool pma;
|
||||
int index;
|
||||
void *texture;
|
||||
int index;
|
||||
void *texture;
|
||||
|
||||
explicit AtlasPage(const String &inName) : name(inName), format(Format_RGBA8888),
|
||||
minFilter(TextureFilter_Nearest),
|
||||
@ -108,13 +108,16 @@ namespace spine {
|
||||
|
||||
class SP_API AtlasRegion : public TextureRegion {
|
||||
public:
|
||||
AtlasRegion() : TextureRegion(), page(nullptr), name(""), index(0), x(0), y(0) {}
|
||||
~AtlasRegion() {}
|
||||
|
||||
AtlasPage *page;
|
||||
String name;
|
||||
int index;
|
||||
int x, y;
|
||||
Array<int> splits;
|
||||
Array<int> pads;
|
||||
Array <String> names;
|
||||
Array<String> names;
|
||||
Array<float> values;
|
||||
};
|
||||
|
||||
@ -146,6 +149,6 @@ namespace spine {
|
||||
|
||||
void load(const char *begin, int length, const char *dir, bool createTexture);
|
||||
};
|
||||
}
|
||||
}// namespace spine
|
||||
|
||||
#endif /* Spine_Atlas_h */
|
||||
|
||||
@ -121,7 +121,7 @@ namespace spine {
|
||||
color.a = len >= 8 ? parseHex(hexString, 3) : 1.0f;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static float parseHex(const char* value, size_t index) {
|
||||
char digits[3];
|
||||
digits[0] = value[index * 2];
|
||||
|
||||
@ -41,6 +41,9 @@ namespace spine {
|
||||
class BonePose;
|
||||
class TransformConstraintPose;
|
||||
|
||||
class FromProperty;
|
||||
class ToProperty;
|
||||
|
||||
/// Source property for a TransformConstraint.
|
||||
class SP_API FromProperty : public SpineObject {
|
||||
friend class SkeletonBinary;
|
||||
@ -50,7 +53,7 @@ namespace spine {
|
||||
float _offset;
|
||||
|
||||
/// Constrained properties.
|
||||
Array<class ToProperty*> _to;
|
||||
Array<ToProperty*> _to;
|
||||
|
||||
FromProperty();
|
||||
virtual ~FromProperty();
|
||||
@ -85,66 +88,102 @@ namespace spine {
|
||||
|
||||
class SP_API FromRotate : public FromProperty {
|
||||
public:
|
||||
FromRotate() : FromProperty() {}
|
||||
~FromRotate() {}
|
||||
|
||||
float value(Skeleton &skeleton, BonePose& source, bool local, float* offsets) override;
|
||||
};
|
||||
|
||||
class SP_API ToRotate : public ToProperty {
|
||||
public:
|
||||
ToRotate() : ToProperty() {}
|
||||
~ToRotate() {}
|
||||
|
||||
float mix(TransformConstraintPose& pose) override;
|
||||
void apply(Skeleton &skeleton, TransformConstraintPose& pose, BonePose& bone, float value, bool local, bool additive) override;
|
||||
};
|
||||
|
||||
class SP_API FromX : public FromProperty {
|
||||
public:
|
||||
FromX() : FromProperty() {}
|
||||
~FromX() {}
|
||||
|
||||
float value(Skeleton &skeleton, BonePose& source, bool local, float* offsets) override;
|
||||
};
|
||||
|
||||
class SP_API ToX : public ToProperty {
|
||||
public:
|
||||
ToX() : ToProperty() {}
|
||||
~ToX() {}
|
||||
|
||||
float mix(TransformConstraintPose& pose) override;
|
||||
void apply(Skeleton &skeleton, TransformConstraintPose& pose, BonePose& bone, float value, bool local, bool additive) override;
|
||||
};
|
||||
|
||||
class SP_API FromY : public FromProperty {
|
||||
public:
|
||||
FromY() : FromProperty() {}
|
||||
~FromY() {}
|
||||
|
||||
float value(Skeleton &skeleton, BonePose& source, bool local, float* offsets) override;
|
||||
};
|
||||
|
||||
class SP_API ToY : public ToProperty {
|
||||
public:
|
||||
ToY() : ToProperty() {}
|
||||
~ToY() {}
|
||||
|
||||
float mix(TransformConstraintPose& pose) override;
|
||||
void apply(Skeleton &skeleton, TransformConstraintPose& pose, BonePose& bone, float value, bool local, bool additive) override;
|
||||
};
|
||||
|
||||
class SP_API FromScaleX : public FromProperty {
|
||||
public:
|
||||
FromScaleX() : FromProperty() {}
|
||||
~FromScaleX() {}
|
||||
|
||||
float value(Skeleton &skeleton, BonePose& source, bool local, float* offsets) override;
|
||||
};
|
||||
|
||||
class SP_API ToScaleX : public ToProperty {
|
||||
public:
|
||||
ToScaleX() : ToProperty() {}
|
||||
~ToScaleX() {}
|
||||
|
||||
float mix(TransformConstraintPose& pose) override;
|
||||
void apply(Skeleton &skeleton, TransformConstraintPose& pose, BonePose& bone, float value, bool local, bool additive) override;
|
||||
};
|
||||
|
||||
class SP_API FromScaleY : public FromProperty {
|
||||
public:
|
||||
FromScaleY() : FromProperty() {}
|
||||
~FromScaleY() {}
|
||||
|
||||
float value(Skeleton &skeleton, BonePose& source, bool local, float* offsets) override;
|
||||
};
|
||||
|
||||
class SP_API ToScaleY : public ToProperty {
|
||||
public:
|
||||
ToScaleY() : ToProperty() {}
|
||||
~ToScaleY() {}
|
||||
|
||||
float mix(TransformConstraintPose& pose) override;
|
||||
void apply(Skeleton &skeleton, TransformConstraintPose& pose, BonePose& bone, float value, bool local, bool additive) override;
|
||||
};
|
||||
|
||||
class SP_API FromShearY : public FromProperty {
|
||||
public:
|
||||
FromShearY() : FromProperty() {}
|
||||
~FromShearY() {}
|
||||
|
||||
float value(Skeleton &skeleton, BonePose& source, bool local, float* offsets) override;
|
||||
};
|
||||
|
||||
class SP_API ToShearY : public ToProperty {
|
||||
public:
|
||||
ToShearY() : ToProperty() {}
|
||||
~ToShearY() {}
|
||||
|
||||
float mix(TransformConstraintPose& pose) override;
|
||||
void apply(Skeleton &skeleton, TransformConstraintPose& pose, BonePose& bone, float value, bool local, bool additive) override;
|
||||
};
|
||||
@ -216,14 +255,14 @@ namespace spine {
|
||||
void setClamp(bool clamp);
|
||||
|
||||
/// The mapping of transform properties to other transform properties.
|
||||
Array<class FromProperty*>& getProperties();
|
||||
Array<FromProperty*>& getProperties();
|
||||
|
||||
private:
|
||||
Array<BoneData*> _bones;
|
||||
BoneData* _source;
|
||||
float _offsets[6]; // [rotation, x, y, scaleX, scaleY, shearY]
|
||||
bool _localSource, _localTarget, _additive, _clamp;
|
||||
Array<class FromProperty*> _properties;
|
||||
Array<FromProperty*> _properties;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -1,43 +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_Vertices_h
|
||||
#define Spine_Vertices_h
|
||||
|
||||
#include <spine/Array.h>
|
||||
|
||||
namespace spine {
|
||||
class SP_API Vertices : public SpineObject {
|
||||
public:
|
||||
Array <int> _bones;
|
||||
Array<float> _weights;
|
||||
};
|
||||
}
|
||||
|
||||
#endif /* Spine_Vertices_h */
|
||||
@ -73,7 +73,6 @@
|
||||
#include <spine/TransformConstraintData.h>
|
||||
#include <spine/TransformConstraintTimeline.h>
|
||||
#include <spine/TranslateTimeline.h>
|
||||
#include <spine/Vertices.h>
|
||||
#include <spine/SequenceTimeline.h>
|
||||
#include <spine/Version.h>
|
||||
#include <spine/SliderData.h>
|
||||
@ -85,29 +84,42 @@
|
||||
|
||||
using namespace spine;
|
||||
|
||||
class SP_API Vertices : public SpineObject {
|
||||
public:
|
||||
Array<int> _bones;
|
||||
Array<float> _weights;
|
||||
};
|
||||
|
||||
#define SKELETON_JSON_ERROR(root, message, value) \
|
||||
do { \
|
||||
delete skeletonData; \
|
||||
setError(root, message, value); \
|
||||
return NULL; \
|
||||
do { \
|
||||
delete skeletonData; \
|
||||
setError(root, message, value); \
|
||||
return NULL; \
|
||||
} while (0)
|
||||
|
||||
static FromProperty* fromProperty(const char* type) {
|
||||
static FromProperty *fromProperty(const char *type) {
|
||||
if (strcmp(type, "rotate") == 0) return new FromRotate();
|
||||
else if (strcmp(type, "x") == 0) return new FromX();
|
||||
else if (strcmp(type, "y") == 0) return new FromY();
|
||||
else if (strcmp(type, "scaleX") == 0) return new FromScaleX();
|
||||
else if (strcmp(type, "scaleY") == 0) return new FromScaleY();
|
||||
else if (strcmp(type, "shearY") == 0) return new FromShearY();
|
||||
else return NULL;
|
||||
else if (strcmp(type, "x") == 0)
|
||||
return new FromX();
|
||||
else if (strcmp(type, "y") == 0)
|
||||
return new FromY();
|
||||
else if (strcmp(type, "scaleX") == 0)
|
||||
return new FromScaleX();
|
||||
else if (strcmp(type, "scaleY") == 0)
|
||||
return new FromScaleY();
|
||||
else if (strcmp(type, "shearY") == 0)
|
||||
return new FromShearY();
|
||||
else
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static float propertyScale(const char* type, float scale) {
|
||||
static float propertyScale(const char *type, float scale) {
|
||||
if (strcmp(type, "x") == 0 || strcmp(type, "y") == 0) return scale;
|
||||
else return 1;
|
||||
else
|
||||
return 1;
|
||||
}
|
||||
|
||||
SkeletonJson::SkeletonJson(Atlas *atlas) : _attachmentLoader(new (__FILE__, __LINE__) AtlasAttachmentLoader(atlas)),
|
||||
SkeletonJson::SkeletonJson(Atlas *atlas) : _attachmentLoader(new(__FILE__, __LINE__) AtlasAttachmentLoader(atlas)),
|
||||
_scale(1), _ownsLoader(true) {}
|
||||
|
||||
SkeletonJson::SkeletonJson(AttachmentLoader *attachmentLoader, bool ownsLoader) : _attachmentLoader(attachmentLoader),
|
||||
@ -284,8 +296,7 @@ SkeletonData *SkeletonJson::readSkeletonData(const char *json) {
|
||||
|
||||
skeletonData->_ikConstraints.add(data);
|
||||
skeletonData->_constraints.add(data);
|
||||
}
|
||||
else if (strcmp(type, "transform") == 0) {
|
||||
} else if (strcmp(type, "transform") == 0) {
|
||||
TransformConstraintData *data = new (__FILE__, __LINE__) TransformConstraintData(name);
|
||||
data->setSkinRequired(skinRequired);
|
||||
|
||||
@ -351,7 +362,8 @@ SkeletonData *SkeletonJson::readSkeletonData(const char *json) {
|
||||
}
|
||||
|
||||
if (from->_to.size() > 0) data->_properties.add(from);
|
||||
else delete from;
|
||||
else
|
||||
delete from;
|
||||
}
|
||||
}
|
||||
|
||||
@ -372,8 +384,7 @@ SkeletonData *SkeletonJson::readSkeletonData(const char *json) {
|
||||
|
||||
skeletonData->_transformConstraints.add(data);
|
||||
skeletonData->_constraints.add(data);
|
||||
}
|
||||
else if (strcmp(type, "path") == 0) {
|
||||
} else if (strcmp(type, "path") == 0) {
|
||||
PathConstraintData *data = new (__FILE__, __LINE__) PathConstraintData(name);
|
||||
data->setSkinRequired(skinRequired);
|
||||
|
||||
@ -404,8 +415,7 @@ SkeletonData *SkeletonJson::readSkeletonData(const char *json) {
|
||||
|
||||
skeletonData->_pathConstraints.add(data);
|
||||
skeletonData->_constraints.add(data);
|
||||
}
|
||||
else if (strcmp(type, "physics") == 0) {
|
||||
} else if (strcmp(type, "physics") == 0) {
|
||||
PhysicsConstraintData *data = new (__FILE__, __LINE__) PhysicsConstraintData(name);
|
||||
data->setSkinRequired(skinRequired);
|
||||
|
||||
@ -438,8 +448,7 @@ SkeletonData *SkeletonJson::readSkeletonData(const char *json) {
|
||||
|
||||
skeletonData->_physicsConstraints.add(data);
|
||||
skeletonData->_constraints.add(data);
|
||||
}
|
||||
else if (strcmp(type, "slider") == 0) {
|
||||
} else if (strcmp(type, "slider") == 0) {
|
||||
SliderData *data = new (__FILE__, __LINE__) SliderData(name);
|
||||
data->setSkinRequired(skinRequired);
|
||||
data->_additive = Json::getBoolean(constraintMap, "additive", false);
|
||||
@ -529,7 +538,8 @@ SkeletonData *SkeletonJson::readSkeletonData(const char *json) {
|
||||
for (Json *entry = slotEntry->_child; entry; entry = entry->_next) {
|
||||
Attachment *attachment = readAttachment(entry, skin, slot->getIndex(), entry->_name, skeletonData);
|
||||
if (attachment) skin->setAttachment(slot->getIndex(), entry->_name, attachment);
|
||||
else SKELETON_JSON_ERROR(root, "Error reading attachment: ", entry->_name);
|
||||
else
|
||||
SKELETON_JSON_ERROR(root, "Error reading attachment: ", entry->_name);
|
||||
}
|
||||
}
|
||||
|
||||
@ -550,7 +560,7 @@ SkeletonData *SkeletonJson::readSkeletonData(const char *json) {
|
||||
Attachment *parent = skin->getAttachment(linkedMesh->_slotIndex, linkedMesh->_parent);
|
||||
if (parent == NULL) SKELETON_JSON_ERROR(root, "Parent mesh not found: ", linkedMesh->_parent.buffer());
|
||||
linkedMesh->_mesh->_timelineAttachment = linkedMesh->_inheritTimelines ? static_cast<VertexAttachment *>(parent)
|
||||
: linkedMesh->_mesh;
|
||||
: linkedMesh->_mesh;
|
||||
linkedMesh->_mesh->setParentMesh(static_cast<MeshAttachment *>(parent));
|
||||
if (linkedMesh->_mesh->_region != NULL) linkedMesh->_mesh->updateRegion();
|
||||
}
|
||||
@ -694,7 +704,7 @@ Attachment *SkeletonJson::readAttachment(Json *map, Skin *skin, int slotIndex, c
|
||||
readVertices(map, path, vertexCount << 1);
|
||||
|
||||
if (!Json::asArray(Json::getItem(map, "lengths"), path->_lengths)) return NULL;
|
||||
for (int i = 0; i < (int)path->_lengths.size(); i++)
|
||||
for (int i = 0; i < (int) path->_lengths.size(); i++)
|
||||
path->_lengths[i] *= scale;
|
||||
|
||||
const char *color = Json::getString(map, "color", NULL);
|
||||
@ -749,7 +759,7 @@ void SkeletonJson::readVertices(Json *map, VertexAttachment *attachment, size_t
|
||||
if (!Json::asArray(Json::getItem(map, "vertices"), vertices)) return;
|
||||
if (verticesLength == vertices.size()) {
|
||||
if (_scale != 1) {
|
||||
for (int i = 0; i < (int)vertices.size(); ++i)
|
||||
for (int i = 0; i < (int) vertices.size(); ++i)
|
||||
vertices[i] *= _scale;
|
||||
}
|
||||
attachment->getVertices().clearAndAddAll(vertices);
|
||||
@ -824,7 +834,7 @@ Animation *SkeletonJson::readAnimation(Json *map, SkeletonData *skeletonData) {
|
||||
} else if (strcmp(timelineMap->_name, "rgb") == 0) {
|
||||
RGBTimeline *timeline = new (__FILE__, __LINE__) RGBTimeline(frames, frames * 3, slotIndex);
|
||||
float time = Json::getFloat(keyMap, "time", 0);
|
||||
const char* colorStr = Json::getString(keyMap, "color", 0);
|
||||
const char *colorStr = Json::getString(keyMap, "color", 0);
|
||||
Color color;
|
||||
if (colorStr && strlen(colorStr) >= 6) {
|
||||
color.r = Color::parseHex(colorStr, 0);
|
||||
@ -839,7 +849,7 @@ Animation *SkeletonJson::readAnimation(Json *map, SkeletonData *skeletonData) {
|
||||
break;
|
||||
}
|
||||
float time2 = Json::getFloat(nextMap, "time", 0);
|
||||
const char* colorStr2 = Json::getString(nextMap, "color", 0);
|
||||
const char *colorStr2 = Json::getString(nextMap, "color", 0);
|
||||
Color newColor;
|
||||
if (colorStr2 && strlen(colorStr2) >= 6) {
|
||||
newColor.r = Color::parseHex(colorStr2, 0);
|
||||
@ -866,7 +876,7 @@ Animation *SkeletonJson::readAnimation(Json *map, SkeletonData *skeletonData) {
|
||||
float time = Json::getFloat(keyMap, "time", 0);
|
||||
Color color, color2;
|
||||
Color::valueOf(Json::getString(keyMap, "light", 0), color);
|
||||
const char* darkStr = Json::getString(keyMap, "dark", 0);
|
||||
const char *darkStr = Json::getString(keyMap, "dark", 0);
|
||||
if (darkStr && strlen(darkStr) >= 6) {
|
||||
color2.r = Color::parseHex(darkStr, 0);
|
||||
color2.g = Color::parseHex(darkStr, 1);
|
||||
@ -882,7 +892,7 @@ Animation *SkeletonJson::readAnimation(Json *map, SkeletonData *skeletonData) {
|
||||
float time2 = Json::getFloat(nextMap, "time", 0);
|
||||
Color newColor, newColor2;
|
||||
Color::valueOf(Json::getString(nextMap, "light", 0), newColor);
|
||||
const char* darkStr2 = Json::getString(nextMap, "dark", 0);
|
||||
const char *darkStr2 = Json::getString(nextMap, "dark", 0);
|
||||
if (darkStr2 && strlen(darkStr2) >= 6) {
|
||||
newColor2.r = Color::parseHex(darkStr2, 0);
|
||||
newColor2.g = Color::parseHex(darkStr2, 1);
|
||||
@ -907,14 +917,14 @@ Animation *SkeletonJson::readAnimation(Json *map, SkeletonData *skeletonData) {
|
||||
} else if (strcmp(timelineMap->_name, "rgb2") == 0) {
|
||||
RGB2Timeline *timeline = new (__FILE__, __LINE__) RGB2Timeline(frames, frames * 6, slotIndex);
|
||||
float time = Json::getFloat(keyMap, "time", 0);
|
||||
const char* lightStr = Json::getString(keyMap, "light", 0);
|
||||
const char *lightStr = Json::getString(keyMap, "light", 0);
|
||||
Color color, color2;
|
||||
if (lightStr && strlen(lightStr) >= 6) {
|
||||
color.r = Color::parseHex(lightStr, 0);
|
||||
color.g = Color::parseHex(lightStr, 1);
|
||||
color.b = Color::parseHex(lightStr, 2);
|
||||
}
|
||||
const char* darkStr = Json::getString(keyMap, "dark", 0);
|
||||
const char *darkStr = Json::getString(keyMap, "dark", 0);
|
||||
if (darkStr && strlen(darkStr) >= 6) {
|
||||
color2.r = Color::parseHex(darkStr, 0);
|
||||
color2.g = Color::parseHex(darkStr, 1);
|
||||
@ -928,14 +938,14 @@ Animation *SkeletonJson::readAnimation(Json *map, SkeletonData *skeletonData) {
|
||||
break;
|
||||
}
|
||||
float time2 = Json::getFloat(nextMap, "time", 0);
|
||||
const char* lightStr2 = Json::getString(nextMap, "light", 0);
|
||||
const char *lightStr2 = Json::getString(nextMap, "light", 0);
|
||||
Color newColor, newColor2;
|
||||
if (lightStr2 && strlen(lightStr2) >= 6) {
|
||||
newColor.r = Color::parseHex(lightStr2, 0);
|
||||
newColor.g = Color::parseHex(lightStr2, 1);
|
||||
newColor.b = Color::parseHex(lightStr2, 2);
|
||||
}
|
||||
const char* darkStr2 = Json::getString(nextMap, "dark", 0);
|
||||
const char *darkStr2 = Json::getString(nextMap, "dark", 0);
|
||||
if (darkStr2 && strlen(darkStr2) >= 6) {
|
||||
newColor2.r = Color::parseHex(darkStr2, 0);
|
||||
newColor2.g = Color::parseHex(darkStr2, 1);
|
||||
@ -1029,7 +1039,7 @@ Animation *SkeletonJson::readAnimation(Json *map, SkeletonData *skeletonData) {
|
||||
float mix = Json::getFloat(keyMap, "mix", 1), softness = Json::getFloat(keyMap, "softness", 0) * _scale;
|
||||
for (int frame = 0, bezier = 0;; frame++) {
|
||||
timeline->setFrame(frame, time, mix, softness, Json::getBoolean(keyMap, "bendPositive", true) ? 1 : -1,
|
||||
Json::getBoolean(keyMap, "compress", false), Json::getBoolean(keyMap, "stretch", false));
|
||||
Json::getBoolean(keyMap, "compress", false), Json::getBoolean(keyMap, "stretch", false));
|
||||
Json *nextMap = keyMap->_next;
|
||||
if (!nextMap) {
|
||||
break;
|
||||
@ -1123,15 +1133,15 @@ Animation *SkeletonJson::readAnimation(Json *map, SkeletonData *skeletonData) {
|
||||
readTimeline(timelines, keyMap, timeline, 0, constraint->_positionMode == PositionMode_Fixed ? _scale : 1);
|
||||
} else if (strcmp(timelineName, "spacing") == 0) {
|
||||
CurveTimeline1 *timeline = new (__FILE__, __LINE__) PathConstraintSpacingTimeline(frames, frames,
|
||||
index);
|
||||
index);
|
||||
readTimeline(timelines, keyMap, timeline, 0,
|
||||
constraint->_spacingMode == SpacingMode_Length ||
|
||||
constraint->_spacingMode == SpacingMode_Fixed
|
||||
? _scale
|
||||
: 1);
|
||||
constraint->_spacingMode == SpacingMode_Fixed
|
||||
? _scale
|
||||
: 1);
|
||||
} else if (strcmp(timelineName, "mix") == 0) {
|
||||
PathConstraintMixTimeline *timeline = new (__FILE__, __LINE__) PathConstraintMixTimeline(frames,
|
||||
frames * 3, index);
|
||||
frames * 3, index);
|
||||
float time = Json::getFloat(keyMap, "time", 0);
|
||||
float mixRotate = Json::getFloat(keyMap, "mixRotate", 1);
|
||||
float mixX = Json::getFloat(keyMap, "mixX", 1);
|
||||
@ -1307,7 +1317,7 @@ Animation *SkeletonJson::readAnimation(Json *map, SkeletonData *skeletonData) {
|
||||
for (int frame = 0; keyMap != NULL; keyMap = keyMap->_next, frame++) {
|
||||
float delay = Json::getFloat(keyMap, "delay", lastDelay);
|
||||
timeline->setFrame(frame, Json::getFloat(keyMap, "time", 0),
|
||||
SequenceMode_valueOf(Json::getString(keyMap, "mode", "hold")), Json::getInt(keyMap, "index", 0), delay);
|
||||
SequenceMode_valueOf(Json::getString(keyMap, "mode", "hold")), Json::getInt(keyMap, "index", 0), delay);
|
||||
lastDelay = delay;
|
||||
}
|
||||
timelines.add(timeline);
|
||||
@ -1408,7 +1418,7 @@ void SkeletonJson::readTimeline(Array<Timeline *> &timelines, Json *keyMap, Curv
|
||||
}
|
||||
|
||||
void SkeletonJson::readTimeline(Array<Timeline *> &timelines, Json *keyMap, CurveTimeline2 *timeline, const char *name1, const char *name2,
|
||||
float defaultValue, float scale) {
|
||||
float defaultValue, float scale) {
|
||||
float time = Json::getFloat(keyMap, "time", 0);
|
||||
float value1 = Json::getFloat(keyMap, name1, defaultValue) * scale, value2 = Json::getFloat(keyMap, name2, defaultValue) * scale;
|
||||
for (int frame = 0, bezier = 0;; frame++) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user