Migrate spine-plugin into monorepo with custom SpineBatcher#1331
Migrate spine-plugin into monorepo with custom SpineBatcher#1331
Conversation
There was a problem hiding this comment.
Pull request overview
Migrates @melonjs/spine-plugin into the monorepo and aligns its build/tooling with the existing plugin packages (esbuild + shared tsconfig), while bringing over the plugin source and demo assets.
Changes:
- Add
packages/spine-pluginpackage with esbuild-based build + clean scripts and TS configs for declaration generation. - Import Spine plugin source (
src/) and demo/test harness (test/) including sample Spine assets. - Update
pnpm-lock.yamlto include the new workspace package and Spine runtime dependencies.
Reviewed changes
Copilot reviewed 38 out of 61 changed files in this pull request and generated 12 comments.
Show a summary per file
| File | Description |
|---|---|
| pnpm-lock.yaml | Adds spine-plugin workspace importer entries and Spine runtime package resolutions. |
| packages/spine-plugin/tsconfig.json | Package TS config extending shared base; noEmit for local typechecking. |
| packages/spine-plugin/tsconfig.build.json | TS build config for emitting declarations into build/. |
| packages/spine-plugin/scripts/clean.ts | Build artifact cleanup (removes build/). |
| packages/spine-plugin/scripts/build.ts | esbuild bundling entrypoint for the plugin. |
| packages/spine-plugin/package.json | Defines package metadata, exports/types paths, deps, and build scripts. |
| packages/spine-plugin/src/index.js | Main Spine renderable implementation + WebGL/Canvas rendering paths. |
| packages/spine-plugin/src/SpinePlugin.js | Plugin registration entry (asset manager + startup logging). |
| packages/spine-plugin/src/SkeletonRenderer.js | Canvas renderer implementation for Spine skeletons (with clipping/mesh support). |
| packages/spine-plugin/src/AssetManager.js | Integrates Spine asset loading into melonJS loader via a custom parser. |
| packages/spine-plugin/test/index.css | Styles for the demo pages. |
| packages/spine-plugin/test/js/index.js | Demo bootstrap (video init, plugin registration, preload, scene setup). |
| packages/spine-plugin/test/js/manifest.js | Demo asset manifest for loader preload. |
| packages/spine-plugin/test/js/renderables/spine.js | Demo renderable that wraps Spine and adds basic input/body behavior. |
| packages/spine-plugin/test/alien.html | Demo page for alien skeleton. |
| packages/spine-plugin/test/coin.html | Demo page for coin skeleton. |
| packages/spine-plugin/test/dragon.html | Demo page for dragon skeleton. |
| packages/spine-plugin/test/mix-and-match.html | Demo page for mix-and-match skeleton. |
| packages/spine-plugin/test/owl.html | Demo page for owl skeleton. |
| packages/spine-plugin/test/spineboy.html | Demo page for spineboy skeleton. |
| packages/spine-plugin/test/stretchyman.html | Demo page for stretchyman skeleton. |
| packages/spine-plugin/test/data/spine/alien.atlas | Demo Spine atlas data. |
| packages/spine-plugin/test/data/spine/coin-pma.atlas | Demo Spine atlas data (PMA). |
| packages/spine-plugin/test/data/spine/coin-pro.json | Demo Spine skeleton JSON. |
| packages/spine-plugin/test/data/spine/coin-pro.skel | Demo Spine skeleton binary. |
| packages/spine-plugin/test/data/spine/dragon-ess.json | Demo Spine skeleton JSON. |
| packages/spine-plugin/test/data/spine/dragon-ess.skel | Demo Spine skeleton binary. |
| packages/spine-plugin/test/data/spine/dragon-pma.atlas | Demo Spine atlas data (PMA, multi-page). |
| packages/spine-plugin/test/data/spine/dragon-pma_2.png | Demo texture image. |
| packages/spine-plugin/test/data/spine/mix-and-match-pma.atlas | Demo Spine atlas data (PMA). |
| packages/spine-plugin/test/data/spine/owl-pma.atlas | Demo Spine atlas data (PMA). |
| packages/spine-plugin/test/data/spine/owl-pro.skel | Demo Spine skeleton binary. |
| packages/spine-plugin/test/data/spine/owl.atlas | Demo Spine atlas data. |
| packages/spine-plugin/test/data/spine/spineboy-pma.atlas | Demo Spine atlas data (PMA). |
| packages/spine-plugin/test/data/spine/stretchyman-pma.atlas | Demo Spine atlas data (PMA). |
| packages/spine-plugin/test/data/spine/stretchyman.atlas | Demo Spine atlas data. |
| packages/spine-plugin/README.md | Package README brought into monorepo. |
| packages/spine-plugin/LICENSE | Package license file. |
| packages/spine-plugin/CHANGELOG.md | Package changelog brought into monorepo. |
| packages/spine-plugin/.gitignore | Ignores build/ and other package-local artifacts. |
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| "melonjs": "https://cdn.jsdelivr.net/npm/melonjs@15.12/+esm", | ||
| "debugPlugin" : "https://cdn.jsdelivr.net/npm/@melonjs/debug-plugin/dist/@melonjs/debug-plugin.js", | ||
| "spinePlugin" : "./../dist/@melonjs/spine-plugin.js" | ||
| } |
There was a problem hiding this comment.
These test HTML files import the spine plugin from ./../dist/@melonjs/spine-plugin.js, but this package’s build output is now configured to emit into build/ (and dist/ is not present). Update the import map to reference the correct built file (e.g. ../build/index.js) or a published CDN path that matches the new output layout.
| "melonjs": "https://cdn.jsdelivr.net/npm/melonjs@15.12/+esm", | ||
| "debugPlugin" : "https://cdn.jsdelivr.net/npm/@melonjs/debug-plugin/dist/@melonjs/debug-plugin.js", | ||
| "spinePlugin" : "./../dist/@melonjs/spine-plugin.js" | ||
| } |
There was a problem hiding this comment.
These test HTML files import the spine plugin from ./../dist/@melonjs/spine-plugin.js, but this package’s build output is now configured to emit into build/ (and dist/ is not present). Update the import map to reference the correct built file (e.g. ../build/index.js) or a published CDN path that matches the new output layout.
packages/spine-plugin/test/owl.html
Outdated
| "melonjs": "https://cdn.jsdelivr.net/npm/melonjs@15.12/+esm", | ||
| "debugPlugin" : "https://cdn.jsdelivr.net/npm/@melonjs/debug-plugin/dist/@melonjs/debug-plugin.js", | ||
| "spinePlugin" : "./../dist/@melonjs/spine-plugin.js" | ||
| } |
There was a problem hiding this comment.
These test HTML files import the spine plugin from ./../dist/@melonjs/spine-plugin.js, but this package’s build output is now configured to emit into build/ (and dist/ is not present). Update the import map to reference the correct built file (e.g. ../build/index.js) or a published CDN path that matches the new output layout.
| "melonjs": "https://cdn.jsdelivr.net/npm/melonjs@15.12/+esm", | ||
| "debugPlugin" : "https://cdn.jsdelivr.net/npm/@melonjs/debug-plugin/dist/@melonjs/debug-plugin.js", | ||
| "spinePlugin" : "./../dist/@melonjs/spine-plugin.js" | ||
| } |
There was a problem hiding this comment.
These test HTML files import the spine plugin from ./../dist/@melonjs/spine-plugin.js, but this package’s build output is now configured to emit into build/ (and dist/ is not present). Update the import map to reference the correct built file (e.g. ../build/index.js) or a published CDN path that matches the new output layout.
| @@ -0,0 +1,101 @@ | |||
| // a melonJS data manifest | |||
| // note : this is note a webpack manifest | |||
There was a problem hiding this comment.
Typo in comment: "this is note a webpack manifest" should be "this is not a webpack manifest".
packages/spine-plugin/src/index.js
Outdated
| * @param {number} [settings.atlasFile] - the name of the atlasFile to be used to create this spine animation | ||
| * @param {number} [settings.jsonFile] - the name of the atlasFile to be used to create this spine animation |
There was a problem hiding this comment.
JSDoc types for settings.atlasFile and settings.jsonFile are listed as number, but they are used as filenames/keys (strings) throughout the implementation. Please correct the types in the docstring to avoid misleading IDE hints and generated docs.
| * @param {number} [settings.atlasFile] - the name of the atlasFile to be used to create this spine animation | |
| * @param {number} [settings.jsonFile] - the name of the atlasFile to be used to create this spine animation | |
| * @param {string} [settings.atlasFile] - the name of the atlasFile to be used to create this spine animation | |
| * @param {string} [settings.jsonFile] - the name of the atlasFile to be used to create this spine animation |
| "imports": { | ||
| "melonjs": "https://cdn.jsdelivr.net/npm/melonjs@15.12/+esm", | ||
| "debugPlugin" : "https://cdn.jsdelivr.net/npm/@melonjs/debug-plugin/dist/@melonjs/debug-plugin.js", | ||
| "spinePlugin" : "./../dist/@melonjs/spine-plugin.js" |
There was a problem hiding this comment.
These test HTML files import the spine plugin from ./../dist/@melonjs/spine-plugin.js, but this package’s build output is now configured to emit into build/ (and dist/ is not present). Update the import map to reference the correct built file (e.g. ../build/index.js) or a published CDN path that matches the new output layout.
| "spinePlugin" : "./../dist/@melonjs/spine-plugin.js" | |
| "spinePlugin" : "../build/index.js" |
packages/spine-plugin/test/coin.html
Outdated
| "imports": { | ||
| "melonjs": "https://cdn.jsdelivr.net/npm/melonjs@15.12/+esm", | ||
| "debugPlugin" : "https://cdn.jsdelivr.net/npm/@melonjs/debug-plugin/dist/@melonjs/debug-plugin.js", | ||
| "spinePlugin" : "./../dist/@melonjs/spine-plugin.js" |
There was a problem hiding this comment.
These test HTML files import the spine plugin from ./../dist/@melonjs/spine-plugin.js, but this package’s build output is now configured to emit into build/ (and dist/ is not present). Update the import map to reference the correct built file (e.g. ../build/index.js) or a published CDN path that matches the new output layout.
| "spinePlugin" : "./../dist/@melonjs/spine-plugin.js" | |
| "spinePlugin" : "../build/index.js" |
| "melonjs": "https://cdn.jsdelivr.net/npm/melonjs@15.12/+esm", | ||
| "debugPlugin" : "https://cdn.jsdelivr.net/npm/@melonjs/debug-plugin/dist/@melonjs/debug-plugin.js", | ||
| "spinePlugin" : "./../dist/@melonjs/spine-plugin.js" | ||
| } |
There was a problem hiding this comment.
These test HTML files import the spine plugin from ./../dist/@melonjs/spine-plugin.js, but this package’s build output is now configured to emit into build/ (and dist/ is not present). Update the import map to reference the correct built file (e.g. ../build/index.js) or a published CDN path that matches the new output layout.
packages/spine-plugin/src/index.js
Outdated
| // ensure plugin was properly registered | ||
| this.plugin = plugin.get(SpinePlugin); | ||
| if (typeof this.plugin === "undefined") { | ||
| throw "Spine plugin: plugin needs to be registered first using plugin.register"; |
There was a problem hiding this comment.
Throwing a string makes stack traces and error handling harder (e.g., instanceof Error checks). Please throw an Error (or a project-standard error type) with this message instead.
| throw "Spine plugin: plugin needs to be registered first using plugin.register"; | |
| throw new Error("Spine plugin: plugin needs to be registered first using plugin.register"); |
- Support optional indexed drawing via settings.indexed with own VBO and dynamic IndexBuffer (addIndices, flush with drawElements) - Support configurable maxVertices via settings.maxVertices (default 4096) - Support configurable projection uniform name via settings.projectionUniform (default "uProjectionMatrix") for third-party shaders - IndexBuffer: refactored to support both static quad patterns (fillQuadPattern) and dynamic stream drawing (add/upload/clear) - VertexArrayBuffer: new pushFloats() for variable-length all-float vertex data - QuadBatcher: updated to use refactored IndexBuffer API Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
SpineBatcher:
- Custom melonJS Batcher using Spine's official two-color shader
- Indexed drawing via melonJS Batcher's new indexed mode
- Blend modes delegated to melonJS renderer.setBlendMode() with PMA
- No manual GL state management — integrates via setBatcher("spine")
Spine renderable:
- skeleton.update() for Spine 4.2+ physics support
- Physics gravity inversion for Y-down coordinate system
- Auto-detect mesh attachments for canvas triangleRendering optimization
- New API: setCombinedSkin, setEmptyAnimation, findBone, findSlot,
addAnimationListener, getAnimationNames, getSkinNames
- Fixed rotate/scale/dispose, removed redundant methods
Canvas SkeletonRenderer:
- Extracted drawRegion/drawMesh/drawTriangle from monolithic draw()
- Degenerate triangle guard, UV edge bleeding fix
- triangleRendering auto-detection (fast path for region-only skeletons)
AssetManager:
- Cleaned up naming, added dispose(), fixed JSDoc
Examples:
- 17 official Spine characters with character selector dropdown
- Debug plugin visible by default
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1eea087 to
dd83678
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 56 out of 107 changed files in this pull request and generated 5 comments.
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| esbuild: | ||
| specifier: 0.27.4 | ||
| version: 0.27.4 |
There was a problem hiding this comment.
In this lockfile importer entry, the esbuild specifier is 0.27.4, but packages/spine-plugin/package.json declares esbuild as ^0.27.3. The lockfile should reflect the package.json specifier; please update the dependency specifier (or regenerate the lockfile) so they stay consistent.
| "peerDependencies": { | ||
| "melonjs": ">=18.1.0" | ||
| }, |
There was a problem hiding this comment.
peerDependencies.melonjs is set to >=18.1.0, but the README and CHANGELOG in this package state the minimum melonJS version is 18.2.0. This should be aligned so consumers get the correct install-time signal (and so SpinePlugin.version reports the correct minimum).
| default: | ||
| throw new Error( | ||
| `Spine plugin: unknown extension "${ext}" when preloading spine assets`, | ||
| ); | ||
| } |
There was a problem hiding this comment.
The custom loader parser throws an Error for unknown extensions. loader.preload expects parsers to report failures via the provided onerror callback (see loader.setParser docs); throwing here bypasses the loader's failure bookkeeping and can break preload flow. Prefer calling onerror (and returning 0/1 as appropriate) instead of throwing.
| /** | ||
| * Disposes of all rendering-related resources to free GPU memory. | ||
| * Should be called when this Spine object is no longer needed. | ||
| */ | ||
| dispose() { | ||
| if (this.renderer.WebGLVersion >= 1) { | ||
| this.shapes.dispose(); | ||
| this.shapesShader.dispose(); | ||
| this.skeletonDebugRenderer.dispose(); | ||
| } | ||
| } |
There was a problem hiding this comment.
Spine allocates WebGL resources (shapes, shapesShader, skeletonDebugRenderer) but dispose() is never called automatically when the renderable is removed from the world. Renderable.destroy() calls onDestroyEvent, not dispose(), so removing a Spine instance will leak GPU resources unless users remember to call dispose() manually. Consider moving the cleanup into onDestroyEvent() (or overriding destroy()) and keeping dispose() as an alias if you want a public API.
| * @param {WebGL2RenderingContext} gl - the WebGL context | ||
| * @param {number} maxQuads - maximum number of quads this buffer can index | ||
| * @param {number} maxIndices - maximum number of indices this buffer can hold | ||
| * @param {boolean} [useUint32=false] - use Uint32 indices (WebGL2) instead of Uint16 (WebGL1) | ||
| * @param {boolean} [dynamic=false] - if true, use STREAM_DRAW for frequent updates; if false, use STATIC_DRAW | ||
| */ |
There was a problem hiding this comment.
The IndexBuffer constructor JSDoc types gl as WebGL2RenderingContext, but this class is also instantiated with WebGL1 contexts (e.g. QuadBatcher). Updating the type to WebGLRenderingContext|WebGL2RenderingContext would keep the documentation accurate.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Align peerDependencies melonjs to >=18.2.0 (matches README/CHANGELOG) - AssetManager: use onerror callback instead of throw for unknown extensions - Spine: add onDestroyEvent() for automatic GPU resource cleanup on removal - IndexBuffer: fix JSDoc gl type to include WebGLRenderingContext Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 56 out of 107 changed files in this pull request and generated 8 comments.
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if (worldVertices.length < mesh.worldVerticesLength) { | ||
| worldVertices = new Float32Array(mesh.worldVerticesLength); | ||
| } | ||
| mesh.computeWorldVertices( | ||
| slot, | ||
| 0, | ||
| mesh.worldVerticesLength, | ||
| worldVertices, | ||
| 0, | ||
| vertexSize, | ||
| ); |
There was a problem hiding this comment.
worldVertices is grown to mesh.worldVerticesLength, but when calling mesh.computeWorldVertices(..., stride=vertexSize) the destination array needs capacity for vertexCount * vertexSize floats (not vertexCount * 2). With VERTEX_SIZE = 8, the current allocation can be too small and typed-array writes past the end will be dropped, producing corrupted meshes. Allocate based on (mesh.worldVerticesLength / 2) * vertexSize (or similar) and keep it in sync with the stride used.
| if (this.useIndexBuffer) { | ||
| const gl = this.gl; | ||
| this.glVertexBuffer = gl.createBuffer(); | ||
| // max indices: worst case is 3 indices per vertex (all triangles, no sharing) | ||
| this.indexBuffer = new IndexBuffer(gl, maxVertices * 3, false, true); | ||
| } |
There was a problem hiding this comment.
Indexed batchers allocate the dynamic index buffer as maxVertices * 3 with a comment claiming this is the “worst case”. For triangle meshes, index count can legitimately exceed 3 * vertexCount (e.g. shared vertices across many triangles), and there’s no guard if a single indices array exceeds the buffer capacity, which can cause silent out-of-bounds drops in IndexBuffer.add(). Consider (1) making max indices configurable (e.g. settings.maxIndices), (2) using a more conservative default (often maxVertices * 6), and/or (3) adding an explicit capacity check that flushes and/or throws when a single submission can’t fit.
| add(indices, vertexOffset) { | ||
| for (let i = 0; i < indices.length; i++) { | ||
| this.data[this.length + i] = indices[i] + vertexOffset; | ||
| } | ||
| this.length += indices.length; | ||
| } |
There was a problem hiding this comment.
IndexBuffer.add() appends without checking remaining capacity (this.length + indices.length vs this.data.length). When it overflows, typed-array writes past the end are silently ignored, leading to incorrect drawElements output with no obvious error. Add a bounds check and either flush/return false or throw a clear error so callers can recover safely.
| esbuild: | ||
| specifier: 0.27.4 | ||
| version: 0.27.4 | ||
| melonjs: | ||
| specifier: workspace:* | ||
| version: link:../melonjs | ||
| tsconfig: | ||
| specifier: workspace:* | ||
| version: link:../tsconfig | ||
| tsx: | ||
| specifier: ^4.21.0 | ||
| version: 4.21.0 | ||
| typescript: | ||
| specifier: ^5.9.3 | ||
| version: 5.9.3 |
There was a problem hiding this comment.
The lockfile records packages/spine-plugin devDependency esbuild with specifier 0.27.4, but packages/spine-plugin/package.json declares ^0.27.3. This indicates the lockfile is out of sync with the manifests (or vice versa) and will cause a diff on the next pnpm install. Please regenerate the lockfile or align the specifier in package.json.
| console.log( | ||
| `${name} ${version} - spine runtime ${dependencies["@esotericsoftware/spine-core"]} | ${homepage}`, | ||
| ); |
There was a problem hiding this comment.
Plugin log format is inconsistent with the other melonJS plugins in this repo (e.g. debug-plugin uses ${name} ${version} | ${homepage}). Consider matching that convention (and optionally including the spine runtime version after, but keep the same separators) so logs are uniform across plugins.
| constructor(x, y, settings) { | ||
| super(x, y, settings.width, settings.height); | ||
|
|
||
| // ensure plugin was properly registered |
There was a problem hiding this comment.
Spine calls super(x, y, settings.width, settings.height) but settings is optional in your own JSDoc examples (and in the monorepo example you don’t provide width/height). With settings undefined or missing width/height, Rect/Renderable will end up with invalid dimensions (or throw when accessing settings.width). Consider defaulting settings = {} and passing safe numeric dimensions (e.g. 0/0 or 1/1) until the skeleton is loaded, then let updateBounds() size it from skeleton.getBounds().
| // create new spine renderable | ||
| const spineObj = new Spine(char.x, char.y, { | ||
| atlasFile: char.atlas, | ||
| jsonFile: char.json, | ||
| }); |
There was a problem hiding this comment.
This example constructs new Spine(char.x, char.y, { atlasFile, jsonFile }) without width/height, but Spine currently forwards settings.width/height into Renderable/Rect. As written, this will produce invalid bounds (or crash) unless Spine defaults width/height internally.
| const clippedVertexSize = clipper.isClipping() ? 2 : VERTEX_SIZE; | ||
| const slot = drawOrder[i]; | ||
| const bone = slot.bone; | ||
| let image; | ||
| let region; | ||
| let triangles; | ||
|
|
||
| if (!bone.active) { | ||
| clipper.clipEndWithSlot(slot); | ||
| renderer.clearMask(); | ||
| continue; | ||
| } | ||
|
|
||
| const attachment = slot.getAttachment(); | ||
|
|
||
| if (attachment instanceof RegionAttachment) { | ||
| region = attachment.region; | ||
| image = region.texture.getImage(); | ||
| } else if ( | ||
| this.triangleRendering && | ||
| attachment instanceof MeshAttachment | ||
| ) { | ||
| this.computeMeshVertices(slot, attachment, clippedVertexSize); | ||
| triangles = attachment.triangles; | ||
| region = attachment.region; | ||
| image = region.texture.getImage(); | ||
| } else if (attachment instanceof ClippingAttachment) { |
There was a problem hiding this comment.
clippedVertexSize becomes 2 while clipping is active, but mesh rendering still calls drawMesh() which indexes worldVertices assuming the full VERTEX_SIZE layout (e.g. t1 + 6 for UVs). If a mesh attachment is drawn while clipping is active, computeMeshVertices(..., 2) will generate an incompatible buffer layout. Meshes should always be computed with the full vertex stride (and clipping should be applied separately via masking/clipper).
Summary
Brings the
@melonjs/spine-pluginfrom its standalone repo into the monorepo, fully revamped with proper melonJS batcher integration.melonJS Batcher improvements
Batcher: configurablemaxVertices, optional indexed drawing (settings.indexed), configurableprojectionUniformIndexBuffer: refactored for both static quad patterns and dynamic stream drawingVertexArrayBuffer: newpushFloats()for variable-length all-float vertex dataSpine plugin revamp
Batcherusing Spine's official two-color shader, indexeddrawElements, blend modes viarenderer.setBlendMode()drawRegion/drawMesh/drawTriangle, auto-detect mesh attachments for fast path, degenerate triangle guard, UV edge bleeding fixsetCombinedSkin,setEmptyAnimation,findBone,findSlot,addAnimationListener,getAnimationNames,getSkinNamesskeleton.update()for Spine 4.2+Examples
Test plan
🤖 Generated with Claude Code