@codexo/exojs 0.12.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +125 -0
- package/dist/esm/core/BuildInfo.js +2 -2
- package/dist/esm/extensions/Extension.d.ts +39 -7
- package/dist/esm/extensions/Extension.d.ts.map +1 -1
- package/dist/esm/extensions/ExtensionRegistry.d.ts.map +1 -1
- package/dist/esm/extensions/ExtensionRegistry.js.map +1 -1
- package/dist/esm/extensions/snapshot.d.ts +12 -2
- package/dist/esm/extensions/snapshot.d.ts.map +1 -1
- package/dist/esm/extensions/snapshot.js +43 -14
- package/dist/esm/extensions/snapshot.js.map +1 -1
- package/dist/esm/index.js +8 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/rendering/Drawable.d.ts +23 -0
- package/dist/esm/rendering/Drawable.d.ts.map +1 -1
- package/dist/esm/rendering/Drawable.js +34 -0
- package/dist/esm/rendering/Drawable.js.map +1 -1
- package/dist/esm/rendering/coreRendererBindings.d.ts.map +1 -1
- package/dist/esm/rendering/coreRendererBindings.js +22 -0
- package/dist/esm/rendering/coreRendererBindings.js.map +1 -1
- package/dist/esm/rendering/index.d.ts +13 -0
- package/dist/esm/rendering/index.d.ts.map +1 -1
- package/dist/esm/rendering/pixelSnap.d.ts +219 -0
- package/dist/esm/rendering/pixelSnap.d.ts.map +1 -0
- package/dist/esm/rendering/pixelSnap.js +186 -0
- package/dist/esm/rendering/pixelSnap.js.map +1 -0
- package/dist/esm/rendering/plan/RenderPlanPlayer.d.ts.map +1 -1
- package/dist/esm/rendering/plan/RenderPlanPlayer.js +21 -1
- package/dist/esm/rendering/plan/RenderPlanPlayer.js.map +1 -1
- package/dist/esm/rendering/sprite/NineSliceSprite.d.ts +69 -0
- package/dist/esm/rendering/sprite/NineSliceSprite.d.ts.map +1 -0
- package/dist/esm/rendering/sprite/NineSliceSprite.js +207 -0
- package/dist/esm/rendering/sprite/NineSliceSprite.js.map +1 -0
- package/dist/esm/rendering/sprite/RepeatingSprite.d.ts +120 -0
- package/dist/esm/rendering/sprite/RepeatingSprite.d.ts.map +1 -0
- package/dist/esm/rendering/sprite/RepeatingSprite.js +279 -0
- package/dist/esm/rendering/sprite/RepeatingSprite.js.map +1 -0
- package/dist/esm/rendering/sprite/Sprite.d.ts +13 -0
- package/dist/esm/rendering/sprite/Sprite.d.ts.map +1 -1
- package/dist/esm/rendering/sprite/Sprite.js +23 -0
- package/dist/esm/rendering/sprite/Sprite.js.map +1 -1
- package/dist/esm/rendering/sprite/nineSlice.d.ts +53 -0
- package/dist/esm/rendering/sprite/nineSlice.d.ts.map +1 -0
- package/dist/esm/rendering/sprite/nineSlice.js +340 -0
- package/dist/esm/rendering/sprite/nineSlice.js.map +1 -0
- package/dist/esm/rendering/sprite/repeatingSpritePlan.d.ts +57 -0
- package/dist/esm/rendering/sprite/repeatingSpritePlan.d.ts.map +1 -0
- package/dist/esm/rendering/sprite/repeatingSpritePlan.js +156 -0
- package/dist/esm/rendering/sprite/repeatingSpritePlan.js.map +1 -0
- package/dist/esm/rendering/texture/TextureRegion.d.ts +100 -0
- package/dist/esm/rendering/texture/TextureRegion.d.ts.map +1 -0
- package/dist/esm/rendering/texture/TextureRegion.js +144 -0
- package/dist/esm/rendering/texture/TextureRegion.js.map +1 -0
- package/dist/esm/rendering/texture/repeat.d.ts +96 -0
- package/dist/esm/rendering/texture/repeat.d.ts.map +1 -0
- package/dist/esm/rendering/texture/repeat.js +158 -0
- package/dist/esm/rendering/texture/repeat.js.map +1 -0
- package/dist/esm/rendering/webgl2/WebGl2Backend.d.ts +20 -0
- package/dist/esm/rendering/webgl2/WebGl2Backend.d.ts.map +1 -1
- package/dist/esm/rendering/webgl2/WebGl2Backend.js +31 -2
- package/dist/esm/rendering/webgl2/WebGl2Backend.js.map +1 -1
- package/dist/esm/rendering/webgl2/WebGl2NineSliceSpriteRenderer.d.ts +32 -0
- package/dist/esm/rendering/webgl2/WebGl2NineSliceSpriteRenderer.d.ts.map +1 -0
- package/dist/esm/rendering/webgl2/WebGl2NineSliceSpriteRenderer.js +308 -0
- package/dist/esm/rendering/webgl2/WebGl2NineSliceSpriteRenderer.js.map +1 -0
- package/dist/esm/rendering/webgl2/WebGl2RepeatingSpriteRenderer.d.ts +49 -0
- package/dist/esm/rendering/webgl2/WebGl2RepeatingSpriteRenderer.d.ts.map +1 -0
- package/dist/esm/rendering/webgl2/WebGl2RepeatingSpriteRenderer.js +535 -0
- package/dist/esm/rendering/webgl2/WebGl2RepeatingSpriteRenderer.js.map +1 -0
- package/dist/esm/rendering/webgl2/WebGl2SpriteRenderer.d.ts +9 -0
- package/dist/esm/rendering/webgl2/WebGl2SpriteRenderer.d.ts.map +1 -1
- package/dist/esm/rendering/webgl2/WebGl2SpriteRenderer.js +22 -2
- package/dist/esm/rendering/webgl2/WebGl2SpriteRenderer.js.map +1 -1
- package/dist/esm/rendering/webgpu/WebGpuBackend.d.ts +21 -1
- package/dist/esm/rendering/webgpu/WebGpuBackend.d.ts.map +1 -1
- package/dist/esm/rendering/webgpu/WebGpuBackend.js +29 -2
- package/dist/esm/rendering/webgpu/WebGpuBackend.js.map +1 -1
- package/dist/esm/rendering/webgpu/WebGpuNineSliceSpriteRenderer.d.ts +36 -0
- package/dist/esm/rendering/webgpu/WebGpuNineSliceSpriteRenderer.d.ts.map +1 -0
- package/dist/esm/rendering/webgpu/WebGpuNineSliceSpriteRenderer.js +358 -0
- package/dist/esm/rendering/webgpu/WebGpuNineSliceSpriteRenderer.js.map +1 -0
- package/dist/esm/rendering/webgpu/WebGpuRepeatingSpriteRenderer.d.ts +52 -0
- package/dist/esm/rendering/webgpu/WebGpuRepeatingSpriteRenderer.d.ts.map +1 -0
- package/dist/esm/rendering/webgpu/WebGpuRepeatingSpriteRenderer.js +556 -0
- package/dist/esm/rendering/webgpu/WebGpuRepeatingSpriteRenderer.js.map +1 -0
- package/dist/esm/rendering/webgpu/WebGpuSpriteRenderer.d.ts +9 -0
- package/dist/esm/rendering/webgpu/WebGpuSpriteRenderer.d.ts.map +1 -1
- package/dist/esm/rendering/webgpu/WebGpuSpriteRenderer.js +22 -2
- package/dist/esm/rendering/webgpu/WebGpuSpriteRenderer.js.map +1 -1
- package/dist/esm/rendering/webgpu/WebGpuTransformStorage.d.ts +3 -2
- package/dist/esm/rendering/webgpu/WebGpuTransformStorage.d.ts.map +1 -1
- package/dist/esm/rendering/webgpu/WebGpuTransformStorage.js +4 -4
- package/dist/esm/rendering/webgpu/WebGpuTransformStorage.js.map +1 -1
- package/dist/esm/rendering.d.ts +1 -0
- package/dist/esm/rendering.d.ts.map +1 -1
- package/dist/esm/resources/Loader.d.ts +36 -8
- package/dist/esm/resources/Loader.d.ts.map +1 -1
- package/dist/esm/resources/Loader.js +30 -11
- package/dist/esm/resources/Loader.js.map +1 -1
- package/dist/exo.esm.js +3449 -59
- package/dist/exo.esm.js.map +1 -1
- package/package.json +10 -3
package/dist/exo.esm.js
CHANGED
|
@@ -2567,7 +2567,7 @@ var CollisionType;
|
|
|
2567
2567
|
CollisionType[CollisionType["SceneNode"] = 6] = "SceneNode";
|
|
2568
2568
|
})(CollisionType || (CollisionType = {}));
|
|
2569
2569
|
|
|
2570
|
-
const epsilon = 1e-10;
|
|
2570
|
+
const epsilon$1 = 1e-10;
|
|
2571
2571
|
const getCurveSegments = (radiusA, radiusB = radiusA) => Math.max(16, Math.ceil(Math.sqrt(Math.max(radiusA, radiusB)) * 8));
|
|
2572
2572
|
/** Generate a polygon approximation of an ellipse as an array of world-space points. Segment count scales with the larger radius. */
|
|
2573
2573
|
const buildEllipsePoints = ({ x: centerX, y: centerY, rx, ry }) => {
|
|
@@ -2612,10 +2612,10 @@ const buildRectanglePoints = ({ x, y, width, height }) => [
|
|
|
2612
2612
|
];
|
|
2613
2613
|
/** Translate a polygon's local-space `points` by its world offset `(x, y)`. */
|
|
2614
2614
|
const buildPolygonWorldPoints = ({ x: offsetX, y: offsetY, points }) => points.map(({ x, y }) => ({ x: x + offsetX, y: y + offsetY }));
|
|
2615
|
-
const pointOnSegment = ({ x: px, y: py }, { x: x1, y: y1 }, { x: x2, y: y2 }) => px <= Math.max(x1, x2) + epsilon && px >= Math.min(x1, x2) - epsilon && py <= Math.max(y1, y2) + epsilon && py >= Math.min(y1, y2) - epsilon;
|
|
2615
|
+
const pointOnSegment = ({ x: px, y: py }, { x: x1, y: y1 }, { x: x2, y: y2 }) => px <= Math.max(x1, x2) + epsilon$1 && px >= Math.min(x1, x2) - epsilon$1 && py <= Math.max(y1, y2) + epsilon$1 && py >= Math.min(y1, y2) - epsilon$1;
|
|
2616
2616
|
const orientation = ({ x: x1, y: y1 }, { x: x2, y: y2 }, { x: x3, y: y3 }) => {
|
|
2617
2617
|
const determinant = (y2 - y1) * (x3 - x2) - (x2 - x1) * (y3 - y2);
|
|
2618
|
-
if (Math.abs(determinant) <= epsilon) {
|
|
2618
|
+
if (Math.abs(determinant) <= epsilon$1) {
|
|
2619
2619
|
return 0;
|
|
2620
2620
|
}
|
|
2621
2621
|
return determinant > 0 ? 1 : 2;
|
|
@@ -2705,7 +2705,7 @@ const intersectionPointEllipse$1 = ({ x: x1, y: y1 }, { x: x2, y: y2, rx, ry })
|
|
|
2705
2705
|
const intersectionPointPoly$1 = (point, { points }) => polygonContainsPoint(point, points);
|
|
2706
2706
|
const intersectionLineLineSegments = (a1, a2, b1, b2) => {
|
|
2707
2707
|
const denominator = (a2.x - a1.x) * (b2.y - b1.y) - (b2.x - b1.x) * (a2.y - a1.y);
|
|
2708
|
-
if (Math.abs(denominator) <= epsilon) {
|
|
2708
|
+
if (Math.abs(denominator) <= epsilon$1) {
|
|
2709
2709
|
return false;
|
|
2710
2710
|
}
|
|
2711
2711
|
const uA = ((b2.x - b1.x) * (a1.y - b1.y) - (b2.y - b1.y) * (a1.x - b1.x)) / denominator;
|
|
@@ -7975,8 +7975,9 @@ const emptySnapshot = Object.freeze({
|
|
|
7975
7975
|
assets: Object.freeze([]),
|
|
7976
7976
|
});
|
|
7977
7977
|
/**
|
|
7978
|
-
* Flatten an ordered extension list into a snapshot
|
|
7979
|
-
*
|
|
7978
|
+
* Flatten an ordered extension list into a snapshot using stable depth-first
|
|
7979
|
+
* post-order traversal. Dependencies are materialised before their dependents.
|
|
7980
|
+
* De-duplicates same-object entries; throws on same-id/different-object or cycles.
|
|
7980
7981
|
* Binding-level conflicts are checked per Application at materialisation time.
|
|
7981
7982
|
* @internal
|
|
7982
7983
|
*/
|
|
@@ -7984,19 +7985,47 @@ function buildSnapshot(input) {
|
|
|
7984
7985
|
if (input.length === 0) {
|
|
7985
7986
|
return emptySnapshot;
|
|
7986
7987
|
}
|
|
7987
|
-
const
|
|
7988
|
-
const
|
|
7988
|
+
const byId = new Map();
|
|
7989
|
+
const visiting = new Set();
|
|
7990
|
+
const visited = new Set();
|
|
7991
|
+
const stack = [];
|
|
7992
|
+
const ordered = [];
|
|
7993
|
+
function visit(ext) {
|
|
7994
|
+
// (1) Reserve ID + mismatch check FIRST — before dependency traversal.
|
|
7995
|
+
// This catches nested same-id/different-object descriptors immediately.
|
|
7996
|
+
const existing = byId.get(ext.id);
|
|
7997
|
+
if (existing !== undefined && existing !== ext) {
|
|
7998
|
+
throw new Error(`Extension "${ext.id}" was provided by multiple descriptor objects.`);
|
|
7999
|
+
}
|
|
8000
|
+
if (existing === undefined) {
|
|
8001
|
+
byId.set(ext.id, ext);
|
|
8002
|
+
}
|
|
8003
|
+
// (2) Already fully processed — diamond / shared dependency.
|
|
8004
|
+
if (visited.has(ext))
|
|
8005
|
+
return;
|
|
8006
|
+
// (3) Back-edge in current DFS stack — cycle.
|
|
8007
|
+
if (visiting.has(ext)) {
|
|
8008
|
+
const cyclePath = [...stack.slice(stack.indexOf(ext)), ext].map(e => e.id).join(' → ');
|
|
8009
|
+
throw new Error(`Extension dependency cycle detected: ${cyclePath}`);
|
|
8010
|
+
}
|
|
8011
|
+
// (4) Recurse into dependencies (post-order: deps before this extension).
|
|
8012
|
+
visiting.add(ext);
|
|
8013
|
+
stack.push(ext);
|
|
8014
|
+
for (const dep of ext.dependencies ?? []) {
|
|
8015
|
+
visit(dep);
|
|
8016
|
+
}
|
|
8017
|
+
stack.pop();
|
|
8018
|
+
visiting.delete(ext);
|
|
8019
|
+
// (5) Finalise — remove from in-progress, mark as fully processed, push to output.
|
|
8020
|
+
visited.add(ext);
|
|
8021
|
+
ordered.push(ext);
|
|
8022
|
+
}
|
|
8023
|
+
for (const ext of input)
|
|
8024
|
+
visit(ext);
|
|
8025
|
+
// Flatten in topological (post-order) order — deps before dependents.
|
|
7989
8026
|
const renderers = [];
|
|
7990
8027
|
const assets = [];
|
|
7991
|
-
for (const ext of
|
|
7992
|
-
const existing = seenById.get(ext.id);
|
|
7993
|
-
if (existing !== undefined) {
|
|
7994
|
-
if (existing === ext)
|
|
7995
|
-
continue;
|
|
7996
|
-
throw new Error(`Duplicate extension id "${ext.id}" with a different descriptor in the provided extensions list.`);
|
|
7997
|
-
}
|
|
7998
|
-
seenById.set(ext.id, ext);
|
|
7999
|
-
extensions.push(ext);
|
|
8028
|
+
for (const ext of ordered) {
|
|
8000
8029
|
if (ext.renderers) {
|
|
8001
8030
|
for (const binding of ext.renderers) {
|
|
8002
8031
|
renderers.push(binding);
|
|
@@ -8009,7 +8038,7 @@ function buildSnapshot(input) {
|
|
|
8009
8038
|
}
|
|
8010
8039
|
}
|
|
8011
8040
|
return Object.freeze({
|
|
8012
|
-
extensions: Object.freeze(
|
|
8041
|
+
extensions: Object.freeze(ordered),
|
|
8013
8042
|
renderers: Object.freeze(renderers),
|
|
8014
8043
|
assets: Object.freeze(assets),
|
|
8015
8044
|
});
|
|
@@ -13819,13 +13848,33 @@ class RenderPlanPlayer {
|
|
|
13819
13848
|
let groupCursor = 0;
|
|
13820
13849
|
let currentGroup = null;
|
|
13821
13850
|
let currentInstructionIndex = 0;
|
|
13851
|
+
// Phase 1 — populate the CPU transform buffer for all groups in this scope
|
|
13852
|
+
// before any renderer draws execute. Without this separation, each
|
|
13853
|
+
// per-group upload changes the buffer hash while a renderer holds an
|
|
13854
|
+
// in-flight batch; the next flush detects the changed hash and re-uploads
|
|
13855
|
+
// the growing buffer, producing O(groups²) GPU transform writes per pass
|
|
13856
|
+
// (measured: ~240 KB/frame for 100 NineSlice sprites across 8 textures,
|
|
13857
|
+
// ~600 MB/frame for 5000 RepeatingSprites). Writing every group's
|
|
13858
|
+
// transforms first ensures the hash is stable by the time the first flush
|
|
13859
|
+
// calls bindTransformBufferTexture/getTransformStorageBuffer, so all
|
|
13860
|
+
// subsequent flushes within the same scope find an unchanged hash and skip
|
|
13861
|
+
// the upload entirely.
|
|
13862
|
+
if (hooks._prepareRenderGroupUpload !== undefined) {
|
|
13863
|
+
let preInstructionIndex = context.passInstructionIndex;
|
|
13864
|
+
for (let gi = 0; gi < groups.length; gi++) {
|
|
13865
|
+
const group = groups[gi];
|
|
13866
|
+
hooks._prepareRenderGroupUpload(group, this._createRenderGroupPlaybackContext(group.instructions.length, preInstructionIndex, context.passGroupIndex + gi));
|
|
13867
|
+
preInstructionIndex += group.instructions.length;
|
|
13868
|
+
}
|
|
13869
|
+
}
|
|
13870
|
+
// Phase 2 — execute draws in document order. Transform writes are already
|
|
13871
|
+
// done; _prepareRenderGroupUpload is intentionally not called a second time.
|
|
13822
13872
|
for (const entry of scope.entries) {
|
|
13823
13873
|
if (entry.kind === RenderEntryKind.Draw) {
|
|
13824
13874
|
if (currentGroup === null) {
|
|
13825
13875
|
currentGroup = groups[groupCursor];
|
|
13826
13876
|
currentInstructionIndex = 0;
|
|
13827
13877
|
hooks._beginRenderGroup?.(currentGroup);
|
|
13828
|
-
hooks._prepareRenderGroupUpload?.(currentGroup, this._createRenderGroupPlaybackContext(currentGroup.instructions.length, context.passInstructionIndex, context.passGroupIndex));
|
|
13829
13878
|
context.passGroupIndex++;
|
|
13830
13879
|
}
|
|
13831
13880
|
// Allocate the per-draw instruction slot only when a backend consumes
|
|
@@ -15091,6 +15140,190 @@ class InteractionManager {
|
|
|
15091
15140
|
}
|
|
15092
15141
|
}
|
|
15093
15142
|
|
|
15143
|
+
const pixelSnapModes = new Set(['none', 'position', 'geometry']);
|
|
15144
|
+
/**
|
|
15145
|
+
* Runtime guard for the {@link PixelSnapMode} union. Used by the public setter to
|
|
15146
|
+
* reject JavaScript-invalid values atomically.
|
|
15147
|
+
* @internal
|
|
15148
|
+
*/
|
|
15149
|
+
function isPixelSnapMode(value) {
|
|
15150
|
+
return typeof value === 'string' && pixelSnapModes.has(value);
|
|
15151
|
+
}
|
|
15152
|
+
/** Below this magnitude an axis is treated as collapsed / cross-coupled. @internal */
|
|
15153
|
+
const epsilon = 1e-6;
|
|
15154
|
+
/**
|
|
15155
|
+
* Build the device-pixel snap context for `world` (a node's global transform)
|
|
15156
|
+
* under `view`, targeting a surface of `targetPxWidth` × `targetPxHeight` device
|
|
15157
|
+
* pixels. Pure — does not mutate `world` or `view`. Falls back to a no-op context
|
|
15158
|
+
* (snapped origin = original origin) when the target size or projection is
|
|
15159
|
+
* degenerate, so callers can always use the result safely.
|
|
15160
|
+
* @internal
|
|
15161
|
+
*/
|
|
15162
|
+
function buildPixelSnapContext(world, view, targetPxWidth, targetPxHeight) {
|
|
15163
|
+
const ox = world.x;
|
|
15164
|
+
const oy = world.y;
|
|
15165
|
+
if (!(targetPxWidth > 0) || !(targetPxHeight > 0)) {
|
|
15166
|
+
return noopContext(ox, oy);
|
|
15167
|
+
}
|
|
15168
|
+
// Forward projection only (world → device). The view's inverse is deliberately
|
|
15169
|
+
// avoided; instead the origin plus two world-unit axis tips give the exact
|
|
15170
|
+
// world→device Jacobian, which we invert ourselves (a 2×2) to map the snapped
|
|
15171
|
+
// device origin back to world space. This stays exact for any affine view.
|
|
15172
|
+
const origin = view.worldToScreen(ox, oy, targetPxWidth, targetPxHeight);
|
|
15173
|
+
if (!Number.isFinite(origin.x) || !Number.isFinite(origin.y)) {
|
|
15174
|
+
return noopContext(ox, oy);
|
|
15175
|
+
}
|
|
15176
|
+
const tipWorldX = view.worldToScreen(ox + 1, oy, targetPxWidth, targetPxHeight);
|
|
15177
|
+
const tipWorldY = view.worldToScreen(ox, oy + 1, targetPxWidth, targetPxHeight);
|
|
15178
|
+
// Columns of the world→device Jacobian J = [[jxx, jyx], [jxy, jyy]].
|
|
15179
|
+
const jxx = tipWorldX.x - origin.x;
|
|
15180
|
+
const jxy = tipWorldX.y - origin.y;
|
|
15181
|
+
const jyx = tipWorldY.x - origin.x;
|
|
15182
|
+
const jyy = tipWorldY.y - origin.y;
|
|
15183
|
+
// Local axes in device pixels: apply the node's linear part to the Jacobian —
|
|
15184
|
+
// local (1,0)→world (a,c), local (0,1)→world (b,d).
|
|
15185
|
+
const scaleX = world.a * jxx + world.c * jyx; // device-x per local-x unit
|
|
15186
|
+
const crossXy = world.a * jxy + world.c * jyy; // device-y per local-x unit
|
|
15187
|
+
const crossYx = world.b * jxx + world.d * jyx; // device-x per local-y unit
|
|
15188
|
+
const scaleY = world.b * jxy + world.d * jyy; // device-y per local-y unit
|
|
15189
|
+
const axisAligned = Math.abs(crossXy) < epsilon && Math.abs(crossYx) < epsilon;
|
|
15190
|
+
const snappedOriginX = Math.round(origin.x);
|
|
15191
|
+
const snappedOriginY = Math.round(origin.y);
|
|
15192
|
+
// Map the snapped device origin back to world via J⁻¹ so that, re-projected
|
|
15193
|
+
// through the (unchanged) view, the rendered origin lands on the device pixel.
|
|
15194
|
+
let worldX = ox;
|
|
15195
|
+
let worldY = oy;
|
|
15196
|
+
const det = jxx * jyy - jyx * jxy;
|
|
15197
|
+
if (Math.abs(det) > epsilon) {
|
|
15198
|
+
const ddx = snappedOriginX - origin.x;
|
|
15199
|
+
const ddy = snappedOriginY - origin.y;
|
|
15200
|
+
worldX = ox + (jyy * ddx - jyx * ddy) / det;
|
|
15201
|
+
worldY = oy + (jxx * ddy - jxy * ddx) / det;
|
|
15202
|
+
}
|
|
15203
|
+
return {
|
|
15204
|
+
originX: origin.x,
|
|
15205
|
+
originY: origin.y,
|
|
15206
|
+
snappedOriginX,
|
|
15207
|
+
snappedOriginY,
|
|
15208
|
+
worldX,
|
|
15209
|
+
worldY,
|
|
15210
|
+
scaleX,
|
|
15211
|
+
scaleY,
|
|
15212
|
+
axisAligned,
|
|
15213
|
+
};
|
|
15214
|
+
}
|
|
15215
|
+
function noopContext(ox, oy) {
|
|
15216
|
+
return {
|
|
15217
|
+
originX: ox,
|
|
15218
|
+
originY: oy,
|
|
15219
|
+
snappedOriginX: ox,
|
|
15220
|
+
snappedOriginY: oy,
|
|
15221
|
+
worldX: ox,
|
|
15222
|
+
worldY: oy,
|
|
15223
|
+
scaleX: 0,
|
|
15224
|
+
scaleY: 0,
|
|
15225
|
+
axisAligned: false,
|
|
15226
|
+
};
|
|
15227
|
+
}
|
|
15228
|
+
/**
|
|
15229
|
+
* Copy `world` into `out`, replacing only the translation with the snapped world
|
|
15230
|
+
* origin from `ctx`. The linear part `(a, b, c, d)` and homogeneous row are
|
|
15231
|
+
* preserved, so rotation / scale / skew are untouched — position snapping is safe
|
|
15232
|
+
* under any transform. The source `world` matrix is never mutated.
|
|
15233
|
+
* @internal
|
|
15234
|
+
*/
|
|
15235
|
+
function snapWorldTranslationInto(out, world, ctx) {
|
|
15236
|
+
out.copy(world);
|
|
15237
|
+
out.x = ctx.worldX;
|
|
15238
|
+
out.y = ctx.worldY;
|
|
15239
|
+
return out;
|
|
15240
|
+
}
|
|
15241
|
+
/**
|
|
15242
|
+
* Snap a single local boundary coordinate to the device-pixel grid along an axis
|
|
15243
|
+
* whose local→device scale is `scale`. Returns the local value whose device
|
|
15244
|
+
* position (relative to the already-snapped origin) lands on an integer device
|
|
15245
|
+
* pixel: `round(L · scale) / scale`.
|
|
15246
|
+
*
|
|
15247
|
+
* The function is pure, so two boundaries with the same input value snap to the
|
|
15248
|
+
* same output — shared boundaries stay shared and seams cannot open. It is also
|
|
15249
|
+
* monotonic non-decreasing in `L` for any non-zero `scale` (including negative
|
|
15250
|
+
* scale from a flip), so boundary order is preserved and snapped spans never go
|
|
15251
|
+
* negative. Degenerate scales (`|scale| < epsilon`) and non-finite inputs return
|
|
15252
|
+
* the value unchanged.
|
|
15253
|
+
* @internal
|
|
15254
|
+
*/
|
|
15255
|
+
function snapLocalBoundary(localValue, scale) {
|
|
15256
|
+
if (!Number.isFinite(localValue) || Math.abs(scale) < epsilon) {
|
|
15257
|
+
return localValue;
|
|
15258
|
+
}
|
|
15259
|
+
return Math.round(localValue * scale) / scale;
|
|
15260
|
+
}
|
|
15261
|
+
/**
|
|
15262
|
+
* Snap every quad's local X/Y boundaries to the device grid using the per-axis
|
|
15263
|
+
* scale in `ctx`, writing the result into the reusable `out` buffer (grown /
|
|
15264
|
+
* truncated to match `source`, never reallocated per frame once warm). UVs are
|
|
15265
|
+
* copied verbatim — sampling is unchanged. Because {@link snapLocalBoundary} is
|
|
15266
|
+
* pure, quads sharing a boundary value stay seam-free without any explicit
|
|
15267
|
+
* de-duplication.
|
|
15268
|
+
* @internal
|
|
15269
|
+
*/
|
|
15270
|
+
function snapQuadsInto(source, ctx, out) {
|
|
15271
|
+
const { scaleX, scaleY } = ctx;
|
|
15272
|
+
out.length = source.length;
|
|
15273
|
+
for (let i = 0; i < source.length; i++) {
|
|
15274
|
+
const q = source[i];
|
|
15275
|
+
let t = out[i];
|
|
15276
|
+
if (t === undefined) {
|
|
15277
|
+
t = out[i] = { x0: 0, y0: 0, x1: 0, y1: 0, u0: 0, v0: 0, u1: 0, v1: 0 };
|
|
15278
|
+
}
|
|
15279
|
+
t.x0 = snapLocalBoundary(q.x0, scaleX);
|
|
15280
|
+
t.x1 = snapLocalBoundary(q.x1, scaleX);
|
|
15281
|
+
t.y0 = snapLocalBoundary(q.y0, scaleY);
|
|
15282
|
+
t.y1 = snapLocalBoundary(q.y1, scaleY);
|
|
15283
|
+
t.u0 = q.u0;
|
|
15284
|
+
t.v0 = q.v0;
|
|
15285
|
+
t.u1 = q.u1;
|
|
15286
|
+
t.v1 = q.v1;
|
|
15287
|
+
}
|
|
15288
|
+
return out;
|
|
15289
|
+
}
|
|
15290
|
+
/**
|
|
15291
|
+
* Resolve the world transform to upload for `drawable` at the transform-buffer
|
|
15292
|
+
* write seam. Returns the drawable's live global transform unchanged when its
|
|
15293
|
+
* mode is `'none'` (zero overhead), otherwise a snapped copy written into the
|
|
15294
|
+
* caller-owned `scratch` matrix — the logical global transform is never mutated.
|
|
15295
|
+
*
|
|
15296
|
+
* Both backends call this at their single transform-write boundary, so position
|
|
15297
|
+
* snapping (and tilemap chunk/layer origin snapping) is applied once, backend-
|
|
15298
|
+
* neutrally, to every drawable. `view` and the target device-pixel dimensions
|
|
15299
|
+
* come from the active pass.
|
|
15300
|
+
* @internal
|
|
15301
|
+
*/
|
|
15302
|
+
function resolveUploadTransform(drawable, view, targetPxWidth, targetPxHeight, scratch) {
|
|
15303
|
+
const world = drawable.getGlobalTransform();
|
|
15304
|
+
if (drawable.pixelSnapMode === 'none') {
|
|
15305
|
+
return world;
|
|
15306
|
+
}
|
|
15307
|
+
const ctx = buildPixelSnapContext(world, view, targetPxWidth, targetPxHeight);
|
|
15308
|
+
return snapWorldTranslationInto(scratch, world, ctx);
|
|
15309
|
+
}
|
|
15310
|
+
/**
|
|
15311
|
+
* Snap a single local-space bounds rectangle (e.g. a sprite quad) to the device
|
|
15312
|
+
* grid using the per-axis scale in `ctx`, writing the result into `out`. Each of
|
|
15313
|
+
* the four edges is snapped by {@link snapLocalBoundary}, so combined with the
|
|
15314
|
+
* device-snapped origin every corner lands on a whole device pixel. `out` may be
|
|
15315
|
+
* the same instance across frames (no allocation). UV/texture mapping is the
|
|
15316
|
+
* caller's concern and is unaffected.
|
|
15317
|
+
* @internal
|
|
15318
|
+
*/
|
|
15319
|
+
function snapBoundsInto(base, ctx, out) {
|
|
15320
|
+
const left = snapLocalBoundary(base.left, ctx.scaleX);
|
|
15321
|
+
const top = snapLocalBoundary(base.top, ctx.scaleY);
|
|
15322
|
+
const right = snapLocalBoundary(base.right, ctx.scaleX);
|
|
15323
|
+
const bottom = snapLocalBoundary(base.bottom, ctx.scaleY);
|
|
15324
|
+
return out.set(left, top, right - left, bottom - top);
|
|
15325
|
+
}
|
|
15326
|
+
|
|
15094
15327
|
/**
|
|
15095
15328
|
* Base class for every renderable scene object.
|
|
15096
15329
|
*
|
|
@@ -15101,6 +15334,7 @@ class InteractionManager {
|
|
|
15101
15334
|
class Drawable extends RenderNode {
|
|
15102
15335
|
_tint = Color.white.clone();
|
|
15103
15336
|
_blendMode = BlendModes.Normal;
|
|
15337
|
+
_pixelSnapMode = 'none';
|
|
15104
15338
|
get tint() {
|
|
15105
15339
|
return this._tint;
|
|
15106
15340
|
}
|
|
@@ -15113,6 +15347,38 @@ class Drawable extends RenderNode {
|
|
|
15113
15347
|
set blendMode(blendMode) {
|
|
15114
15348
|
this.setBlendMode(blendMode);
|
|
15115
15349
|
}
|
|
15350
|
+
/**
|
|
15351
|
+
* Render-only pixel-snapping policy for this drawable. Aligns the rendered
|
|
15352
|
+
* origin (`'position'`) or origin plus shared geometry boundaries
|
|
15353
|
+
* (`'geometry'`) to the active render target's device-pixel grid. Purely
|
|
15354
|
+
* visual: logical `x`/`y`, transforms, bounds, collision, tween and physics
|
|
15355
|
+
* state are never affected, and {@link getBounds}/{@link getGlobalTransform}
|
|
15356
|
+
* keep returning logical values.
|
|
15357
|
+
*
|
|
15358
|
+
* `'geometry'` is guaranteed only for axis-aligned transforms; rotation or
|
|
15359
|
+
* skew (on this node, an ancestor, or the view) downgrade it to `'position'`
|
|
15360
|
+
* for the affected frame, with no logical-state change. Snapping targets
|
|
15361
|
+
* device pixels (× view scale × pixel ratio), not integer world units.
|
|
15362
|
+
*
|
|
15363
|
+
* Setting the current value is a no-op. Setting a value outside the
|
|
15364
|
+
* {@link PixelSnapMode} union throws and leaves the prior mode unchanged.
|
|
15365
|
+
*
|
|
15366
|
+
* @default 'none'
|
|
15367
|
+
* @stable
|
|
15368
|
+
*/
|
|
15369
|
+
get pixelSnapMode() {
|
|
15370
|
+
return this._pixelSnapMode;
|
|
15371
|
+
}
|
|
15372
|
+
set pixelSnapMode(mode) {
|
|
15373
|
+
if (mode === this._pixelSnapMode) {
|
|
15374
|
+
return;
|
|
15375
|
+
}
|
|
15376
|
+
if (!isPixelSnapMode(mode)) {
|
|
15377
|
+
throw new Error(`Drawable.pixelSnapMode must be 'none', 'position', or 'geometry' (got ${String(mode)}).`);
|
|
15378
|
+
}
|
|
15379
|
+
this._pixelSnapMode = mode;
|
|
15380
|
+
this.invalidateCache();
|
|
15381
|
+
}
|
|
15116
15382
|
/**
|
|
15117
15383
|
* Set the tint colour by copying `color` into the internal {@link Color} instance.
|
|
15118
15384
|
* Invalidates the render cache so the change is picked up on the next frame.
|
|
@@ -15387,6 +15653,1264 @@ function clamp01(value) {
|
|
|
15387
15653
|
return value;
|
|
15388
15654
|
}
|
|
15389
15655
|
|
|
15656
|
+
function isFinite(value) {
|
|
15657
|
+
return typeof value === 'number' && Number.isFinite(value);
|
|
15658
|
+
}
|
|
15659
|
+
function normalizeExtrusion(extrusion) {
|
|
15660
|
+
if (extrusion === undefined) {
|
|
15661
|
+
return Object.freeze({
|
|
15662
|
+
left: 0,
|
|
15663
|
+
top: 0,
|
|
15664
|
+
right: 0,
|
|
15665
|
+
bottom: 0,
|
|
15666
|
+
});
|
|
15667
|
+
}
|
|
15668
|
+
if (typeof extrusion === 'number') {
|
|
15669
|
+
return Object.freeze({
|
|
15670
|
+
left: extrusion,
|
|
15671
|
+
top: extrusion,
|
|
15672
|
+
right: extrusion,
|
|
15673
|
+
bottom: extrusion,
|
|
15674
|
+
});
|
|
15675
|
+
}
|
|
15676
|
+
// Copy caller-owned values into an engine-owned frozen object so that
|
|
15677
|
+
// external mutation of the original never affects the region.
|
|
15678
|
+
return Object.freeze({
|
|
15679
|
+
left: extrusion.left,
|
|
15680
|
+
top: extrusion.top,
|
|
15681
|
+
right: extrusion.right,
|
|
15682
|
+
bottom: extrusion.bottom,
|
|
15683
|
+
});
|
|
15684
|
+
}
|
|
15685
|
+
function validateExtrusion(extrusion, x, y, width, height, textureWidth, textureHeight) {
|
|
15686
|
+
const { left, top, right, bottom } = extrusion;
|
|
15687
|
+
if (!isFinite(left) || !isFinite(top) || !isFinite(right) || !isFinite(bottom)) {
|
|
15688
|
+
throw new Error(`TextureRegion extrusion values must be finite numbers (got left=${left}, top=${top}, right=${right}, bottom=${bottom}).`);
|
|
15689
|
+
}
|
|
15690
|
+
if (left < 0 || top < 0 || right < 0 || bottom < 0) {
|
|
15691
|
+
throw new Error(`TextureRegion extrusion values must be non-negative (got left=${left}, top=${top}, right=${right}, bottom=${bottom}).`);
|
|
15692
|
+
}
|
|
15693
|
+
if (left > x || top > y || right > textureWidth - (x + width) || bottom > textureHeight - (y + height)) {
|
|
15694
|
+
throw new Error(`TextureRegion extrusion exceeds available source texture bounds: left=${left} (>${x}), top=${top} (>${y}), ` +
|
|
15695
|
+
`right=${right} (>${textureWidth - (x + width)}), bottom=${bottom} (>${textureHeight - (y + height)}).`);
|
|
15696
|
+
}
|
|
15697
|
+
}
|
|
15698
|
+
function validateOptions(options, textureWidth, textureHeight) {
|
|
15699
|
+
const { x, y, width, height } = options;
|
|
15700
|
+
if (!isFinite(x) || !isFinite(y) || !isFinite(width) || !isFinite(height)) {
|
|
15701
|
+
throw new Error(`TextureRegion coordinates and dimensions must be finite numbers (got x=${x}, y=${y}, width=${width}, height=${height}).`);
|
|
15702
|
+
}
|
|
15703
|
+
if (width <= 0 || height <= 0) {
|
|
15704
|
+
throw new Error(`TextureRegion dimensions must be positive (got width=${width}, height=${height}).`);
|
|
15705
|
+
}
|
|
15706
|
+
if (textureWidth <= 0 || textureHeight <= 0) {
|
|
15707
|
+
throw new Error(`Texture must have positive dimensions (got ${textureWidth}x${textureHeight}).`);
|
|
15708
|
+
}
|
|
15709
|
+
if (x < 0 || y < 0) {
|
|
15710
|
+
throw new Error(`TextureRegion origin must be non-negative (got x=${x}, y=${y}).`);
|
|
15711
|
+
}
|
|
15712
|
+
if (x >= textureWidth || y >= textureHeight) {
|
|
15713
|
+
throw new Error(`TextureRegion origin (${x}, ${y}) is outside texture bounds (${textureWidth}x${textureHeight}).`);
|
|
15714
|
+
}
|
|
15715
|
+
if (x + width > textureWidth) {
|
|
15716
|
+
throw new Error(`TextureRegion right edge (${x + width}) exceeds texture width (${textureWidth}).`);
|
|
15717
|
+
}
|
|
15718
|
+
if (y + height > textureHeight) {
|
|
15719
|
+
throw new Error(`TextureRegion bottom edge (${y + height}) exceeds texture height (${textureHeight}).`);
|
|
15720
|
+
}
|
|
15721
|
+
}
|
|
15722
|
+
/**
|
|
15723
|
+
* An immutable descriptor for a rectangular sub-region of a {@link Texture}.
|
|
15724
|
+
*
|
|
15725
|
+
* Stores the pixel-space source rectangle, pre-computed normalised UV bounds,
|
|
15726
|
+
* and optional extrusion/padding metadata for atlas-safe linear filtering.
|
|
15727
|
+
* Constructed once and reused across sprites, tile-sets, atlas lookups, and
|
|
15728
|
+
* the scalable-sprite repeat planners.
|
|
15729
|
+
*
|
|
15730
|
+
* All region descriptor fields are stable after construction. Extrusion
|
|
15731
|
+
* metadata is copied and frozen during construction — the caller retains no
|
|
15732
|
+
* mutable reference to the stored object. The underlying {@link Texture}
|
|
15733
|
+
* reference is stable, but the Texture's own lifecycle and sampler state
|
|
15734
|
+
* remain owned by {@link Texture}.
|
|
15735
|
+
*
|
|
15736
|
+
* Texture dimensions **must** be known at construction time; a texture with
|
|
15737
|
+
* zero dimensions will cause the constructor to throw.
|
|
15738
|
+
*
|
|
15739
|
+
* @example
|
|
15740
|
+
* ```ts
|
|
15741
|
+
* const region = new TextureRegion(texture, {
|
|
15742
|
+
* x: 32, y: 16,
|
|
15743
|
+
* width: 64, height: 32,
|
|
15744
|
+
* });
|
|
15745
|
+
* ```
|
|
15746
|
+
* @stable
|
|
15747
|
+
*/
|
|
15748
|
+
class TextureRegion {
|
|
15749
|
+
/** The underlying {@link Texture} this region belongs to. */
|
|
15750
|
+
texture;
|
|
15751
|
+
/** Left edge of the region in texture pixels. */
|
|
15752
|
+
x;
|
|
15753
|
+
/** Top edge of the region in texture pixels. */
|
|
15754
|
+
y;
|
|
15755
|
+
/** Width of the region in texture pixels. */
|
|
15756
|
+
width;
|
|
15757
|
+
/** Height of the region in texture pixels. */
|
|
15758
|
+
height;
|
|
15759
|
+
/** Normalised left texture coordinate (U-min). */
|
|
15760
|
+
u0;
|
|
15761
|
+
/** Normalised top texture coordinate (V-min). */
|
|
15762
|
+
v0;
|
|
15763
|
+
/** Normalised right texture coordinate (U-max). */
|
|
15764
|
+
u1;
|
|
15765
|
+
/** Normalised bottom texture coordinate (V-max). */
|
|
15766
|
+
v1;
|
|
15767
|
+
/** Per-edge extrusion/padding metadata (engine-owned, frozen). */
|
|
15768
|
+
extrusion;
|
|
15769
|
+
/**
|
|
15770
|
+
* Create a new immutable region.
|
|
15771
|
+
*
|
|
15772
|
+
* @throws When coordinates or dimensions are non-finite, zero, negative, or
|
|
15773
|
+
* extend beyond the texture bounds, or when extrusion values are
|
|
15774
|
+
* invalid.
|
|
15775
|
+
*/
|
|
15776
|
+
constructor(texture, options) {
|
|
15777
|
+
if (!texture) {
|
|
15778
|
+
throw new Error('TextureRegion requires a non-null Texture.');
|
|
15779
|
+
}
|
|
15780
|
+
const textureWidth = texture.width;
|
|
15781
|
+
const textureHeight = texture.height;
|
|
15782
|
+
validateOptions(options, textureWidth, textureHeight);
|
|
15783
|
+
const extrusion = normalizeExtrusion(options.extrusion);
|
|
15784
|
+
validateExtrusion(extrusion, options.x, options.y, options.width, options.height, textureWidth, textureHeight);
|
|
15785
|
+
this.texture = texture;
|
|
15786
|
+
this.x = options.x;
|
|
15787
|
+
this.y = options.y;
|
|
15788
|
+
this.width = options.width;
|
|
15789
|
+
this.height = options.height;
|
|
15790
|
+
this.u0 = options.x / textureWidth;
|
|
15791
|
+
this.v0 = options.y / textureHeight;
|
|
15792
|
+
this.u1 = (options.x + options.width) / textureWidth;
|
|
15793
|
+
this.v1 = (options.y + options.height) / textureHeight;
|
|
15794
|
+
this.extrusion = extrusion;
|
|
15795
|
+
}
|
|
15796
|
+
}
|
|
15797
|
+
|
|
15798
|
+
/**
|
|
15799
|
+
* Validate generic numeric inputs shared across all modes.
|
|
15800
|
+
* Throws unconditionally — these are user/programming errors, not dev-only
|
|
15801
|
+
* assertions.
|
|
15802
|
+
*/
|
|
15803
|
+
function validateInputs(sourceLength, destinationLength) {
|
|
15804
|
+
if (!Number.isFinite(sourceLength) || !Number.isFinite(destinationLength)) {
|
|
15805
|
+
throw new Error(`RepeatPlanner: sourceLength and destinationLength must be finite numbers (got ${sourceLength}, ${destinationLength}).`);
|
|
15806
|
+
}
|
|
15807
|
+
if (sourceLength <= 0) {
|
|
15808
|
+
throw new Error(`RepeatPlanner: sourceLength must be positive (got ${sourceLength}).`);
|
|
15809
|
+
}
|
|
15810
|
+
if (destinationLength < 0) {
|
|
15811
|
+
throw new Error(`RepeatPlanner: destinationLength must be non-negative (got ${destinationLength}).`);
|
|
15812
|
+
}
|
|
15813
|
+
}
|
|
15814
|
+
function buildStretchPlan(destinationLength, sourceLength) {
|
|
15815
|
+
if (destinationLength === 0) {
|
|
15816
|
+
return {
|
|
15817
|
+
destinationLength: 0,
|
|
15818
|
+
sourceLength,
|
|
15819
|
+
segments: [],
|
|
15820
|
+
};
|
|
15821
|
+
}
|
|
15822
|
+
const segment = {
|
|
15823
|
+
destinationStart: 0,
|
|
15824
|
+
destinationLength,
|
|
15825
|
+
sourceStart: 0,
|
|
15826
|
+
sourceEnd: 1,
|
|
15827
|
+
mirrored: false,
|
|
15828
|
+
};
|
|
15829
|
+
return {
|
|
15830
|
+
destinationLength,
|
|
15831
|
+
sourceLength,
|
|
15832
|
+
segments: [segment],
|
|
15833
|
+
};
|
|
15834
|
+
}
|
|
15835
|
+
function buildClipPlan(destinationLength, sourceLength, mirror) {
|
|
15836
|
+
if (destinationLength === 0) {
|
|
15837
|
+
return {
|
|
15838
|
+
destinationLength: 0,
|
|
15839
|
+
sourceLength,
|
|
15840
|
+
segments: [],
|
|
15841
|
+
};
|
|
15842
|
+
}
|
|
15843
|
+
const segments = [];
|
|
15844
|
+
let cursor = 0;
|
|
15845
|
+
let index = 0;
|
|
15846
|
+
while (cursor < destinationLength) {
|
|
15847
|
+
const remaining = destinationLength - cursor;
|
|
15848
|
+
const segLength = Math.min(sourceLength, remaining);
|
|
15849
|
+
const sourceFraction = segLength / sourceLength;
|
|
15850
|
+
const mirrored = mirror && (index % 2 === 1);
|
|
15851
|
+
const segment = {
|
|
15852
|
+
destinationStart: cursor,
|
|
15853
|
+
destinationLength: segLength,
|
|
15854
|
+
sourceStart: mirrored ? 1 : 0,
|
|
15855
|
+
sourceEnd: mirrored ? 1 - sourceFraction : sourceFraction,
|
|
15856
|
+
mirrored,
|
|
15857
|
+
};
|
|
15858
|
+
segments.push(segment);
|
|
15859
|
+
cursor += segLength;
|
|
15860
|
+
index++;
|
|
15861
|
+
}
|
|
15862
|
+
return {
|
|
15863
|
+
destinationLength,
|
|
15864
|
+
sourceLength,
|
|
15865
|
+
segments,
|
|
15866
|
+
};
|
|
15867
|
+
}
|
|
15868
|
+
function buildRoundPlan(destinationLength, sourceLength, mirror) {
|
|
15869
|
+
if (destinationLength === 0) {
|
|
15870
|
+
return {
|
|
15871
|
+
destinationLength: 0,
|
|
15872
|
+
sourceLength,
|
|
15873
|
+
segments: [],
|
|
15874
|
+
};
|
|
15875
|
+
}
|
|
15876
|
+
const ratio = destinationLength / sourceLength;
|
|
15877
|
+
const count = Math.max(1, Math.round(ratio));
|
|
15878
|
+
const segLength = destinationLength / count;
|
|
15879
|
+
const segments = [];
|
|
15880
|
+
for (let i = 0; i < count; i++) {
|
|
15881
|
+
const mirrored = mirror && (i % 2 === 1);
|
|
15882
|
+
const segment = {
|
|
15883
|
+
destinationStart: i * segLength,
|
|
15884
|
+
destinationLength: segLength,
|
|
15885
|
+
sourceStart: mirrored ? 1 : 0,
|
|
15886
|
+
sourceEnd: mirrored ? 0 : 1,
|
|
15887
|
+
mirrored,
|
|
15888
|
+
};
|
|
15889
|
+
segments.push(segment);
|
|
15890
|
+
}
|
|
15891
|
+
return {
|
|
15892
|
+
destinationLength,
|
|
15893
|
+
sourceLength,
|
|
15894
|
+
segments,
|
|
15895
|
+
};
|
|
15896
|
+
}
|
|
15897
|
+
/**
|
|
15898
|
+
* Compute a deterministic repeat plan for filling a destination span with a
|
|
15899
|
+
* tiled or stretched source pattern.
|
|
15900
|
+
*
|
|
15901
|
+
* This is a **pure, renderer-independent** function used as the shared repeat
|
|
15902
|
+
* layout engine by {@link NineSliceSprite} edges/center,
|
|
15903
|
+
* {@link RepeatingSprite} geometry path, and tilemap chunk builders.
|
|
15904
|
+
*
|
|
15905
|
+
* ## Modes
|
|
15906
|
+
*
|
|
15907
|
+
* | Mode | Behaviour |
|
|
15908
|
+
* |------------------|-----------|
|
|
15909
|
+
* | `'stretch'` | One segment stretched to the full destination. `fit` is ignored. |
|
|
15910
|
+
* | `'repeat'` | Native-size repeats; final segment is clipped when the destination is not an exact multiple. |
|
|
15911
|
+
* | `'mirror-repeat'`| Alternating normal/mirrored segments (period-2). Clipped final segment when non-exact. |
|
|
15912
|
+
*
|
|
15913
|
+
* ## Fit
|
|
15914
|
+
*
|
|
15915
|
+
* | Fit | Effect |
|
|
15916
|
+
* |----------|--------|
|
|
15917
|
+
* | `'clip'` | Native-size segments; the final segment is clipped if necessary. |
|
|
15918
|
+
* | `'round'`| Integer count of equally-sized segments stretched or squeezed so the destination fills exactly. |
|
|
15919
|
+
*
|
|
15920
|
+
* ## Allocation and caching
|
|
15921
|
+
*
|
|
15922
|
+
* The planner returns a new result object on every call. Callers should
|
|
15923
|
+
* cache the plan and recompute only when `sourceLength`, `destinationLength`,
|
|
15924
|
+
* `mode`, or `fit` changes — typically on resize or texture change.
|
|
15925
|
+
*
|
|
15926
|
+
* @param sourceLength - Native size of the source pattern in destination units.
|
|
15927
|
+
* @param destinationLength - Total span to fill.
|
|
15928
|
+
* @param mode - Fill strategy.
|
|
15929
|
+
* @param fit - Fitting strategy for `'repeat'` / `'mirror-repeat'`. Ignored for `'stretch'`.
|
|
15930
|
+
* @throws When inputs are non-finite, negative, or zero-length source.
|
|
15931
|
+
* @advanced
|
|
15932
|
+
*/
|
|
15933
|
+
function planRepeat(sourceLength, destinationLength, mode, fit = 'round') {
|
|
15934
|
+
validateInputs(sourceLength, destinationLength);
|
|
15935
|
+
switch (mode) {
|
|
15936
|
+
case 'stretch':
|
|
15937
|
+
return buildStretchPlan(destinationLength, sourceLength);
|
|
15938
|
+
case 'repeat':
|
|
15939
|
+
if (fit === 'round') {
|
|
15940
|
+
return buildRoundPlan(destinationLength, sourceLength, false);
|
|
15941
|
+
}
|
|
15942
|
+
return buildClipPlan(destinationLength, sourceLength, false);
|
|
15943
|
+
case 'mirror-repeat':
|
|
15944
|
+
if (fit === 'round') {
|
|
15945
|
+
return buildRoundPlan(destinationLength, sourceLength, true);
|
|
15946
|
+
}
|
|
15947
|
+
return buildClipPlan(destinationLength, sourceLength, true);
|
|
15948
|
+
default: {
|
|
15949
|
+
throw new Error(`RepeatPlanner: unknown RepeatMode.`);
|
|
15950
|
+
}
|
|
15951
|
+
}
|
|
15952
|
+
}
|
|
15953
|
+
|
|
15954
|
+
const halfTexelInset$1 = 0.5;
|
|
15955
|
+
// ---------------------------------------------------------------------------
|
|
15956
|
+
// Validation helpers (module-private)
|
|
15957
|
+
// ---------------------------------------------------------------------------
|
|
15958
|
+
function isFiniteNumber(value) {
|
|
15959
|
+
return typeof value === 'number' && Number.isFinite(value);
|
|
15960
|
+
}
|
|
15961
|
+
function validateSlices(slices, regionWidth, regionHeight) {
|
|
15962
|
+
const { left, top, right, bottom } = slices;
|
|
15963
|
+
if (!isFiniteNumber(left) || !isFiniteNumber(top) ||
|
|
15964
|
+
!isFiniteNumber(right) || !isFiniteNumber(bottom)) {
|
|
15965
|
+
throw new Error(`NineSliceSprite: slice values must be finite numbers (got left=${left}, top=${top}, right=${right}, bottom=${bottom}).`);
|
|
15966
|
+
}
|
|
15967
|
+
if (left < 0 || top < 0 || right < 0 || bottom < 0) {
|
|
15968
|
+
throw new Error(`NineSliceSprite: slice values must be non-negative (got left=${left}, top=${top}, right=${right}, bottom=${bottom}).`);
|
|
15969
|
+
}
|
|
15970
|
+
if (left + right > regionWidth) {
|
|
15971
|
+
throw new Error(`NineSliceSprite: slices.left (${left}) + slices.right (${right}) exceeds region width (${regionWidth}).`);
|
|
15972
|
+
}
|
|
15973
|
+
if (top + bottom > regionHeight) {
|
|
15974
|
+
throw new Error(`NineSliceSprite: slices.top (${top}) + slices.bottom (${bottom}) exceeds region height (${regionHeight}).`);
|
|
15975
|
+
}
|
|
15976
|
+
}
|
|
15977
|
+
function validateBorder(border) {
|
|
15978
|
+
const { left, top, right, bottom } = border;
|
|
15979
|
+
if (!isFiniteNumber(left) || !isFiniteNumber(top) ||
|
|
15980
|
+
!isFiniteNumber(right) || !isFiniteNumber(bottom)) {
|
|
15981
|
+
throw new Error(`NineSliceSprite: border values must be finite numbers (got left=${left}, top=${top}, right=${right}, bottom=${bottom}).`);
|
|
15982
|
+
}
|
|
15983
|
+
if (left < 0 || top < 0 || right < 0 || bottom < 0) {
|
|
15984
|
+
throw new Error(`NineSliceSprite: border values must be non-negative (got left=${left}, top=${top}, right=${right}, bottom=${bottom}).`);
|
|
15985
|
+
}
|
|
15986
|
+
}
|
|
15987
|
+
// ---------------------------------------------------------------------------
|
|
15988
|
+
// Normalization helpers (module-private)
|
|
15989
|
+
// ---------------------------------------------------------------------------
|
|
15990
|
+
/** Normalise a uniform number or partial insets object into a full {@link NineSliceInsets}. */
|
|
15991
|
+
function normalizeInsets(value, fallback) {
|
|
15992
|
+
if (typeof value === 'number') {
|
|
15993
|
+
return Object.freeze({ left: value, top: value, right: value, bottom: value });
|
|
15994
|
+
}
|
|
15995
|
+
return Object.freeze({
|
|
15996
|
+
left: value.left ?? fallback?.left ?? 0,
|
|
15997
|
+
top: value.top ?? fallback?.top ?? 0,
|
|
15998
|
+
right: value.right ?? fallback?.right ?? 0,
|
|
15999
|
+
bottom: value.bottom ?? fallback?.bottom ?? 0,
|
|
16000
|
+
});
|
|
16001
|
+
}
|
|
16002
|
+
const validRepeatModes$1 = new Set(['stretch', 'repeat', 'mirror-repeat']);
|
|
16003
|
+
const validRepeatFits$1 = new Set(['clip', 'round']);
|
|
16004
|
+
function validateModeField(value, label) {
|
|
16005
|
+
if (typeof value !== 'string' || !validRepeatModes$1.has(value)) {
|
|
16006
|
+
throw new Error(`NineSliceSprite: ${label} must be "stretch", "repeat", or "mirror-repeat".`);
|
|
16007
|
+
}
|
|
16008
|
+
}
|
|
16009
|
+
function validateFitField(value, label) {
|
|
16010
|
+
if (typeof value !== 'string' || !validRepeatFits$1.has(value)) {
|
|
16011
|
+
throw new Error(`NineSliceSprite: ${label} must be "clip" or "round".`);
|
|
16012
|
+
}
|
|
16013
|
+
}
|
|
16014
|
+
function normalizeModes(modes) {
|
|
16015
|
+
if (!modes) {
|
|
16016
|
+
return _defaultModes;
|
|
16017
|
+
}
|
|
16018
|
+
const normalized = {};
|
|
16019
|
+
if (modes.edges !== undefined) {
|
|
16020
|
+
validateModeField(modes.edges, 'modes.edges');
|
|
16021
|
+
normalized.edges = modes.edges;
|
|
16022
|
+
}
|
|
16023
|
+
if (modes.center !== undefined) {
|
|
16024
|
+
validateModeField(modes.center, 'modes.center');
|
|
16025
|
+
normalized.center = modes.center;
|
|
16026
|
+
}
|
|
16027
|
+
if (modes.top !== undefined) {
|
|
16028
|
+
validateModeField(modes.top, 'modes.top');
|
|
16029
|
+
normalized.top = modes.top;
|
|
16030
|
+
}
|
|
16031
|
+
if (modes.right !== undefined) {
|
|
16032
|
+
validateModeField(modes.right, 'modes.right');
|
|
16033
|
+
normalized.right = modes.right;
|
|
16034
|
+
}
|
|
16035
|
+
if (modes.bottom !== undefined) {
|
|
16036
|
+
validateModeField(modes.bottom, 'modes.bottom');
|
|
16037
|
+
normalized.bottom = modes.bottom;
|
|
16038
|
+
}
|
|
16039
|
+
if (modes.left !== undefined) {
|
|
16040
|
+
validateModeField(modes.left, 'modes.left');
|
|
16041
|
+
normalized.left = modes.left;
|
|
16042
|
+
}
|
|
16043
|
+
if (modes.edgeFit !== undefined) {
|
|
16044
|
+
validateFitField(modes.edgeFit, 'modes.edgeFit');
|
|
16045
|
+
normalized.edgeFit = modes.edgeFit;
|
|
16046
|
+
}
|
|
16047
|
+
if (modes.centerFit !== undefined) {
|
|
16048
|
+
validateFitField(modes.centerFit, 'modes.centerFit');
|
|
16049
|
+
normalized.centerFit = modes.centerFit;
|
|
16050
|
+
}
|
|
16051
|
+
return Object.freeze(normalized);
|
|
16052
|
+
}
|
|
16053
|
+
const _defaultModes = Object.freeze({});
|
|
16054
|
+
function equalInsets(a, b) {
|
|
16055
|
+
return a.left === b.left && a.top === b.top && a.right === b.right && a.bottom === b.bottom;
|
|
16056
|
+
}
|
|
16057
|
+
function equalModes(a, b) {
|
|
16058
|
+
return a.edges === b.edges
|
|
16059
|
+
&& a.center === b.center
|
|
16060
|
+
&& a.top === b.top
|
|
16061
|
+
&& a.right === b.right
|
|
16062
|
+
&& a.bottom === b.bottom
|
|
16063
|
+
&& a.left === b.left
|
|
16064
|
+
&& a.edgeFit === b.edgeFit
|
|
16065
|
+
&& a.centerFit === b.centerFit;
|
|
16066
|
+
}
|
|
16067
|
+
// ---------------------------------------------------------------------------
|
|
16068
|
+
// Mode resolution helpers (module-private)
|
|
16069
|
+
// ---------------------------------------------------------------------------
|
|
16070
|
+
function resolveEdgeMode(modes, side) {
|
|
16071
|
+
return modes?.[side] ?? modes?.edges ?? 'stretch';
|
|
16072
|
+
}
|
|
16073
|
+
function resolveCenterMode(modes) {
|
|
16074
|
+
return modes?.center ?? 'stretch';
|
|
16075
|
+
}
|
|
16076
|
+
function resolveEdgeFit(modes) {
|
|
16077
|
+
return modes?.edgeFit ?? 'round';
|
|
16078
|
+
}
|
|
16079
|
+
function resolveCenterFit(modes) {
|
|
16080
|
+
return modes?.centerFit ?? 'round';
|
|
16081
|
+
}
|
|
16082
|
+
/**
|
|
16083
|
+
* Compute exact per-slice UV boundaries from source pixel positions.
|
|
16084
|
+
*
|
|
16085
|
+
* Source-slice boundaries correspond to actual pixel coordinates:
|
|
16086
|
+
* left bound = region.x + slices.left
|
|
16087
|
+
* right bound = region.x + region.width - slices.right
|
|
16088
|
+
* top bound = region.y + slices.top
|
|
16089
|
+
* bottom bound = region.y + region.height - slices.bottom
|
|
16090
|
+
*
|
|
16091
|
+
* A half-texel inset is applied at internal boundaries to prevent bilinear
|
|
16092
|
+
* filtering from sampling across slice seams. Outer boundaries use the full
|
|
16093
|
+
* region UV range when extrusion is present, or a half-texel inset otherwise.
|
|
16094
|
+
*/
|
|
16095
|
+
function computeSliceUvGrid(region, slices) {
|
|
16096
|
+
const tw = region.texture.width;
|
|
16097
|
+
const th = region.texture.height;
|
|
16098
|
+
const rx = region.x;
|
|
16099
|
+
const ry = region.y;
|
|
16100
|
+
const rw = region.width;
|
|
16101
|
+
const rh = region.height;
|
|
16102
|
+
const ext = region.extrusion;
|
|
16103
|
+
// Outer boundary insets: use extrusion if available, otherwise half-texel.
|
|
16104
|
+
const outerInsetU = ext.left > 0 || ext.right > 0
|
|
16105
|
+
? 0
|
|
16106
|
+
: halfTexelInset$1 / tw;
|
|
16107
|
+
const outerInsetV = ext.top > 0 || ext.bottom > 0
|
|
16108
|
+
? 0
|
|
16109
|
+
: halfTexelInset$1 / th;
|
|
16110
|
+
// Inner slice boundary insets: always apply half-texel to prevent
|
|
16111
|
+
// bilinear bleed across internal slice seams.
|
|
16112
|
+
const innerInsetU = halfTexelInset$1 / tw;
|
|
16113
|
+
const innerInsetV = halfTexelInset$1 / th;
|
|
16114
|
+
// Exact source pixel boundaries in UV space
|
|
16115
|
+
const uLeftBound = (rx + slices.left) / tw;
|
|
16116
|
+
const uRightBound = (rx + rw - slices.right) / tw;
|
|
16117
|
+
const vTopBound = (ry + slices.top) / th;
|
|
16118
|
+
const vBottomBound = (ry + rh - slices.bottom) / th;
|
|
16119
|
+
// Outer UV bounds (possibly inset)
|
|
16120
|
+
const uOuter0 = region.u0 + outerInsetU;
|
|
16121
|
+
const uOuter1 = region.u1 - outerInsetU;
|
|
16122
|
+
const vOuter0 = region.v0 + outerInsetV;
|
|
16123
|
+
const vOuter1 = region.v1 - outerInsetV;
|
|
16124
|
+
// Clamp internal boundaries to prevent UV inversion on narrow cells
|
|
16125
|
+
const u1 = clampUv(uLeftBound - innerInsetU, uOuter0, uOuter1);
|
|
16126
|
+
const u2 = clampUv(uRightBound + innerInsetU, uOuter0, uOuter1);
|
|
16127
|
+
const v1 = clampUv(vTopBound - innerInsetV, vOuter0, vOuter1);
|
|
16128
|
+
const v2 = clampUv(vBottomBound + innerInsetV, vOuter0, vOuter1);
|
|
16129
|
+
return {
|
|
16130
|
+
col0: { u0: uOuter0, u1 },
|
|
16131
|
+
col1: { u0: u1, u1: u2 },
|
|
16132
|
+
col2: { u0: u2, u1: uOuter1 },
|
|
16133
|
+
row0: { u0: vOuter0, u1: v1 },
|
|
16134
|
+
row1: { u0: v1, u1: v2 },
|
|
16135
|
+
row2: { u0: v2, u1: vOuter1 },
|
|
16136
|
+
};
|
|
16137
|
+
}
|
|
16138
|
+
function clampUv(value, min, max) {
|
|
16139
|
+
if (!isFiniteNumber(value) || value < min)
|
|
16140
|
+
return min;
|
|
16141
|
+
if (value > max)
|
|
16142
|
+
return max;
|
|
16143
|
+
return value;
|
|
16144
|
+
}
|
|
16145
|
+
function compressBorders(border, width, height) {
|
|
16146
|
+
let bl = border.left;
|
|
16147
|
+
let br = border.right;
|
|
16148
|
+
let bt = border.top;
|
|
16149
|
+
let bb = border.bottom;
|
|
16150
|
+
if (bl + br > width && bl + br > 0) {
|
|
16151
|
+
const k = width / (bl + br);
|
|
16152
|
+
bl *= k;
|
|
16153
|
+
br *= k;
|
|
16154
|
+
}
|
|
16155
|
+
if (bt + bb > height && bt + bb > 0) {
|
|
16156
|
+
const k = height / (bt + bb);
|
|
16157
|
+
bt *= k;
|
|
16158
|
+
bb *= k;
|
|
16159
|
+
}
|
|
16160
|
+
return { bl, br, bt, bb };
|
|
16161
|
+
}
|
|
16162
|
+
// ---------------------------------------------------------------------------
|
|
16163
|
+
// Geometry builder
|
|
16164
|
+
// ---------------------------------------------------------------------------
|
|
16165
|
+
/**
|
|
16166
|
+
* Build the list of quads that collectively render a nine-slice sprite at the
|
|
16167
|
+
* requested destination size. Called lazily by {@link NineSliceSprite}.
|
|
16168
|
+
* @internal
|
|
16169
|
+
*/
|
|
16170
|
+
function buildNineSliceQuads(region, slices, border, width, height, modes) {
|
|
16171
|
+
const uvGrid = computeSliceUvGrid(region, slices);
|
|
16172
|
+
const compressed = compressBorders(border, width, height);
|
|
16173
|
+
const bl = compressed.bl;
|
|
16174
|
+
const br = compressed.br;
|
|
16175
|
+
const bt = compressed.bt;
|
|
16176
|
+
const bb = compressed.bb;
|
|
16177
|
+
const centerW = Math.max(0, width - bl - br);
|
|
16178
|
+
const centerH = Math.max(0, height - bt - bb);
|
|
16179
|
+
const dx0 = 0;
|
|
16180
|
+
const dx1 = bl;
|
|
16181
|
+
const dx2 = bl + centerW;
|
|
16182
|
+
const dx3 = width;
|
|
16183
|
+
const dy0 = 0;
|
|
16184
|
+
const dy1 = bt;
|
|
16185
|
+
const dy2 = bt + centerH;
|
|
16186
|
+
const dy3 = height;
|
|
16187
|
+
const srcEdgeW = Math.max(0, region.width - slices.left - slices.right);
|
|
16188
|
+
const srcEdgeH = Math.max(0, region.height - slices.top - slices.bottom);
|
|
16189
|
+
const topScale = slices.top > 0 ? bt / slices.top : 1;
|
|
16190
|
+
const bottomScale = slices.bottom > 0 ? bb / slices.bottom : 1;
|
|
16191
|
+
const leftScale = slices.left > 0 ? bl / slices.left : 1;
|
|
16192
|
+
const rightScale = slices.right > 0 ? br / slices.right : 1;
|
|
16193
|
+
const topNativeW = srcEdgeW * topScale;
|
|
16194
|
+
const bottomNativeW = srcEdgeW * bottomScale;
|
|
16195
|
+
const leftNativeH = srcEdgeH * leftScale;
|
|
16196
|
+
const rightNativeH = srcEdgeH * rightScale;
|
|
16197
|
+
const centerNativeW = topNativeW;
|
|
16198
|
+
const centerNativeH = leftNativeH;
|
|
16199
|
+
const topMode = resolveEdgeMode(modes, 'top');
|
|
16200
|
+
const bottomMode = resolveEdgeMode(modes, 'bottom');
|
|
16201
|
+
const leftMode = resolveEdgeMode(modes, 'left');
|
|
16202
|
+
const rightMode = resolveEdgeMode(modes, 'right');
|
|
16203
|
+
const centerMode = resolveCenterMode(modes);
|
|
16204
|
+
const edgeFit = resolveEdgeFit(modes);
|
|
16205
|
+
const centerFit = resolveCenterFit(modes);
|
|
16206
|
+
const quads = [];
|
|
16207
|
+
const { col0, col1, col2, row0, row1, row2 } = uvGrid;
|
|
16208
|
+
// Corners
|
|
16209
|
+
if (bl > 0 && bt > 0) {
|
|
16210
|
+
quads.push({ x0: dx0, y0: dy0, x1: dx1, y1: dy1, u0: col0.u0, v0: row0.u0, u1: col0.u1, v1: row0.u1 });
|
|
16211
|
+
}
|
|
16212
|
+
if (br > 0 && bt > 0) {
|
|
16213
|
+
quads.push({ x0: dx2, y0: dy0, x1: dx3, y1: dy1, u0: col2.u0, v0: row0.u0, u1: col2.u1, v1: row0.u1 });
|
|
16214
|
+
}
|
|
16215
|
+
if (bl > 0 && bb > 0) {
|
|
16216
|
+
quads.push({ x0: dx0, y0: dy2, x1: dx1, y1: dy3, u0: col0.u0, v0: row2.u0, u1: col0.u1, v1: row2.u1 });
|
|
16217
|
+
}
|
|
16218
|
+
if (br > 0 && bb > 0) {
|
|
16219
|
+
quads.push({ x0: dx2, y0: dy2, x1: dx3, y1: dy3, u0: col2.u0, v0: row2.u0, u1: col2.u1, v1: row2.u1 });
|
|
16220
|
+
}
|
|
16221
|
+
const col1U0 = col1.u0;
|
|
16222
|
+
const col1U1 = col1.u1;
|
|
16223
|
+
const row1U0 = row1.u0;
|
|
16224
|
+
const row1U1 = row1.u1;
|
|
16225
|
+
// Top edge
|
|
16226
|
+
if (centerW > 0 && bt > 0 && srcEdgeW > 0 && topNativeW > 0) {
|
|
16227
|
+
const plan = planRepeat(topNativeW, centerW, topMode, edgeFit);
|
|
16228
|
+
for (const seg of plan.segments) {
|
|
16229
|
+
const qx0 = dx1 + seg.destinationStart;
|
|
16230
|
+
const qx1 = dx1 + seg.destinationStart + seg.destinationLength;
|
|
16231
|
+
const qu0 = col1U0 + seg.sourceStart * (col1U1 - col1U0);
|
|
16232
|
+
const qu1 = col1U0 + seg.sourceEnd * (col1U1 - col1U0);
|
|
16233
|
+
quads.push({ x0: qx0, y0: dy0, x1: qx1, y1: dy1, u0: qu0, v0: row0.u0, u1: qu1, v1: row0.u1 });
|
|
16234
|
+
}
|
|
16235
|
+
}
|
|
16236
|
+
// Bottom edge
|
|
16237
|
+
if (centerW > 0 && bb > 0 && srcEdgeW > 0 && bottomNativeW > 0) {
|
|
16238
|
+
const plan = planRepeat(bottomNativeW, centerW, bottomMode, edgeFit);
|
|
16239
|
+
for (const seg of plan.segments) {
|
|
16240
|
+
const qx0 = dx1 + seg.destinationStart;
|
|
16241
|
+
const qx1 = dx1 + seg.destinationStart + seg.destinationLength;
|
|
16242
|
+
const qu0 = col1U0 + seg.sourceStart * (col1U1 - col1U0);
|
|
16243
|
+
const qu1 = col1U0 + seg.sourceEnd * (col1U1 - col1U0);
|
|
16244
|
+
quads.push({ x0: qx0, y0: dy2, x1: qx1, y1: dy3, u0: qu0, v0: row2.u0, u1: qu1, v1: row2.u1 });
|
|
16245
|
+
}
|
|
16246
|
+
}
|
|
16247
|
+
// Left edge
|
|
16248
|
+
if (bl > 0 && centerH > 0 && srcEdgeH > 0 && leftNativeH > 0) {
|
|
16249
|
+
const plan = planRepeat(leftNativeH, centerH, leftMode, edgeFit);
|
|
16250
|
+
for (const seg of plan.segments) {
|
|
16251
|
+
const qy0 = dy1 + seg.destinationStart;
|
|
16252
|
+
const qy1 = dy1 + seg.destinationStart + seg.destinationLength;
|
|
16253
|
+
const qv0 = row1U0 + seg.sourceStart * (row1U1 - row1U0);
|
|
16254
|
+
const qv1 = row1U0 + seg.sourceEnd * (row1U1 - row1U0);
|
|
16255
|
+
quads.push({ x0: dx0, y0: qy0, x1: dx1, y1: qy1, u0: col0.u0, v0: qv0, u1: col0.u1, v1: qv1 });
|
|
16256
|
+
}
|
|
16257
|
+
}
|
|
16258
|
+
// Right edge
|
|
16259
|
+
if (br > 0 && centerH > 0 && srcEdgeH > 0 && rightNativeH > 0) {
|
|
16260
|
+
const plan = planRepeat(rightNativeH, centerH, rightMode, edgeFit);
|
|
16261
|
+
for (const seg of plan.segments) {
|
|
16262
|
+
const qy0 = dy1 + seg.destinationStart;
|
|
16263
|
+
const qy1 = dy1 + seg.destinationStart + seg.destinationLength;
|
|
16264
|
+
const qv0 = row1U0 + seg.sourceStart * (row1U1 - row1U0);
|
|
16265
|
+
const qv1 = row1U0 + seg.sourceEnd * (row1U1 - row1U0);
|
|
16266
|
+
quads.push({ x0: dx2, y0: qy0, x1: dx3, y1: qy1, u0: col2.u0, v0: qv0, u1: col2.u1, v1: qv1 });
|
|
16267
|
+
}
|
|
16268
|
+
}
|
|
16269
|
+
// Center
|
|
16270
|
+
if (centerW > 0 && centerH > 0 && srcEdgeW > 0 && srcEdgeH > 0 && centerNativeW > 0 && centerNativeH > 0) {
|
|
16271
|
+
const planX = planRepeat(centerNativeW, centerW, centerMode, centerFit);
|
|
16272
|
+
const planY = planRepeat(centerNativeH, centerH, centerMode, centerFit);
|
|
16273
|
+
for (const segY of planY.segments) {
|
|
16274
|
+
const qy0 = dy1 + segY.destinationStart;
|
|
16275
|
+
const qy1 = dy1 + segY.destinationStart + segY.destinationLength;
|
|
16276
|
+
const qv0 = row1U0 + segY.sourceStart * (row1U1 - row1U0);
|
|
16277
|
+
const qv1 = row1U0 + segY.sourceEnd * (row1U1 - row1U0);
|
|
16278
|
+
for (const segX of planX.segments) {
|
|
16279
|
+
const qx0 = dx1 + segX.destinationStart;
|
|
16280
|
+
const qx1 = dx1 + segX.destinationStart + segX.destinationLength;
|
|
16281
|
+
const qu0 = col1U0 + segX.sourceStart * (col1U1 - col1U0);
|
|
16282
|
+
const qu1 = col1U0 + segX.sourceEnd * (col1U1 - col1U0);
|
|
16283
|
+
quads.push({ x0: qx0, y0: qy0, x1: qx1, y1: qy1, u0: qu0, v0: qv0, u1: qu1, v1: qv1 });
|
|
16284
|
+
}
|
|
16285
|
+
}
|
|
16286
|
+
}
|
|
16287
|
+
return quads;
|
|
16288
|
+
}
|
|
16289
|
+
|
|
16290
|
+
function validateSizeInput$1(width, height) {
|
|
16291
|
+
if (!Number.isFinite(width) || !Number.isFinite(height)) {
|
|
16292
|
+
throw new Error(`NineSliceSprite: width and height must be finite numbers (got ${width}, ${height}).`);
|
|
16293
|
+
}
|
|
16294
|
+
if (width < 0) {
|
|
16295
|
+
throw new Error(`NineSliceSprite: width must be non-negative (got ${width}).`);
|
|
16296
|
+
}
|
|
16297
|
+
if (height < 0) {
|
|
16298
|
+
throw new Error(`NineSliceSprite: height must be non-negative (got ${height}).`);
|
|
16299
|
+
}
|
|
16300
|
+
}
|
|
16301
|
+
/**
|
|
16302
|
+
* A scalable nine-slice (9-patch) sprite.
|
|
16303
|
+
* Corners stay pixel-perfect; edges/center fill by stretch, repeat, or mirror-repeat.
|
|
16304
|
+
* @stable
|
|
16305
|
+
*/
|
|
16306
|
+
class NineSliceSprite extends Drawable {
|
|
16307
|
+
_region;
|
|
16308
|
+
_slices;
|
|
16309
|
+
_border;
|
|
16310
|
+
_width;
|
|
16311
|
+
_height;
|
|
16312
|
+
_modes;
|
|
16313
|
+
_quads = [];
|
|
16314
|
+
_geometryDirty = true;
|
|
16315
|
+
_renderQuads = [];
|
|
16316
|
+
constructor(texture, options) {
|
|
16317
|
+
super();
|
|
16318
|
+
this._region = texture instanceof TextureRegion
|
|
16319
|
+
? texture
|
|
16320
|
+
: new TextureRegion(texture, {
|
|
16321
|
+
x: 0,
|
|
16322
|
+
y: 0,
|
|
16323
|
+
width: texture.width,
|
|
16324
|
+
height: texture.height,
|
|
16325
|
+
});
|
|
16326
|
+
const region = this._region;
|
|
16327
|
+
// Validate and own slices
|
|
16328
|
+
const rawSlices = normalizeInsets(options.slices);
|
|
16329
|
+
validateSlices(rawSlices, region.width, region.height);
|
|
16330
|
+
this._slices = rawSlices;
|
|
16331
|
+
// Validate and own border
|
|
16332
|
+
const rawBorder = options.border !== undefined
|
|
16333
|
+
? normalizeInsets(options.border)
|
|
16334
|
+
: normalizeInsets(options.slices);
|
|
16335
|
+
validateBorder(rawBorder);
|
|
16336
|
+
this._border = rawBorder;
|
|
16337
|
+
// Validate and own size
|
|
16338
|
+
const width = options.width ?? region.width;
|
|
16339
|
+
const height = options.height ?? region.height;
|
|
16340
|
+
validateSizeInput$1(width, height);
|
|
16341
|
+
this._width = width;
|
|
16342
|
+
this._height = height;
|
|
16343
|
+
// Copy and freeze modes
|
|
16344
|
+
this._modes = normalizeModes(options.modes);
|
|
16345
|
+
}
|
|
16346
|
+
// -----------------------------------------------------------------------
|
|
16347
|
+
// Public read-only accessors (engine-owned, frozen)
|
|
16348
|
+
// -----------------------------------------------------------------------
|
|
16349
|
+
/** The TextureRegion this nine-slice samples from. */
|
|
16350
|
+
get region() {
|
|
16351
|
+
return this._region;
|
|
16352
|
+
}
|
|
16353
|
+
/** Convenience accessor: the texture underlying the region. */
|
|
16354
|
+
get texture() {
|
|
16355
|
+
return this._region.texture;
|
|
16356
|
+
}
|
|
16357
|
+
/** The engine-owned, frozen source slice insets. */
|
|
16358
|
+
get slices() {
|
|
16359
|
+
return this._slices;
|
|
16360
|
+
}
|
|
16361
|
+
/** The engine-owned, frozen destination border insets. */
|
|
16362
|
+
get border() {
|
|
16363
|
+
return this._border;
|
|
16364
|
+
}
|
|
16365
|
+
/** The engine-owned, frozen edge/center fill modes. */
|
|
16366
|
+
get modes() {
|
|
16367
|
+
return this._modes;
|
|
16368
|
+
}
|
|
16369
|
+
// -----------------------------------------------------------------------
|
|
16370
|
+
// Width / Height (with atomic validation)
|
|
16371
|
+
// -----------------------------------------------------------------------
|
|
16372
|
+
/** Destination width in local units. */
|
|
16373
|
+
get width() {
|
|
16374
|
+
return this._width;
|
|
16375
|
+
}
|
|
16376
|
+
set width(value) {
|
|
16377
|
+
this.setSize(value, this._height);
|
|
16378
|
+
}
|
|
16379
|
+
/** Destination height in local units. */
|
|
16380
|
+
get height() {
|
|
16381
|
+
return this._height;
|
|
16382
|
+
}
|
|
16383
|
+
set height(value) {
|
|
16384
|
+
this.setSize(this._width, value);
|
|
16385
|
+
}
|
|
16386
|
+
// -----------------------------------------------------------------------
|
|
16387
|
+
// Mutators
|
|
16388
|
+
// -----------------------------------------------------------------------
|
|
16389
|
+
/** Set destination size. Fails atomically — prior state is preserved on invalid input. */
|
|
16390
|
+
setSize(width, height) {
|
|
16391
|
+
validateSizeInput$1(width, height);
|
|
16392
|
+
if (this._width !== width || this._height !== height) {
|
|
16393
|
+
this._width = width;
|
|
16394
|
+
this._height = height;
|
|
16395
|
+
this._geometryDirty = true;
|
|
16396
|
+
this.invalidateCache();
|
|
16397
|
+
}
|
|
16398
|
+
return this;
|
|
16399
|
+
}
|
|
16400
|
+
/** Update the SOURCE-space slice insets. Fails atomically. No-ops on equivalent values. */
|
|
16401
|
+
setSlices(slices) {
|
|
16402
|
+
const region = this._region;
|
|
16403
|
+
const normalized = normalizeInsets(slices);
|
|
16404
|
+
validateSlices(normalized, region.width, region.height);
|
|
16405
|
+
if (equalInsets(normalized, this._slices)) {
|
|
16406
|
+
return this;
|
|
16407
|
+
}
|
|
16408
|
+
this._slices = normalized;
|
|
16409
|
+
this._geometryDirty = true;
|
|
16410
|
+
this.invalidateCache();
|
|
16411
|
+
return this;
|
|
16412
|
+
}
|
|
16413
|
+
/** Update the DESTINATION border sizes. Fails atomically. No-ops on equivalent values. */
|
|
16414
|
+
setBorder(border) {
|
|
16415
|
+
const normalized = normalizeInsets(border);
|
|
16416
|
+
validateBorder(normalized);
|
|
16417
|
+
if (equalInsets(normalized, this._border)) {
|
|
16418
|
+
return this;
|
|
16419
|
+
}
|
|
16420
|
+
this._border = normalized;
|
|
16421
|
+
this._geometryDirty = true;
|
|
16422
|
+
this.invalidateCache();
|
|
16423
|
+
return this;
|
|
16424
|
+
}
|
|
16425
|
+
/** Update the edge/center fill modes. Input is copied, validated, and frozen. No-ops on equivalent values. */
|
|
16426
|
+
setModes(modes) {
|
|
16427
|
+
const normalized = normalizeModes(modes);
|
|
16428
|
+
if (equalModes(normalized, this._modes)) {
|
|
16429
|
+
return this;
|
|
16430
|
+
}
|
|
16431
|
+
this._modes = normalized;
|
|
16432
|
+
this._geometryDirty = true;
|
|
16433
|
+
this.invalidateCache();
|
|
16434
|
+
return this;
|
|
16435
|
+
}
|
|
16436
|
+
// -----------------------------------------------------------------------
|
|
16437
|
+
// Bounds
|
|
16438
|
+
// -----------------------------------------------------------------------
|
|
16439
|
+
getLocalBounds() {
|
|
16440
|
+
const bounds = super.getLocalBounds();
|
|
16441
|
+
bounds.set(0, 0, this._width, this._height);
|
|
16442
|
+
return bounds;
|
|
16443
|
+
}
|
|
16444
|
+
// -----------------------------------------------------------------------
|
|
16445
|
+
// Internal geometry (for renderers)
|
|
16446
|
+
// -----------------------------------------------------------------------
|
|
16447
|
+
/**
|
|
16448
|
+
* Lazily-built geometry quads. Each quad describes one rendered sub-region
|
|
16449
|
+
* in local space with its corresponding UV bounds.
|
|
16450
|
+
* @internal
|
|
16451
|
+
*/
|
|
16452
|
+
get quads() {
|
|
16453
|
+
if (this._geometryDirty) {
|
|
16454
|
+
this._rebuildGeometry();
|
|
16455
|
+
}
|
|
16456
|
+
return this._quads;
|
|
16457
|
+
}
|
|
16458
|
+
/**
|
|
16459
|
+
* Render-time quads for the active pass. In `'geometry'` pixel-snap mode (and
|
|
16460
|
+
* only when the combined node+view transform is axis-aligned) the shared
|
|
16461
|
+
* boundary plan is snapped to the render target's device-pixel grid via the
|
|
16462
|
+
* common {@link snapQuadsInto} helper, so every corner/edge/center quad reuses
|
|
16463
|
+
* the exact same snapped boundary value and no seams can open. The content
|
|
16464
|
+
* quad cache ({@link quads}) is never rebuilt by snapping — camera movement
|
|
16465
|
+
* reuses it — and snapped quads are written into a reused buffer. Returns the
|
|
16466
|
+
* unsnapped content quads for `'none'`/`'position'` or under a rotation/skew
|
|
16467
|
+
* downgrade.
|
|
16468
|
+
* @internal
|
|
16469
|
+
*/
|
|
16470
|
+
getRenderQuads(view, targetPxWidth, targetPxHeight) {
|
|
16471
|
+
const base = this.quads;
|
|
16472
|
+
if (base.length === 0) {
|
|
16473
|
+
return base;
|
|
16474
|
+
}
|
|
16475
|
+
const ctx = buildPixelSnapContext(this.getGlobalTransform(), view, targetPxWidth, targetPxHeight);
|
|
16476
|
+
if (!ctx.axisAligned) {
|
|
16477
|
+
return base;
|
|
16478
|
+
}
|
|
16479
|
+
return snapQuadsInto(base, ctx, this._renderQuads);
|
|
16480
|
+
}
|
|
16481
|
+
// -----------------------------------------------------------------------
|
|
16482
|
+
// Private helpers
|
|
16483
|
+
// -----------------------------------------------------------------------
|
|
16484
|
+
_rebuildGeometry() {
|
|
16485
|
+
this._quads = buildNineSliceQuads(this._region, this._slices, this._border, this._width, this._height, this._modes);
|
|
16486
|
+
this._geometryDirty = false;
|
|
16487
|
+
}
|
|
16488
|
+
}
|
|
16489
|
+
|
|
16490
|
+
// ---------------------------------------------------------------------------
|
|
16491
|
+
// Validation
|
|
16492
|
+
// ---------------------------------------------------------------------------
|
|
16493
|
+
const validRepeatModes = new Set(['stretch', 'repeat', 'mirror-repeat']);
|
|
16494
|
+
const validRepeatFits = new Set(['clip', 'round']);
|
|
16495
|
+
function validateSizeInput(width, height) {
|
|
16496
|
+
if (!Number.isFinite(width) || !Number.isFinite(height)) {
|
|
16497
|
+
throw new Error(`RepeatingSprite: width and height must be finite numbers (got ${width}, ${height}).`);
|
|
16498
|
+
}
|
|
16499
|
+
if (width < 0) {
|
|
16500
|
+
throw new Error(`RepeatingSprite: width must be non-negative (got ${width}).`);
|
|
16501
|
+
}
|
|
16502
|
+
if (height < 0) {
|
|
16503
|
+
throw new Error(`RepeatingSprite: height must be non-negative (got ${height}).`);
|
|
16504
|
+
}
|
|
16505
|
+
}
|
|
16506
|
+
function validateMode(mode, label) {
|
|
16507
|
+
if (typeof mode !== 'string' || !validRepeatModes.has(mode)) {
|
|
16508
|
+
throw new Error(`RepeatingSprite: ${label} must be "stretch", "repeat", or "mirror-repeat" (got ${String(mode)}).`);
|
|
16509
|
+
}
|
|
16510
|
+
}
|
|
16511
|
+
function validateFit(fit, label) {
|
|
16512
|
+
if (typeof fit !== 'string' || !validRepeatFits.has(fit)) {
|
|
16513
|
+
throw new Error(`RepeatingSprite: ${label} must be "clip" or "round" (got ${String(fit)}).`);
|
|
16514
|
+
}
|
|
16515
|
+
}
|
|
16516
|
+
function validateOffset(value, label) {
|
|
16517
|
+
if (!Number.isFinite(value)) {
|
|
16518
|
+
throw new Error(`RepeatingSprite: ${label} must be a finite number (got ${value}).`);
|
|
16519
|
+
}
|
|
16520
|
+
}
|
|
16521
|
+
// ---------------------------------------------------------------------------
|
|
16522
|
+
// Shader-path tiling helpers
|
|
16523
|
+
// ---------------------------------------------------------------------------
|
|
16524
|
+
/**
|
|
16525
|
+
* Compute the UV tiling scale for the standalone shader path.
|
|
16526
|
+
*
|
|
16527
|
+
* Returns how many times the source repeats across the destination span.
|
|
16528
|
+
* - `stretch`: always 1.0
|
|
16529
|
+
* - `repeat`/`mirror-repeat` + `round`: nearest integer count (≥ 1)
|
|
16530
|
+
* - `repeat`/`mirror-repeat` + `clip`: exact fraction `destLen / srcLen`
|
|
16531
|
+
* @internal
|
|
16532
|
+
*/
|
|
16533
|
+
function computeShaderTiling(srcLen, destLen, mode, fit) {
|
|
16534
|
+
if (mode === 'stretch' || srcLen <= 0 || destLen <= 0) {
|
|
16535
|
+
return 1;
|
|
16536
|
+
}
|
|
16537
|
+
if (fit === 'round') {
|
|
16538
|
+
return Math.max(1, Math.round(destLen / srcLen));
|
|
16539
|
+
}
|
|
16540
|
+
return destLen / srcLen;
|
|
16541
|
+
}
|
|
16542
|
+
// ---------------------------------------------------------------------------
|
|
16543
|
+
// Geometry-path quad builder
|
|
16544
|
+
// ---------------------------------------------------------------------------
|
|
16545
|
+
const halfTexelInset = 0.5;
|
|
16546
|
+
/**
|
|
16547
|
+
* Build the geometry quads for the atlas-region repeat path.
|
|
16548
|
+
*
|
|
16549
|
+
* Runs {@link planRepeat} independently on both X and Y axes with optional
|
|
16550
|
+
* phase offsets, then generates the Cartesian-product quad list. Each quad
|
|
16551
|
+
* carries UV coordinates clamped inside the region's UV bounds, with
|
|
16552
|
+
* extrusion-aware outer insets.
|
|
16553
|
+
* @internal
|
|
16554
|
+
*/
|
|
16555
|
+
function buildRepeatingSpriteQuads(region, width, height, modeX, modeY, fitX, fitY, offsetX, offsetY) {
|
|
16556
|
+
if (width === 0 || height === 0 || region.width <= 0 || region.height <= 0) {
|
|
16557
|
+
return [];
|
|
16558
|
+
}
|
|
16559
|
+
const tw = region.texture.width;
|
|
16560
|
+
const th = region.texture.height;
|
|
16561
|
+
const ext = region.extrusion;
|
|
16562
|
+
const outerInsetU = (ext.left > 0 || ext.right > 0) ? 0 : halfTexelInset / tw;
|
|
16563
|
+
const outerInsetV = (ext.top > 0 || ext.bottom > 0) ? 0 : halfTexelInset / th;
|
|
16564
|
+
const uMin = region.u0 + outerInsetU;
|
|
16565
|
+
const uMax = region.u1 - outerInsetU;
|
|
16566
|
+
const vMin = region.v0 + outerInsetV;
|
|
16567
|
+
const vMax = region.v1 - outerInsetV;
|
|
16568
|
+
const srcW = region.width;
|
|
16569
|
+
const srcH = region.height;
|
|
16570
|
+
const uRange = uMax - uMin;
|
|
16571
|
+
const vRange = vMax - vMin;
|
|
16572
|
+
const segsX = buildAxisSegmentsWithOffset(srcW, width, modeX, fitX, offsetX);
|
|
16573
|
+
const segsY = buildAxisSegmentsWithOffset(srcH, height, modeY, fitY, offsetY);
|
|
16574
|
+
const quads = [];
|
|
16575
|
+
for (const sy of segsY) {
|
|
16576
|
+
const qy0 = sy.destinationStart;
|
|
16577
|
+
const qy1 = sy.destinationStart + sy.destinationLength;
|
|
16578
|
+
const qv0 = vMin + sy.sourceStart * vRange;
|
|
16579
|
+
const qv1 = vMin + sy.sourceEnd * vRange;
|
|
16580
|
+
for (const sx of segsX) {
|
|
16581
|
+
const qx0 = sx.destinationStart;
|
|
16582
|
+
const qx1 = sx.destinationStart + sx.destinationLength;
|
|
16583
|
+
const qu0 = uMin + sx.sourceStart * uRange;
|
|
16584
|
+
const qu1 = uMin + sx.sourceEnd * uRange;
|
|
16585
|
+
quads.push({ x0: qx0, y0: qy0, x1: qx1, y1: qy1, u0: qu0, v0: qv0, u1: qu1, v1: qv1 });
|
|
16586
|
+
}
|
|
16587
|
+
}
|
|
16588
|
+
return quads;
|
|
16589
|
+
}
|
|
16590
|
+
/**
|
|
16591
|
+
* Build repeat segments for one axis with an optional phase offset.
|
|
16592
|
+
*
|
|
16593
|
+
* The offset shifts the starting phase of the repeat in source-pixel units.
|
|
16594
|
+
* For `stretch` mode the offset is silently ignored.
|
|
16595
|
+
*
|
|
16596
|
+
* Implementation: generates a plan for `(destLen + phase)`, shifts all
|
|
16597
|
+
* segment destinations back by `phase`, and clips to `[0, destLen]`. This
|
|
16598
|
+
* is correct for `clip` fit; for `round` fit the extended-span count may
|
|
16599
|
+
* differ slightly from the zero-offset count — atlas scrolling is
|
|
16600
|
+
* discouraged by design.
|
|
16601
|
+
* @internal
|
|
16602
|
+
*/
|
|
16603
|
+
function buildAxisSegmentsWithOffset(srcLen, destLen, mode, fit, offset) {
|
|
16604
|
+
if (destLen === 0 || srcLen <= 0) {
|
|
16605
|
+
return [];
|
|
16606
|
+
}
|
|
16607
|
+
if (mode === 'stretch') {
|
|
16608
|
+
return [...planRepeat(srcLen, destLen, mode, fit).segments];
|
|
16609
|
+
}
|
|
16610
|
+
const phase = ((offset % srcLen) + srcLen) % srcLen;
|
|
16611
|
+
if (phase === 0) {
|
|
16612
|
+
return [...planRepeat(srcLen, destLen, mode, fit).segments];
|
|
16613
|
+
}
|
|
16614
|
+
const extendedPlan = planRepeat(srcLen, destLen + phase, mode, fit);
|
|
16615
|
+
const result = [];
|
|
16616
|
+
for (const seg of extendedPlan.segments) {
|
|
16617
|
+
const dStart = seg.destinationStart - phase;
|
|
16618
|
+
const dEnd = dStart + seg.destinationLength;
|
|
16619
|
+
if (dEnd <= 0 || dStart >= destLen) {
|
|
16620
|
+
continue;
|
|
16621
|
+
}
|
|
16622
|
+
const clippedStart = Math.max(0, dStart);
|
|
16623
|
+
const clippedEnd = Math.min(destLen, dEnd);
|
|
16624
|
+
const clippedLen = clippedEnd - clippedStart;
|
|
16625
|
+
if (clippedLen <= 0) {
|
|
16626
|
+
continue;
|
|
16627
|
+
}
|
|
16628
|
+
const t0 = (clippedStart - dStart) / seg.destinationLength;
|
|
16629
|
+
const t1 = (clippedEnd - dStart) / seg.destinationLength;
|
|
16630
|
+
const srcRange = seg.sourceEnd - seg.sourceStart;
|
|
16631
|
+
result.push({
|
|
16632
|
+
destinationStart: clippedStart,
|
|
16633
|
+
destinationLength: clippedLen,
|
|
16634
|
+
sourceStart: seg.sourceStart + t0 * srcRange,
|
|
16635
|
+
sourceEnd: seg.sourceStart + t1 * srcRange,
|
|
16636
|
+
mirrored: seg.mirrored,
|
|
16637
|
+
});
|
|
16638
|
+
}
|
|
16639
|
+
return result;
|
|
16640
|
+
}
|
|
16641
|
+
|
|
16642
|
+
/**
|
|
16643
|
+
* A sprite that fills its destination by repeating, mirroring, or stretching
|
|
16644
|
+
* a source texture or atlas region.
|
|
16645
|
+
*
|
|
16646
|
+
* Supports independent per-axis modes (`modeX`, `modeY`) and fit strategies
|
|
16647
|
+
* (`fitX`, `fitY`).
|
|
16648
|
+
*
|
|
16649
|
+
* ## Internal rendering strategy (selected automatically)
|
|
16650
|
+
*
|
|
16651
|
+
* | Source type | Internal path |
|
|
16652
|
+
* |-------------------|-------------------------------------------------------|
|
|
16653
|
+
* | Bare `Texture` | **Shader path** — one quad, GPU sampler repeat wrap. |
|
|
16654
|
+
* | `TextureRegion` | **Geometry path** — Cartesian-product quads, clamped. |
|
|
16655
|
+
*
|
|
16656
|
+
* The public class identity and API do not change based on which path the
|
|
16657
|
+
* renderer uses.
|
|
16658
|
+
*
|
|
16659
|
+
* @stable
|
|
16660
|
+
*/
|
|
16661
|
+
class RepeatingSprite extends Drawable {
|
|
16662
|
+
_source;
|
|
16663
|
+
_region;
|
|
16664
|
+
_width;
|
|
16665
|
+
_height;
|
|
16666
|
+
_modeX;
|
|
16667
|
+
_modeY;
|
|
16668
|
+
_fitX;
|
|
16669
|
+
_fitY;
|
|
16670
|
+
_offsetX;
|
|
16671
|
+
_offsetY;
|
|
16672
|
+
_quads = [];
|
|
16673
|
+
_geometryDirty = true;
|
|
16674
|
+
_renderQuads = [];
|
|
16675
|
+
constructor(source, options) {
|
|
16676
|
+
super();
|
|
16677
|
+
this._source = source;
|
|
16678
|
+
this._region = source instanceof TextureRegion
|
|
16679
|
+
? source
|
|
16680
|
+
: new TextureRegion(source, { x: 0, y: 0, width: source.width, height: source.height });
|
|
16681
|
+
const region = this._region;
|
|
16682
|
+
const opts = options ?? {};
|
|
16683
|
+
const modeX = opts.modeX ?? 'repeat';
|
|
16684
|
+
const modeY = opts.modeY ?? 'repeat';
|
|
16685
|
+
validateMode(modeX, 'modeX');
|
|
16686
|
+
validateMode(modeY, 'modeY');
|
|
16687
|
+
this._modeX = modeX;
|
|
16688
|
+
this._modeY = modeY;
|
|
16689
|
+
const fitX = opts.fitX ?? 'round';
|
|
16690
|
+
const fitY = opts.fitY ?? 'round';
|
|
16691
|
+
validateFit(fitX, 'fitX');
|
|
16692
|
+
validateFit(fitY, 'fitY');
|
|
16693
|
+
this._fitX = fitX;
|
|
16694
|
+
this._fitY = fitY;
|
|
16695
|
+
const offsetX = opts.offsetX ?? 0;
|
|
16696
|
+
const offsetY = opts.offsetY ?? 0;
|
|
16697
|
+
validateOffset(offsetX, 'offsetX');
|
|
16698
|
+
validateOffset(offsetY, 'offsetY');
|
|
16699
|
+
this._offsetX = offsetX;
|
|
16700
|
+
this._offsetY = offsetY;
|
|
16701
|
+
const width = opts.width ?? region.width;
|
|
16702
|
+
const height = opts.height ?? region.height;
|
|
16703
|
+
validateSizeInput(width, height);
|
|
16704
|
+
this._width = width;
|
|
16705
|
+
this._height = height;
|
|
16706
|
+
}
|
|
16707
|
+
// -----------------------------------------------------------------------
|
|
16708
|
+
// Accessors
|
|
16709
|
+
// -----------------------------------------------------------------------
|
|
16710
|
+
/** The original source passed to the constructor. */
|
|
16711
|
+
get source() {
|
|
16712
|
+
return this._source;
|
|
16713
|
+
}
|
|
16714
|
+
/** TextureRegion wrapping the source (bare Texture sources auto-wrapped). */
|
|
16715
|
+
get region() {
|
|
16716
|
+
return this._region;
|
|
16717
|
+
}
|
|
16718
|
+
/** Convenience accessor: the underlying Texture. */
|
|
16719
|
+
get texture() {
|
|
16720
|
+
return this._region.texture;
|
|
16721
|
+
}
|
|
16722
|
+
/** Destination width in local units. */
|
|
16723
|
+
get width() {
|
|
16724
|
+
return this._width;
|
|
16725
|
+
}
|
|
16726
|
+
set width(value) {
|
|
16727
|
+
this.setSize(value, this._height);
|
|
16728
|
+
}
|
|
16729
|
+
/** Destination height in local units. */
|
|
16730
|
+
get height() {
|
|
16731
|
+
return this._height;
|
|
16732
|
+
}
|
|
16733
|
+
set height(value) {
|
|
16734
|
+
this.setSize(this._width, value);
|
|
16735
|
+
}
|
|
16736
|
+
/** Horizontal repeat mode. */
|
|
16737
|
+
get modeX() {
|
|
16738
|
+
return this._modeX;
|
|
16739
|
+
}
|
|
16740
|
+
set modeX(value) {
|
|
16741
|
+
validateMode(value, 'modeX');
|
|
16742
|
+
if (this._modeX !== value) {
|
|
16743
|
+
this._modeX = value;
|
|
16744
|
+
this._geometryDirty = true;
|
|
16745
|
+
this.invalidateCache();
|
|
16746
|
+
}
|
|
16747
|
+
}
|
|
16748
|
+
/** Vertical repeat mode. */
|
|
16749
|
+
get modeY() {
|
|
16750
|
+
return this._modeY;
|
|
16751
|
+
}
|
|
16752
|
+
set modeY(value) {
|
|
16753
|
+
validateMode(value, 'modeY');
|
|
16754
|
+
if (this._modeY !== value) {
|
|
16755
|
+
this._modeY = value;
|
|
16756
|
+
this._geometryDirty = true;
|
|
16757
|
+
this.invalidateCache();
|
|
16758
|
+
}
|
|
16759
|
+
}
|
|
16760
|
+
/** Horizontal fit mode (how partial repeats are resolved). */
|
|
16761
|
+
get fitX() {
|
|
16762
|
+
return this._fitX;
|
|
16763
|
+
}
|
|
16764
|
+
set fitX(value) {
|
|
16765
|
+
validateFit(value, 'fitX');
|
|
16766
|
+
if (this._fitX !== value) {
|
|
16767
|
+
this._fitX = value;
|
|
16768
|
+
this._geometryDirty = true;
|
|
16769
|
+
this.invalidateCache();
|
|
16770
|
+
}
|
|
16771
|
+
}
|
|
16772
|
+
/** Vertical fit mode (how partial repeats are resolved). */
|
|
16773
|
+
get fitY() {
|
|
16774
|
+
return this._fitY;
|
|
16775
|
+
}
|
|
16776
|
+
set fitY(value) {
|
|
16777
|
+
validateFit(value, 'fitY');
|
|
16778
|
+
if (this._fitY !== value) {
|
|
16779
|
+
this._fitY = value;
|
|
16780
|
+
this._geometryDirty = true;
|
|
16781
|
+
this.invalidateCache();
|
|
16782
|
+
}
|
|
16783
|
+
}
|
|
16784
|
+
/**
|
|
16785
|
+
* Horizontal scroll offset in source-pixel units.
|
|
16786
|
+
*
|
|
16787
|
+
* On the shader path (bare `Texture` source), changing the offset does
|
|
16788
|
+
* **not** trigger a geometry rebuild — the value is forwarded as
|
|
16789
|
+
* per-instance data at render time. On the geometry path
|
|
16790
|
+
* (`TextureRegion` source), offset changes mark quads dirty.
|
|
16791
|
+
*/
|
|
16792
|
+
get offsetX() {
|
|
16793
|
+
return this._offsetX;
|
|
16794
|
+
}
|
|
16795
|
+
set offsetX(value) {
|
|
16796
|
+
this.setOffset(value, this._offsetY);
|
|
16797
|
+
}
|
|
16798
|
+
/** Vertical scroll offset in source-pixel units. See {@link offsetX}. */
|
|
16799
|
+
get offsetY() {
|
|
16800
|
+
return this._offsetY;
|
|
16801
|
+
}
|
|
16802
|
+
set offsetY(value) {
|
|
16803
|
+
this.setOffset(this._offsetX, value);
|
|
16804
|
+
}
|
|
16805
|
+
// -----------------------------------------------------------------------
|
|
16806
|
+
// Fluent mutators
|
|
16807
|
+
// -----------------------------------------------------------------------
|
|
16808
|
+
/** Set destination size atomically. Preserves prior state on invalid input. */
|
|
16809
|
+
setSize(width, height) {
|
|
16810
|
+
validateSizeInput(width, height);
|
|
16811
|
+
if (this._width !== width || this._height !== height) {
|
|
16812
|
+
this._width = width;
|
|
16813
|
+
this._height = height;
|
|
16814
|
+
this._geometryDirty = true;
|
|
16815
|
+
this.invalidateCache();
|
|
16816
|
+
}
|
|
16817
|
+
return this;
|
|
16818
|
+
}
|
|
16819
|
+
/**
|
|
16820
|
+
* Set scroll offset atomically in source-pixel units.
|
|
16821
|
+
* On the shader path this does not rebuild geometry.
|
|
16822
|
+
* On the geometry path this marks geometry dirty.
|
|
16823
|
+
*/
|
|
16824
|
+
setOffset(offsetX, offsetY) {
|
|
16825
|
+
validateOffset(offsetX, 'offsetX');
|
|
16826
|
+
validateOffset(offsetY, 'offsetY');
|
|
16827
|
+
if (this._offsetX !== offsetX || this._offsetY !== offsetY) {
|
|
16828
|
+
this._offsetX = offsetX;
|
|
16829
|
+
this._offsetY = offsetY;
|
|
16830
|
+
if (this.resolvedStrategy === 'geometry') {
|
|
16831
|
+
this._geometryDirty = true;
|
|
16832
|
+
}
|
|
16833
|
+
this.invalidateCache();
|
|
16834
|
+
}
|
|
16835
|
+
return this;
|
|
16836
|
+
}
|
|
16837
|
+
// -----------------------------------------------------------------------
|
|
16838
|
+
// Bounds
|
|
16839
|
+
// -----------------------------------------------------------------------
|
|
16840
|
+
getLocalBounds() {
|
|
16841
|
+
const bounds = super.getLocalBounds();
|
|
16842
|
+
bounds.set(0, 0, this._width, this._height);
|
|
16843
|
+
return bounds;
|
|
16844
|
+
}
|
|
16845
|
+
// -----------------------------------------------------------------------
|
|
16846
|
+
// Internal renderer interface
|
|
16847
|
+
// -----------------------------------------------------------------------
|
|
16848
|
+
/**
|
|
16849
|
+
* Rendering strategy determined by the source type:
|
|
16850
|
+
* - `'shader'` — bare `Texture`; renderer uses GPU sampler wrap.
|
|
16851
|
+
* - `'geometry'` — `TextureRegion`; renderer builds repeat quads.
|
|
16852
|
+
* @internal
|
|
16853
|
+
*/
|
|
16854
|
+
get resolvedStrategy() {
|
|
16855
|
+
return this._source instanceof TextureRegion ? 'geometry' : 'shader';
|
|
16856
|
+
}
|
|
16857
|
+
/**
|
|
16858
|
+
* Lazily-built geometry quads for the geometry path.
|
|
16859
|
+
* Returns an empty array on the shader path.
|
|
16860
|
+
* @internal
|
|
16861
|
+
*/
|
|
16862
|
+
get quads() {
|
|
16863
|
+
if (this.resolvedStrategy === 'geometry' && this._geometryDirty) {
|
|
16864
|
+
this._rebuildGeometry();
|
|
16865
|
+
}
|
|
16866
|
+
return this._quads;
|
|
16867
|
+
}
|
|
16868
|
+
/**
|
|
16869
|
+
* Render-time quads for the **geometry strategy**, device-pixel-snapped in
|
|
16870
|
+
* `'geometry'` pixel-snap mode (axis-aligned only). Like NineSlice, every
|
|
16871
|
+
* shared repeat-segment boundary is snapped once by {@link snapQuadsInto}, so
|
|
16872
|
+
* adjacent segments stay gap-free; the content cache ({@link quads}) is never
|
|
16873
|
+
* rebuilt by snapping. Returns the unsnapped quads otherwise.
|
|
16874
|
+
* @internal
|
|
16875
|
+
*/
|
|
16876
|
+
getRenderQuads(view, targetPxWidth, targetPxHeight) {
|
|
16877
|
+
const base = this.quads;
|
|
16878
|
+
if (this.pixelSnapMode !== 'geometry' || base.length === 0) {
|
|
16879
|
+
return base;
|
|
16880
|
+
}
|
|
16881
|
+
const ctx = buildPixelSnapContext(this.getGlobalTransform(), view, targetPxWidth, targetPxHeight);
|
|
16882
|
+
if (!ctx.axisAligned) {
|
|
16883
|
+
return base;
|
|
16884
|
+
}
|
|
16885
|
+
return snapQuadsInto(base, ctx, this._renderQuads);
|
|
16886
|
+
}
|
|
16887
|
+
/**
|
|
16888
|
+
* Render-time destination bounds for the **shader strategy**, written into
|
|
16889
|
+
* `out`. In `'geometry'` pixel-snap mode (axis-aligned only) the destination
|
|
16890
|
+
* quad edges are snapped to the device grid; repetition stays shader-based, so
|
|
16891
|
+
* only the outer rectangle moves. Returns the logical local bounds otherwise.
|
|
16892
|
+
* @internal
|
|
16893
|
+
*/
|
|
16894
|
+
getRenderBounds(view, targetPxWidth, targetPxHeight, out) {
|
|
16895
|
+
const base = this.getLocalBounds();
|
|
16896
|
+
if (this.pixelSnapMode !== 'geometry') {
|
|
16897
|
+
return base;
|
|
16898
|
+
}
|
|
16899
|
+
const ctx = buildPixelSnapContext(this.getGlobalTransform(), view, targetPxWidth, targetPxHeight);
|
|
16900
|
+
if (!ctx.axisAligned) {
|
|
16901
|
+
return base;
|
|
16902
|
+
}
|
|
16903
|
+
return snapBoundsInto(base, ctx, out);
|
|
16904
|
+
}
|
|
16905
|
+
// -----------------------------------------------------------------------
|
|
16906
|
+
// Private helpers
|
|
16907
|
+
// -----------------------------------------------------------------------
|
|
16908
|
+
_rebuildGeometry() {
|
|
16909
|
+
this._quads = buildRepeatingSpriteQuads(this._region, this._width, this._height, this._modeX, this._modeY, this._fitX, this._fitY, this._offsetX, this._offsetY);
|
|
16910
|
+
this._geometryDirty = false;
|
|
16911
|
+
}
|
|
16912
|
+
}
|
|
16913
|
+
|
|
15390
16914
|
/**
|
|
15391
16915
|
* Internal dirty-flag bitmask used by {@link Sprite} to lazily recompute
|
|
15392
16916
|
* derived data (vertices, normals, texture coordinates, bounding boxes).
|
|
@@ -15501,6 +17025,28 @@ class Sprite extends Drawable {
|
|
|
15501
17025
|
}
|
|
15502
17026
|
return this._vertices;
|
|
15503
17027
|
}
|
|
17028
|
+
/**
|
|
17029
|
+
* Local-space quad bounds for the active pass, written into `out`. In
|
|
17030
|
+
* `'geometry'` pixel-snap mode (and only when the combined node+view transform
|
|
17031
|
+
* is axis-aligned) the quad edges are snapped to the render target's
|
|
17032
|
+
* device-pixel grid via the shared {@link snapLocalBoundary} helper — combined
|
|
17033
|
+
* with the device-snapped origin from the transform seam, all four corners
|
|
17034
|
+
* land on whole device pixels. The logical local bounds (used by collision /
|
|
17035
|
+
* `getBounds`) are never changed. Returns the unsnapped local bounds for
|
|
17036
|
+
* `'none'`/`'position'` or under a rotation/skew downgrade.
|
|
17037
|
+
* @internal
|
|
17038
|
+
*/
|
|
17039
|
+
getRenderBounds(view, targetPxWidth, targetPxHeight, out) {
|
|
17040
|
+
const base = this.getLocalBounds();
|
|
17041
|
+
if (this.pixelSnapMode !== 'geometry') {
|
|
17042
|
+
return base;
|
|
17043
|
+
}
|
|
17044
|
+
const ctx = buildPixelSnapContext(this.getGlobalTransform(), view, targetPxWidth, targetPxHeight);
|
|
17045
|
+
if (!ctx.axisAligned) {
|
|
17046
|
+
return base;
|
|
17047
|
+
}
|
|
17048
|
+
return snapBoundsInto(base, ctx, out);
|
|
17049
|
+
}
|
|
15504
17050
|
/**
|
|
15505
17051
|
* Packed UV coordinates for the four quad corners, encoded as two
|
|
15506
17052
|
* 16-bit fixed-point values per element (low 16 bits = U, high 16 bits = V,
|
|
@@ -18308,16 +19854,16 @@ const initialIndexCapacity$2 = 192;
|
|
|
18308
19854
|
const initialNodeIndexCapacity = 64;
|
|
18309
19855
|
const defaultVertexColor = 0xffffffff; // white, full alpha
|
|
18310
19856
|
const maxCustomTextureSlots$2 = 8;
|
|
18311
|
-
const transformTextureUnit$
|
|
19857
|
+
const transformTextureUnit$3 = 8;
|
|
18312
19858
|
class WebGl2MeshRenderer extends AbstractWebGl2Renderer {
|
|
18313
19859
|
_defaultShader = new Shader(vertexSource$3, fragmentSource$3);
|
|
18314
19860
|
_customShaders = new Map();
|
|
18315
19861
|
_compatibilityCache = new Map();
|
|
18316
19862
|
_textureUnitScratch = new Int32Array([0]);
|
|
18317
|
-
_transformUnitScratch = new Int32Array([transformTextureUnit$
|
|
19863
|
+
_transformUnitScratch = new Int32Array([transformTextureUnit$3]);
|
|
18318
19864
|
// Pre-built texture-unit indices used for custom-shader sampler bindings;
|
|
18319
19865
|
// pre-allocated so the per-frame uniform path stays allocation-free.
|
|
18320
|
-
_slotScratches = Array.from({ length: Math.max(transformTextureUnit$
|
|
19866
|
+
_slotScratches = Array.from({ length: Math.max(transformTextureUnit$3 + 1, maxCustomTextureSlots$2 + 1) }, (_, i) => new Int32Array([i]));
|
|
18321
19867
|
_vertexCapacity = initialVertexCapacity$2;
|
|
18322
19868
|
_indexCapacity = initialIndexCapacity$2;
|
|
18323
19869
|
_nodeIndexCapacity = initialNodeIndexCapacity;
|
|
@@ -18544,7 +20090,7 @@ class WebGl2MeshRenderer extends AbstractWebGl2Renderer {
|
|
|
18544
20090
|
shader.getUniform('u_projection').setValue(backend.view.getTransform().toArray(false));
|
|
18545
20091
|
}
|
|
18546
20092
|
if (shader.uniforms.has('u_transforms')) {
|
|
18547
|
-
backend.bindTransformBufferTexture(transformTextureUnit$
|
|
20093
|
+
backend.bindTransformBufferTexture(transformTextureUnit$3, maxNodeIndex + 1);
|
|
18548
20094
|
shader.getUniform('u_transforms').setValue(this._transformUnitScratch);
|
|
18549
20095
|
}
|
|
18550
20096
|
if (shader.uniforms.has('u_texture')) {
|
|
@@ -18895,6 +20441,828 @@ class WebGl2MeshRenderer extends AbstractWebGl2Renderer {
|
|
|
18895
20441
|
}
|
|
18896
20442
|
}
|
|
18897
20443
|
|
|
20444
|
+
const nineSliceVertexSource = `#version 300 es
|
|
20445
|
+
precision lowp float;
|
|
20446
|
+
precision highp int;
|
|
20447
|
+
|
|
20448
|
+
// Per-instance attributes (divisor = 1). One entry per nine-slice quad.
|
|
20449
|
+
// gl_VertexID 0..3 selects which corner of the quad this invocation computes.
|
|
20450
|
+
layout(location = 0) in vec4 a_quadBounds; // x0, y0, x1, y1 (local space)
|
|
20451
|
+
layout(location = 1) in vec4 a_uvBounds; // u0, v0, u1, v1 (normalised, flipY pre-applied)
|
|
20452
|
+
layout(location = 2) in vec4 a_color; // RGBA tint
|
|
20453
|
+
layout(location = 3) in uint a_nodeIndex; // row into the shared transform buffer
|
|
20454
|
+
|
|
20455
|
+
uniform mat3 u_projection;
|
|
20456
|
+
uniform sampler2D u_transforms; // shared per-frame transform buffer (3 texels/row)
|
|
20457
|
+
|
|
20458
|
+
out vec2 v_texcoord;
|
|
20459
|
+
out vec4 v_color;
|
|
20460
|
+
|
|
20461
|
+
void main(void) {
|
|
20462
|
+
// gl_VertexID 0..3 -> corner: 0=TL, 1=TR, 2=BL, 3=BR (TRIANGLE_STRIP order)
|
|
20463
|
+
int vid = gl_VertexID;
|
|
20464
|
+
int cornerX = vid & 1;
|
|
20465
|
+
int cornerY = (vid >> 1) & 1;
|
|
20466
|
+
|
|
20467
|
+
float localX = (cornerX == 0) ? a_quadBounds.x : a_quadBounds.z;
|
|
20468
|
+
float localY = (cornerY == 0) ? a_quadBounds.y : a_quadBounds.w;
|
|
20469
|
+
|
|
20470
|
+
int row = int(a_nodeIndex);
|
|
20471
|
+
vec4 m0 = texelFetch(u_transforms, ivec2(0, row), 0); // a, b, c, d
|
|
20472
|
+
vec4 m1 = texelFetch(u_transforms, ivec2(1, row), 0); // tx, ty, 0, 0
|
|
20473
|
+
|
|
20474
|
+
float worldX = (m0.x * localX) + (m0.y * localY) + m1.x;
|
|
20475
|
+
float worldY = (m0.z * localX) + (m0.w * localY) + m1.y;
|
|
20476
|
+
|
|
20477
|
+
gl_Position = vec4((u_projection * vec3(worldX, worldY, 1.0)).xy, 0.0, 1.0);
|
|
20478
|
+
|
|
20479
|
+
float u = (cornerX == 0) ? a_uvBounds.x : a_uvBounds.z;
|
|
20480
|
+
float v = (cornerY == 0) ? a_uvBounds.y : a_uvBounds.w;
|
|
20481
|
+
v_texcoord = vec2(u, v);
|
|
20482
|
+
|
|
20483
|
+
v_color = vec4(a_color.rgb * a_color.a, a_color.a);
|
|
20484
|
+
}`;
|
|
20485
|
+
const nineSliceFragmentSource = `#version 300 es
|
|
20486
|
+
precision lowp float;
|
|
20487
|
+
|
|
20488
|
+
uniform sampler2D u_texture;
|
|
20489
|
+
|
|
20490
|
+
in vec2 v_texcoord;
|
|
20491
|
+
in vec4 v_color;
|
|
20492
|
+
|
|
20493
|
+
layout(location = 0) out vec4 fragColor;
|
|
20494
|
+
|
|
20495
|
+
void main(void) {
|
|
20496
|
+
vec4 sampleColor = texture(u_texture, v_texcoord);
|
|
20497
|
+
fragColor = sampleColor * v_color;
|
|
20498
|
+
}`;
|
|
20499
|
+
const instanceStrideBytes$3 = 32;
|
|
20500
|
+
const wordsPerInstance$3 = instanceStrideBytes$3 / Uint32Array.BYTES_PER_ELEMENT;
|
|
20501
|
+
const transformTextureUnit$2 = 1;
|
|
20502
|
+
/** Instanced renderer for {@link NineSliceSprite} using WebGL2. */
|
|
20503
|
+
class WebGl2NineSliceSpriteRenderer extends AbstractWebGl2Renderer {
|
|
20504
|
+
_shader;
|
|
20505
|
+
_batchSize;
|
|
20506
|
+
_instanceData;
|
|
20507
|
+
_instanceFloat32;
|
|
20508
|
+
_instanceUint32;
|
|
20509
|
+
_transformUnitScratch = new Int32Array([transformTextureUnit$2]);
|
|
20510
|
+
_quadIndex = 0;
|
|
20511
|
+
_maxNodeIndex = 0;
|
|
20512
|
+
_currentBlendMode = null;
|
|
20513
|
+
_currentTexture = null;
|
|
20514
|
+
_currentView = null;
|
|
20515
|
+
_currentViewId = -1;
|
|
20516
|
+
_instanceBuffer = null;
|
|
20517
|
+
_vao = null;
|
|
20518
|
+
_connection = null;
|
|
20519
|
+
constructor(batchSize) {
|
|
20520
|
+
super();
|
|
20521
|
+
this._batchSize = batchSize;
|
|
20522
|
+
this._shader = new Shader(nineSliceVertexSource, nineSliceFragmentSource);
|
|
20523
|
+
this._instanceData = new ArrayBuffer(batchSize * instanceStrideBytes$3);
|
|
20524
|
+
this._instanceFloat32 = new Float32Array(this._instanceData);
|
|
20525
|
+
this._instanceUint32 = new Uint32Array(this._instanceData);
|
|
20526
|
+
}
|
|
20527
|
+
render(sprite) {
|
|
20528
|
+
const backend = this.getBackend();
|
|
20529
|
+
let quads = sprite.quads;
|
|
20530
|
+
if (sprite.pixelSnapMode === 'geometry') {
|
|
20531
|
+
const snap = backend._getSnapPixelSize();
|
|
20532
|
+
quads = sprite.getRenderQuads(backend.view, snap.width, snap.height);
|
|
20533
|
+
}
|
|
20534
|
+
if (quads.length === 0) {
|
|
20535
|
+
return;
|
|
20536
|
+
}
|
|
20537
|
+
const texture = sprite.texture;
|
|
20538
|
+
const blendMode = sprite.blendMode;
|
|
20539
|
+
const tintRgba = sprite.tint.toRgba();
|
|
20540
|
+
const command = backend.activeDrawCommand;
|
|
20541
|
+
const nodeIndex = command !== null ? command.nodeIndex : backend._pushTransform(sprite);
|
|
20542
|
+
const textureChanged = this._currentTexture !== null && texture !== this._currentTexture;
|
|
20543
|
+
const blendModeChanged = blendMode !== this._currentBlendMode;
|
|
20544
|
+
// If the batch would overflow with current quads + new quads, flush first.
|
|
20545
|
+
if (this._quadIndex > 0) {
|
|
20546
|
+
if (blendModeChanged || textureChanged || this._quadIndex + quads.length > this._batchSize) {
|
|
20547
|
+
this.flush();
|
|
20548
|
+
}
|
|
20549
|
+
}
|
|
20550
|
+
// Establish blend and texture state (may have been cleared by flush).
|
|
20551
|
+
if (this._currentBlendMode === null || this._currentBlendMode !== blendMode) {
|
|
20552
|
+
this._currentBlendMode = blendMode;
|
|
20553
|
+
backend.setBlendMode(blendMode);
|
|
20554
|
+
}
|
|
20555
|
+
if (this._currentTexture !== texture) {
|
|
20556
|
+
this._currentTexture = texture;
|
|
20557
|
+
backend.bindTexture(texture, 0);
|
|
20558
|
+
}
|
|
20559
|
+
// A single sprite may produce more quads than the fixed batch buffer can hold.
|
|
20560
|
+
// Process in [start, end) chunks, flushing between each. Iterating by index
|
|
20561
|
+
// range (rather than `quads.slice(...)`) keeps the overflow path allocation-free.
|
|
20562
|
+
let offset = 0;
|
|
20563
|
+
while (offset < quads.length) {
|
|
20564
|
+
const chunkEnd = Math.min(offset + this._batchSize, quads.length);
|
|
20565
|
+
this._writeQuadChunk(quads, offset, chunkEnd, texture, tintRgba, nodeIndex);
|
|
20566
|
+
offset = chunkEnd;
|
|
20567
|
+
if (offset < quads.length) {
|
|
20568
|
+
this.flush();
|
|
20569
|
+
// Re-establish state after flush
|
|
20570
|
+
this._currentBlendMode = blendMode;
|
|
20571
|
+
backend.setBlendMode(blendMode);
|
|
20572
|
+
this._currentTexture = texture;
|
|
20573
|
+
backend.bindTexture(texture, 0);
|
|
20574
|
+
}
|
|
20575
|
+
}
|
|
20576
|
+
}
|
|
20577
|
+
_writeQuadChunk(quads, start, end, texture, tintRgba, nodeIndex) {
|
|
20578
|
+
const f32 = this._instanceFloat32;
|
|
20579
|
+
const u32 = this._instanceUint32;
|
|
20580
|
+
const flipY = texture.flipY;
|
|
20581
|
+
for (let i = start; i < end; i++) {
|
|
20582
|
+
const q = quads[i];
|
|
20583
|
+
const idx = this._quadIndex * wordsPerInstance$3;
|
|
20584
|
+
f32[idx + 0] = q.x0;
|
|
20585
|
+
f32[idx + 1] = q.y0;
|
|
20586
|
+
f32[idx + 2] = q.x1;
|
|
20587
|
+
f32[idx + 3] = q.y1;
|
|
20588
|
+
const uMin = (q.u0 * 0xffff) & 0xffff;
|
|
20589
|
+
const uMax = (q.u1 * 0xffff) & 0xffff;
|
|
20590
|
+
const v0Raw = (q.v0 * 0xffff) & 0xffff;
|
|
20591
|
+
const v1Raw = (q.v1 * 0xffff) & 0xffff;
|
|
20592
|
+
const vMin = flipY ? v1Raw : v0Raw;
|
|
20593
|
+
const vMax = flipY ? v0Raw : v1Raw;
|
|
20594
|
+
u32[idx + 4] = uMin | (vMin << 16);
|
|
20595
|
+
u32[idx + 5] = uMax | (vMax << 16);
|
|
20596
|
+
u32[idx + 6] = tintRgba;
|
|
20597
|
+
u32[idx + 7] = nodeIndex >>> 0;
|
|
20598
|
+
this._quadIndex++;
|
|
20599
|
+
if (nodeIndex > this._maxNodeIndex) {
|
|
20600
|
+
this._maxNodeIndex = nodeIndex;
|
|
20601
|
+
}
|
|
20602
|
+
}
|
|
20603
|
+
}
|
|
20604
|
+
flush() {
|
|
20605
|
+
const backend = this.getBackendOrNull();
|
|
20606
|
+
const instanceBuffer = this._instanceBuffer;
|
|
20607
|
+
const vao = this._vao;
|
|
20608
|
+
if (this._quadIndex === 0 || backend === null || instanceBuffer === null || vao === null) {
|
|
20609
|
+
this._quadIndex = 0;
|
|
20610
|
+
this._maxNodeIndex = 0;
|
|
20611
|
+
return;
|
|
20612
|
+
}
|
|
20613
|
+
const view = backend.view;
|
|
20614
|
+
if (this._currentView !== view || this._currentViewId !== view.updateId) {
|
|
20615
|
+
this._currentView = view;
|
|
20616
|
+
this._currentViewId = view.updateId;
|
|
20617
|
+
this._shader.getUniform('u_projection').setValue(view.getTransform().toArray(false));
|
|
20618
|
+
}
|
|
20619
|
+
if (this._currentTexture !== null) {
|
|
20620
|
+
this._shader.getUniform('u_texture').setValue(new Int32Array([0]));
|
|
20621
|
+
}
|
|
20622
|
+
backend.bindTransformBufferTexture(transformTextureUnit$2, this._maxNodeIndex + 1);
|
|
20623
|
+
this._shader.getUniform('u_transforms').setValue(this._transformUnitScratch);
|
|
20624
|
+
this._shader.sync();
|
|
20625
|
+
backend.bindVertexArrayObject(vao);
|
|
20626
|
+
instanceBuffer.upload(this._instanceFloat32.subarray(0, this._quadIndex * wordsPerInstance$3));
|
|
20627
|
+
vao.drawInstanced(4, 0, this._quadIndex, RenderingPrimitives.TriangleStrip);
|
|
20628
|
+
backend.stats.batches++;
|
|
20629
|
+
backend.stats.drawCalls++;
|
|
20630
|
+
this._quadIndex = 0;
|
|
20631
|
+
this._maxNodeIndex = 0;
|
|
20632
|
+
}
|
|
20633
|
+
onConnect(backend) {
|
|
20634
|
+
const gl = backend.context;
|
|
20635
|
+
this._shader.connect(createWebGl2ShaderProgram(gl));
|
|
20636
|
+
this._connection = this._createConnection(gl);
|
|
20637
|
+
this._instanceBuffer = new WebGl2RenderBuffer(BufferTypes.ArrayBuffer, this._instanceData, BufferUsage.DynamicDraw).connect(this._createBufferRuntime(this._connection));
|
|
20638
|
+
this._shader.sync();
|
|
20639
|
+
this._vao = new WebGl2VertexArrayObject(RenderingPrimitives.TriangleStrip)
|
|
20640
|
+
.addAttribute(this._instanceBuffer, this._shader.getAttribute('a_quadBounds'), gl.FLOAT, false, instanceStrideBytes$3, 0, false, 1)
|
|
20641
|
+
.addAttribute(this._instanceBuffer, this._shader.getAttribute('a_uvBounds'), gl.UNSIGNED_SHORT, true, instanceStrideBytes$3, 16, false, 1)
|
|
20642
|
+
.addAttribute(this._instanceBuffer, this._shader.getAttribute('a_color'), gl.UNSIGNED_BYTE, true, instanceStrideBytes$3, 24, false, 1)
|
|
20643
|
+
.addAttribute(this._instanceBuffer, this._shader.getAttribute('a_nodeIndex'), gl.UNSIGNED_INT, false, instanceStrideBytes$3, 28, true, 1)
|
|
20644
|
+
.connect(this._createVaoRuntime(this._connection));
|
|
20645
|
+
}
|
|
20646
|
+
onDisconnect() {
|
|
20647
|
+
this._shader.disconnect();
|
|
20648
|
+
this._instanceBuffer?.destroy();
|
|
20649
|
+
this._instanceBuffer = null;
|
|
20650
|
+
this._vao?.destroy();
|
|
20651
|
+
this._vao = null;
|
|
20652
|
+
this._connection = null;
|
|
20653
|
+
this._currentBlendMode = null;
|
|
20654
|
+
this._currentTexture = null;
|
|
20655
|
+
this._currentView = null;
|
|
20656
|
+
this._currentViewId = -1;
|
|
20657
|
+
this._quadIndex = 0;
|
|
20658
|
+
this._maxNodeIndex = 0;
|
|
20659
|
+
}
|
|
20660
|
+
destroy() {
|
|
20661
|
+
this.disconnect();
|
|
20662
|
+
this._shader.destroy();
|
|
20663
|
+
}
|
|
20664
|
+
_createConnection(gl) {
|
|
20665
|
+
const vaoHandle = gl.createVertexArray();
|
|
20666
|
+
if (vaoHandle === null) {
|
|
20667
|
+
throw new Error('WebGl2NineSliceSpriteRenderer: could not create vertex array object.');
|
|
20668
|
+
}
|
|
20669
|
+
return { gl, buffers: new Map(), vaoHandle };
|
|
20670
|
+
}
|
|
20671
|
+
_createBufferRuntime(connection) {
|
|
20672
|
+
const handle = connection.gl.createBuffer();
|
|
20673
|
+
if (handle === null) {
|
|
20674
|
+
throw new Error('WebGl2NineSliceSpriteRenderer: could not create render buffer.');
|
|
20675
|
+
}
|
|
20676
|
+
return {
|
|
20677
|
+
bind: (buffer) => {
|
|
20678
|
+
connection.gl.bindBuffer(buffer.type, handle);
|
|
20679
|
+
},
|
|
20680
|
+
upload: (buffer, offset) => {
|
|
20681
|
+
const gl = connection.gl;
|
|
20682
|
+
const data = buffer.data;
|
|
20683
|
+
const state = connection.buffers.get(buffer);
|
|
20684
|
+
gl.bindBuffer(buffer.type, handle);
|
|
20685
|
+
if (state && state.dataByteLength >= data.byteLength) {
|
|
20686
|
+
gl.bufferSubData(buffer.type, offset, data);
|
|
20687
|
+
state.dataByteLength = data.byteLength;
|
|
20688
|
+
}
|
|
20689
|
+
else {
|
|
20690
|
+
gl.bufferData(buffer.type, data, buffer.usage);
|
|
20691
|
+
connection.buffers.set(buffer, { handle, dataByteLength: data.byteLength });
|
|
20692
|
+
}
|
|
20693
|
+
},
|
|
20694
|
+
destroy: (buffer) => {
|
|
20695
|
+
connection.gl.deleteBuffer(handle);
|
|
20696
|
+
connection.buffers.delete(buffer);
|
|
20697
|
+
buffer.disconnect();
|
|
20698
|
+
},
|
|
20699
|
+
};
|
|
20700
|
+
}
|
|
20701
|
+
_createVaoRuntime(connection) {
|
|
20702
|
+
let appliedVersion = -1;
|
|
20703
|
+
return {
|
|
20704
|
+
bind: (vao) => {
|
|
20705
|
+
const gl = connection.gl;
|
|
20706
|
+
gl.bindVertexArray(connection.vaoHandle);
|
|
20707
|
+
if (appliedVersion !== vao.version) {
|
|
20708
|
+
let lastBuffer = null;
|
|
20709
|
+
for (const attribute of vao.attributes) {
|
|
20710
|
+
if (lastBuffer !== attribute.buffer) {
|
|
20711
|
+
attribute.buffer.bind();
|
|
20712
|
+
lastBuffer = attribute.buffer;
|
|
20713
|
+
}
|
|
20714
|
+
if (attribute.integer) {
|
|
20715
|
+
gl.vertexAttribIPointer(attribute.location, attribute.size, attribute.type, attribute.stride, attribute.start);
|
|
20716
|
+
}
|
|
20717
|
+
else {
|
|
20718
|
+
gl.vertexAttribPointer(attribute.location, attribute.size, attribute.type, attribute.normalized, attribute.stride, attribute.start);
|
|
20719
|
+
}
|
|
20720
|
+
gl.enableVertexAttribArray(attribute.location);
|
|
20721
|
+
gl.vertexAttribDivisor(attribute.location, attribute.divisor);
|
|
20722
|
+
}
|
|
20723
|
+
appliedVersion = vao.version;
|
|
20724
|
+
}
|
|
20725
|
+
},
|
|
20726
|
+
unbind: () => {
|
|
20727
|
+
connection.gl.bindVertexArray(null);
|
|
20728
|
+
},
|
|
20729
|
+
draw: (_vao, size, start, type) => {
|
|
20730
|
+
connection.gl.drawArrays(type, start, size);
|
|
20731
|
+
},
|
|
20732
|
+
drawInstanced: (_vao, count, start, instanceCount, type) => {
|
|
20733
|
+
connection.gl.drawArraysInstanced(type, start, count, instanceCount);
|
|
20734
|
+
},
|
|
20735
|
+
destroy: (vao) => {
|
|
20736
|
+
connection.gl.deleteVertexArray(connection.vaoHandle);
|
|
20737
|
+
vao.disconnect();
|
|
20738
|
+
},
|
|
20739
|
+
};
|
|
20740
|
+
}
|
|
20741
|
+
}
|
|
20742
|
+
|
|
20743
|
+
// ---------------------------------------------------------------------------
|
|
20744
|
+
// Shader path: one quad per sprite, UVs computed in vertex shader.
|
|
20745
|
+
// ---------------------------------------------------------------------------
|
|
20746
|
+
const shaderPathVertSource = `#version 300 es
|
|
20747
|
+
precision mediump float;
|
|
20748
|
+
precision highp int;
|
|
20749
|
+
|
|
20750
|
+
layout(location = 0) in vec4 a_quadBounds; // x0,y0,x1,y1 (local space)
|
|
20751
|
+
layout(location = 1) in vec4 a_uvParams; // tilingX, tilingY, offsetU, offsetV
|
|
20752
|
+
layout(location = 2) in vec4 a_color; // RGBA tint (normalised)
|
|
20753
|
+
layout(location = 3) in uint a_nodeIndex; // transform row
|
|
20754
|
+
|
|
20755
|
+
uniform mat3 u_projection;
|
|
20756
|
+
uniform sampler2D u_transforms;
|
|
20757
|
+
|
|
20758
|
+
out vec2 v_texcoord;
|
|
20759
|
+
out vec4 v_color;
|
|
20760
|
+
|
|
20761
|
+
void main(void) {
|
|
20762
|
+
int vid = gl_VertexID;
|
|
20763
|
+
int cx = vid & 1;
|
|
20764
|
+
int cy = (vid >> 1) & 1;
|
|
20765
|
+
|
|
20766
|
+
float lx = (cx == 0) ? a_quadBounds.x : a_quadBounds.z;
|
|
20767
|
+
float ly = (cy == 0) ? a_quadBounds.y : a_quadBounds.w;
|
|
20768
|
+
|
|
20769
|
+
float destW = a_quadBounds.z - a_quadBounds.x;
|
|
20770
|
+
float destH = a_quadBounds.w - a_quadBounds.y;
|
|
20771
|
+
|
|
20772
|
+
int row = int(a_nodeIndex);
|
|
20773
|
+
vec4 m0 = texelFetch(u_transforms, ivec2(0, row), 0);
|
|
20774
|
+
vec4 m1 = texelFetch(u_transforms, ivec2(1, row), 0);
|
|
20775
|
+
|
|
20776
|
+
float wx = m0.x * lx + m0.y * ly + m1.x;
|
|
20777
|
+
float wy = m0.z * lx + m0.w * ly + m1.y;
|
|
20778
|
+
gl_Position = vec4((u_projection * vec3(wx, wy, 1.0)).xy, 0.0, 1.0);
|
|
20779
|
+
|
|
20780
|
+
float u = (destW > 0.0)
|
|
20781
|
+
? ((lx - a_quadBounds.x) / destW) * a_uvParams.x + a_uvParams.z
|
|
20782
|
+
: a_uvParams.z;
|
|
20783
|
+
float v = (destH > 0.0)
|
|
20784
|
+
? ((ly - a_quadBounds.y) / destH) * a_uvParams.y + a_uvParams.w
|
|
20785
|
+
: a_uvParams.w;
|
|
20786
|
+
v_texcoord = vec2(u, v);
|
|
20787
|
+
|
|
20788
|
+
v_color = vec4(a_color.rgb * a_color.a, a_color.a);
|
|
20789
|
+
}`;
|
|
20790
|
+
// ---------------------------------------------------------------------------
|
|
20791
|
+
// Geometry path: N quads per sprite, UVs pre-computed in CPU (like NineSlice).
|
|
20792
|
+
// ---------------------------------------------------------------------------
|
|
20793
|
+
const geoPathVertSource = `#version 300 es
|
|
20794
|
+
precision lowp float;
|
|
20795
|
+
precision highp int;
|
|
20796
|
+
|
|
20797
|
+
layout(location = 0) in vec4 a_quadBounds; // x0,y0,x1,y1 (local space)
|
|
20798
|
+
layout(location = 1) in vec4 a_uvBounds; // u0,v0,u1,v1 (normalised, flipY pre-applied)
|
|
20799
|
+
layout(location = 2) in vec4 a_color; // RGBA tint
|
|
20800
|
+
layout(location = 3) in uint a_nodeIndex; // transform row
|
|
20801
|
+
|
|
20802
|
+
uniform mat3 u_projection;
|
|
20803
|
+
uniform sampler2D u_transforms;
|
|
20804
|
+
|
|
20805
|
+
out vec2 v_texcoord;
|
|
20806
|
+
out vec4 v_color;
|
|
20807
|
+
|
|
20808
|
+
void main(void) {
|
|
20809
|
+
int vid = gl_VertexID;
|
|
20810
|
+
int cx = vid & 1;
|
|
20811
|
+
int cy = (vid >> 1) & 1;
|
|
20812
|
+
|
|
20813
|
+
float lx = (cx == 0) ? a_quadBounds.x : a_quadBounds.z;
|
|
20814
|
+
float ly = (cy == 0) ? a_quadBounds.y : a_quadBounds.w;
|
|
20815
|
+
|
|
20816
|
+
int row = int(a_nodeIndex);
|
|
20817
|
+
vec4 m0 = texelFetch(u_transforms, ivec2(0, row), 0);
|
|
20818
|
+
vec4 m1 = texelFetch(u_transforms, ivec2(1, row), 0);
|
|
20819
|
+
|
|
20820
|
+
float wx = m0.x * lx + m0.y * ly + m1.x;
|
|
20821
|
+
float wy = m0.z * lx + m0.w * ly + m1.y;
|
|
20822
|
+
gl_Position = vec4((u_projection * vec3(wx, wy, 1.0)).xy, 0.0, 1.0);
|
|
20823
|
+
|
|
20824
|
+
float u = (cx == 0) ? a_uvBounds.x : a_uvBounds.z;
|
|
20825
|
+
float v = (cy == 0) ? a_uvBounds.y : a_uvBounds.w;
|
|
20826
|
+
v_texcoord = vec2(u, v);
|
|
20827
|
+
|
|
20828
|
+
v_color = vec4(a_color.rgb * a_color.a, a_color.a);
|
|
20829
|
+
}`;
|
|
20830
|
+
const sharedFragSource = `#version 300 es
|
|
20831
|
+
precision lowp float;
|
|
20832
|
+
|
|
20833
|
+
uniform sampler2D u_texture;
|
|
20834
|
+
|
|
20835
|
+
in vec2 v_texcoord;
|
|
20836
|
+
in vec4 v_color;
|
|
20837
|
+
|
|
20838
|
+
layout(location = 0) out vec4 fragColor;
|
|
20839
|
+
|
|
20840
|
+
void main(void) {
|
|
20841
|
+
fragColor = texture(u_texture, v_texcoord) * v_color;
|
|
20842
|
+
}`;
|
|
20843
|
+
// ---------------------------------------------------------------------------
|
|
20844
|
+
// Layout constants
|
|
20845
|
+
// ---------------------------------------------------------------------------
|
|
20846
|
+
const shaderStrideBytes$1 = 40; // 10 × float32
|
|
20847
|
+
const shaderWordsPerInstance = shaderStrideBytes$1 / Uint32Array.BYTES_PER_ELEMENT;
|
|
20848
|
+
const geoStrideBytes$1 = 32; // 8 × uint32 (matches NineSlice layout)
|
|
20849
|
+
const geoWordsPerInstance = geoStrideBytes$1 / Uint32Array.BYTES_PER_ELEMENT;
|
|
20850
|
+
const transformTextureUnit$1 = 1;
|
|
20851
|
+
// ---------------------------------------------------------------------------
|
|
20852
|
+
// Sampler cache helper
|
|
20853
|
+
// ---------------------------------------------------------------------------
|
|
20854
|
+
function repeatModeToWrap(mode) {
|
|
20855
|
+
if (mode === 'repeat')
|
|
20856
|
+
return WrapModes.Repeat;
|
|
20857
|
+
if (mode === 'mirror-repeat')
|
|
20858
|
+
return WrapModes.MirroredRepeat;
|
|
20859
|
+
return WrapModes.ClampToEdge;
|
|
20860
|
+
}
|
|
20861
|
+
/** Instanced renderer for {@link RepeatingSprite} using WebGL2. Handles both shader and geometry paths internally. */
|
|
20862
|
+
class WebGl2RepeatingSpriteRenderer extends AbstractWebGl2Renderer {
|
|
20863
|
+
_shaderPathShader;
|
|
20864
|
+
_geoPathShader;
|
|
20865
|
+
_batchSize;
|
|
20866
|
+
// Shader-path buffers
|
|
20867
|
+
_shaderData;
|
|
20868
|
+
_shaderF32;
|
|
20869
|
+
_shaderU32;
|
|
20870
|
+
_shaderBuf = null;
|
|
20871
|
+
_shaderVao = null;
|
|
20872
|
+
_shaderQuadCount = 0;
|
|
20873
|
+
// Geometry-path buffers
|
|
20874
|
+
_geoData;
|
|
20875
|
+
_geoF32;
|
|
20876
|
+
_geoU32;
|
|
20877
|
+
_geoBuf = null;
|
|
20878
|
+
_geoVao = null;
|
|
20879
|
+
_geoQuadCount = 0;
|
|
20880
|
+
// Sampler cache keyed by "wrapS:wrapT:scaleMode"
|
|
20881
|
+
_samplers = new Map();
|
|
20882
|
+
// Shared batch state
|
|
20883
|
+
_maxNodeIndex = 0;
|
|
20884
|
+
_currentTexture = null;
|
|
20885
|
+
_currentBlendMode = null;
|
|
20886
|
+
_currentModeX = null;
|
|
20887
|
+
_currentModeY = null;
|
|
20888
|
+
_currentPath = null;
|
|
20889
|
+
_connection = null;
|
|
20890
|
+
_transformUnitScratch = new Int32Array([transformTextureUnit$1]);
|
|
20891
|
+
_snapBounds = new Rectangle();
|
|
20892
|
+
_currentView = null;
|
|
20893
|
+
_currentViewId = -1;
|
|
20894
|
+
constructor(batchSize) {
|
|
20895
|
+
super();
|
|
20896
|
+
this._batchSize = batchSize;
|
|
20897
|
+
this._shaderPathShader = new Shader(shaderPathVertSource, sharedFragSource);
|
|
20898
|
+
this._geoPathShader = new Shader(geoPathVertSource, sharedFragSource);
|
|
20899
|
+
this._shaderData = new ArrayBuffer(batchSize * shaderStrideBytes$1);
|
|
20900
|
+
this._shaderF32 = new Float32Array(this._shaderData);
|
|
20901
|
+
this._shaderU32 = new Uint32Array(this._shaderData);
|
|
20902
|
+
this._geoData = new ArrayBuffer(batchSize * geoStrideBytes$1);
|
|
20903
|
+
this._geoF32 = new Float32Array(this._geoData);
|
|
20904
|
+
this._geoU32 = new Uint32Array(this._geoData);
|
|
20905
|
+
}
|
|
20906
|
+
render(sprite) {
|
|
20907
|
+
const strategy = sprite.resolvedStrategy;
|
|
20908
|
+
const texture = sprite.texture;
|
|
20909
|
+
const blendMode = sprite.blendMode;
|
|
20910
|
+
const modeX = sprite.modeX;
|
|
20911
|
+
const modeY = sprite.modeY;
|
|
20912
|
+
const hasData = this._shaderQuadCount > 0 || this._geoQuadCount > 0;
|
|
20913
|
+
if (hasData) {
|
|
20914
|
+
const pathChanged = this._currentPath !== strategy;
|
|
20915
|
+
const texChanged = this._currentTexture !== texture;
|
|
20916
|
+
const blendChanged = this._currentBlendMode !== blendMode;
|
|
20917
|
+
const modeChanged = strategy === 'shader'
|
|
20918
|
+
&& (this._currentModeX !== modeX || this._currentModeY !== modeY);
|
|
20919
|
+
if (pathChanged || texChanged || blendChanged || modeChanged) {
|
|
20920
|
+
this.flush();
|
|
20921
|
+
}
|
|
20922
|
+
}
|
|
20923
|
+
const backend = this.getBackend();
|
|
20924
|
+
if (this._currentTexture !== texture) {
|
|
20925
|
+
this._currentTexture = texture;
|
|
20926
|
+
backend.bindTexture(texture, 0);
|
|
20927
|
+
}
|
|
20928
|
+
if (this._currentBlendMode !== blendMode) {
|
|
20929
|
+
this._currentBlendMode = blendMode;
|
|
20930
|
+
backend.setBlendMode(blendMode);
|
|
20931
|
+
}
|
|
20932
|
+
this._currentPath = strategy;
|
|
20933
|
+
const command = backend.activeDrawCommand;
|
|
20934
|
+
const nodeIndex = command !== null ? command.nodeIndex : backend._pushTransform(sprite);
|
|
20935
|
+
if (nodeIndex > this._maxNodeIndex) {
|
|
20936
|
+
this._maxNodeIndex = nodeIndex;
|
|
20937
|
+
}
|
|
20938
|
+
if (strategy === 'shader') {
|
|
20939
|
+
this._currentModeX = modeX;
|
|
20940
|
+
this._currentModeY = modeY;
|
|
20941
|
+
this._writeShaderInstance(sprite, nodeIndex);
|
|
20942
|
+
}
|
|
20943
|
+
else {
|
|
20944
|
+
this._writeGeoQuads(sprite, nodeIndex);
|
|
20945
|
+
}
|
|
20946
|
+
}
|
|
20947
|
+
_writeShaderInstance(sprite, nodeIndex) {
|
|
20948
|
+
const texture = sprite.texture;
|
|
20949
|
+
const srcW = sprite.region.width;
|
|
20950
|
+
const srcH = sprite.region.height;
|
|
20951
|
+
let destW = sprite.width;
|
|
20952
|
+
let destH = sprite.height;
|
|
20953
|
+
const flipY = texture instanceof Texture && texture.flipY;
|
|
20954
|
+
// 'geometry' mode: snap the destination quad to the device grid. Repetition
|
|
20955
|
+
// stays shader-based; only the outer rectangle (and the tiling derived from
|
|
20956
|
+
// it) moves. Position/none leave the destination unchanged.
|
|
20957
|
+
if (sprite.pixelSnapMode === 'geometry') {
|
|
20958
|
+
const backend = this.getBackend();
|
|
20959
|
+
const snap = backend._getSnapPixelSize();
|
|
20960
|
+
const rb = sprite.getRenderBounds(backend.view, snap.width, snap.height, this._snapBounds);
|
|
20961
|
+
destW = rb.width;
|
|
20962
|
+
destH = rb.height;
|
|
20963
|
+
}
|
|
20964
|
+
const tilingX = computeShaderTiling(srcW, destW, sprite.modeX, sprite.fitX);
|
|
20965
|
+
const tilingY = computeShaderTiling(srcH, destH, sprite.modeY, sprite.fitY);
|
|
20966
|
+
const offsetU = sprite.offsetX / (srcW > 0 ? srcW : 1);
|
|
20967
|
+
const offsetV = sprite.offsetY / (srcH > 0 ? srcH : 1);
|
|
20968
|
+
// When flipY, negate tilingY and start from tilingY so V runs top→bottom.
|
|
20969
|
+
const uvParamY = flipY ? -tilingY : tilingY;
|
|
20970
|
+
const uvParamW = flipY ? tilingY + offsetV : offsetV;
|
|
20971
|
+
if (this._shaderQuadCount >= this._batchSize) {
|
|
20972
|
+
this.flush();
|
|
20973
|
+
}
|
|
20974
|
+
const idx = this._shaderQuadCount * shaderWordsPerInstance;
|
|
20975
|
+
const f32 = this._shaderF32;
|
|
20976
|
+
const u32 = this._shaderU32;
|
|
20977
|
+
f32[idx + 0] = 0;
|
|
20978
|
+
f32[idx + 1] = 0;
|
|
20979
|
+
f32[idx + 2] = destW;
|
|
20980
|
+
f32[idx + 3] = destH;
|
|
20981
|
+
f32[idx + 4] = tilingX;
|
|
20982
|
+
f32[idx + 5] = uvParamY;
|
|
20983
|
+
f32[idx + 6] = offsetU;
|
|
20984
|
+
f32[idx + 7] = uvParamW;
|
|
20985
|
+
u32[idx + 8] = sprite.tint.toRgba();
|
|
20986
|
+
u32[idx + 9] = nodeIndex >>> 0;
|
|
20987
|
+
this._shaderQuadCount++;
|
|
20988
|
+
}
|
|
20989
|
+
_writeGeoQuads(sprite, nodeIndex) {
|
|
20990
|
+
let quads = sprite.quads;
|
|
20991
|
+
// 'geometry' mode: snap shared segment boundaries once (gap-free), like NineSlice.
|
|
20992
|
+
if (sprite.pixelSnapMode === 'geometry') {
|
|
20993
|
+
const backend = this.getBackend();
|
|
20994
|
+
const snap = backend._getSnapPixelSize();
|
|
20995
|
+
quads = sprite.getRenderQuads(backend.view, snap.width, snap.height);
|
|
20996
|
+
}
|
|
20997
|
+
const flipY = (sprite.texture instanceof Texture) && sprite.texture.flipY;
|
|
20998
|
+
const tint = sprite.tint.toRgba();
|
|
20999
|
+
let offset = 0;
|
|
21000
|
+
while (offset < quads.length) {
|
|
21001
|
+
const remaining = quads.length - offset;
|
|
21002
|
+
const chunk = Math.min(remaining, this._batchSize - this._geoQuadCount);
|
|
21003
|
+
if (chunk <= 0) {
|
|
21004
|
+
this.flush();
|
|
21005
|
+
// Re-establish texture/blend after flush
|
|
21006
|
+
const backend = this.getBackend();
|
|
21007
|
+
backend.bindTexture(sprite.texture, 0);
|
|
21008
|
+
backend.setBlendMode(sprite.blendMode);
|
|
21009
|
+
this._currentTexture = sprite.texture;
|
|
21010
|
+
this._currentBlendMode = sprite.blendMode;
|
|
21011
|
+
this._currentPath = 'geometry';
|
|
21012
|
+
if (nodeIndex > this._maxNodeIndex) {
|
|
21013
|
+
this._maxNodeIndex = nodeIndex;
|
|
21014
|
+
}
|
|
21015
|
+
continue;
|
|
21016
|
+
}
|
|
21017
|
+
const f32 = this._geoF32;
|
|
21018
|
+
const u32 = this._geoU32;
|
|
21019
|
+
for (let i = 0; i < chunk; i++) {
|
|
21020
|
+
const q = quads[offset + i];
|
|
21021
|
+
const idx = (this._geoQuadCount + i) * geoWordsPerInstance;
|
|
21022
|
+
f32[idx + 0] = q.x0;
|
|
21023
|
+
f32[idx + 1] = q.y0;
|
|
21024
|
+
f32[idx + 2] = q.x1;
|
|
21025
|
+
f32[idx + 3] = q.y1;
|
|
21026
|
+
const uMin = (q.u0 * 0xffff) & 0xffff;
|
|
21027
|
+
const uMax = (q.u1 * 0xffff) & 0xffff;
|
|
21028
|
+
const v0Raw = (q.v0 * 0xffff) & 0xffff;
|
|
21029
|
+
const v1Raw = (q.v1 * 0xffff) & 0xffff;
|
|
21030
|
+
const vMin = flipY ? v1Raw : v0Raw;
|
|
21031
|
+
const vMax = flipY ? v0Raw : v1Raw;
|
|
21032
|
+
u32[idx + 4] = uMin | (vMin << 16);
|
|
21033
|
+
u32[idx + 5] = uMax | (vMax << 16);
|
|
21034
|
+
u32[idx + 6] = tint;
|
|
21035
|
+
u32[idx + 7] = nodeIndex >>> 0;
|
|
21036
|
+
}
|
|
21037
|
+
this._geoQuadCount += chunk;
|
|
21038
|
+
offset += chunk;
|
|
21039
|
+
}
|
|
21040
|
+
}
|
|
21041
|
+
flush() {
|
|
21042
|
+
const backend = this.getBackendOrNull();
|
|
21043
|
+
if (backend === null) {
|
|
21044
|
+
this._resetBatchState();
|
|
21045
|
+
return;
|
|
21046
|
+
}
|
|
21047
|
+
const view = backend.view;
|
|
21048
|
+
if (this._currentView !== view || this._currentViewId !== view.updateId) {
|
|
21049
|
+
this._currentView = view;
|
|
21050
|
+
this._currentViewId = view.updateId;
|
|
21051
|
+
const proj = view.getTransform().toArray(false);
|
|
21052
|
+
this._shaderPathShader.getUniform('u_projection').setValue(proj);
|
|
21053
|
+
this._geoPathShader.getUniform('u_projection').setValue(proj);
|
|
21054
|
+
}
|
|
21055
|
+
if (this._shaderQuadCount > 0) {
|
|
21056
|
+
this._flushShaderBatch(backend);
|
|
21057
|
+
}
|
|
21058
|
+
if (this._geoQuadCount > 0) {
|
|
21059
|
+
this._flushGeoBatch(backend);
|
|
21060
|
+
}
|
|
21061
|
+
this._resetBatchState();
|
|
21062
|
+
}
|
|
21063
|
+
_flushShaderBatch(backend) {
|
|
21064
|
+
const conn = this._connection;
|
|
21065
|
+
const buf = this._shaderBuf;
|
|
21066
|
+
const vao = this._shaderVao;
|
|
21067
|
+
if (!conn || !buf || !vao || this._shaderQuadCount === 0)
|
|
21068
|
+
return;
|
|
21069
|
+
const gl = conn.gl;
|
|
21070
|
+
const texture = this._currentTexture;
|
|
21071
|
+
const scaleMode = (texture instanceof Texture) ? texture.scaleMode : ScaleModes.Linear;
|
|
21072
|
+
const wrapS = repeatModeToWrap(this._currentModeX ?? 'repeat');
|
|
21073
|
+
const wrapT = repeatModeToWrap(this._currentModeY ?? 'repeat');
|
|
21074
|
+
// Bind repeat sampler (overrides texture's own wrap params for this unit).
|
|
21075
|
+
const samplerHandle = this._getOrCreateSampler(gl, wrapS, wrapT, scaleMode);
|
|
21076
|
+
gl.bindSampler(0, samplerHandle);
|
|
21077
|
+
backend.bindTransformBufferTexture(transformTextureUnit$1, this._maxNodeIndex + 1);
|
|
21078
|
+
this._shaderPathShader.getUniform('u_texture').setValue(new Int32Array([0]));
|
|
21079
|
+
this._shaderPathShader.getUniform('u_transforms').setValue(this._transformUnitScratch);
|
|
21080
|
+
this._shaderPathShader.sync();
|
|
21081
|
+
backend.bindVertexArrayObject(vao);
|
|
21082
|
+
buf.upload(this._shaderF32.subarray(0, this._shaderQuadCount * shaderWordsPerInstance));
|
|
21083
|
+
vao.drawInstanced(4, 0, this._shaderQuadCount, RenderingPrimitives.TriangleStrip);
|
|
21084
|
+
backend.stats.batches++;
|
|
21085
|
+
backend.stats.drawCalls++;
|
|
21086
|
+
// Unbind sampler so subsequent draws use the texture's own wrap params.
|
|
21087
|
+
gl.bindSampler(0, null);
|
|
21088
|
+
this._shaderQuadCount = 0;
|
|
21089
|
+
}
|
|
21090
|
+
_flushGeoBatch(backend) {
|
|
21091
|
+
const conn = this._connection;
|
|
21092
|
+
const buf = this._geoBuf;
|
|
21093
|
+
const vao = this._geoVao;
|
|
21094
|
+
if (!conn || !buf || !vao || this._geoQuadCount === 0)
|
|
21095
|
+
return;
|
|
21096
|
+
backend.bindTransformBufferTexture(transformTextureUnit$1, this._maxNodeIndex + 1);
|
|
21097
|
+
this._geoPathShader.getUniform('u_texture').setValue(new Int32Array([0]));
|
|
21098
|
+
this._geoPathShader.getUniform('u_transforms').setValue(this._transformUnitScratch);
|
|
21099
|
+
this._geoPathShader.sync();
|
|
21100
|
+
backend.bindVertexArrayObject(vao);
|
|
21101
|
+
buf.upload(this._geoF32.subarray(0, this._geoQuadCount * geoWordsPerInstance));
|
|
21102
|
+
vao.drawInstanced(4, 0, this._geoQuadCount, RenderingPrimitives.TriangleStrip);
|
|
21103
|
+
backend.stats.batches++;
|
|
21104
|
+
backend.stats.drawCalls++;
|
|
21105
|
+
this._geoQuadCount = 0;
|
|
21106
|
+
}
|
|
21107
|
+
_resetBatchState() {
|
|
21108
|
+
this._shaderQuadCount = 0;
|
|
21109
|
+
this._geoQuadCount = 0;
|
|
21110
|
+
this._maxNodeIndex = 0;
|
|
21111
|
+
this._currentTexture = null;
|
|
21112
|
+
this._currentBlendMode = null;
|
|
21113
|
+
this._currentModeX = null;
|
|
21114
|
+
this._currentModeY = null;
|
|
21115
|
+
this._currentPath = null;
|
|
21116
|
+
}
|
|
21117
|
+
_getOrCreateSampler(gl, wrapS, wrapT, scaleMode) {
|
|
21118
|
+
const key = `${wrapS}:${wrapT}:${scaleMode}`;
|
|
21119
|
+
const existing = this._samplers.get(key);
|
|
21120
|
+
if (existing !== undefined)
|
|
21121
|
+
return existing;
|
|
21122
|
+
const sampler = gl.createSampler();
|
|
21123
|
+
if (sampler === null)
|
|
21124
|
+
throw new Error('WebGl2RepeatingSpriteRenderer: could not create sampler.');
|
|
21125
|
+
gl.samplerParameteri(sampler, gl.TEXTURE_WRAP_S, wrapS);
|
|
21126
|
+
gl.samplerParameteri(sampler, gl.TEXTURE_WRAP_T, wrapT);
|
|
21127
|
+
gl.samplerParameteri(sampler, gl.TEXTURE_MAG_FILTER, scaleMode);
|
|
21128
|
+
gl.samplerParameteri(sampler, gl.TEXTURE_MIN_FILTER, scaleMode);
|
|
21129
|
+
this._samplers.set(key, sampler);
|
|
21130
|
+
return sampler;
|
|
21131
|
+
}
|
|
21132
|
+
onConnect(backend) {
|
|
21133
|
+
const gl = backend.context;
|
|
21134
|
+
this._shaderPathShader.connect(createWebGl2ShaderProgram(gl));
|
|
21135
|
+
this._geoPathShader.connect(createWebGl2ShaderProgram(gl));
|
|
21136
|
+
// sync() triggers finalize() which compiles the shaders and populates the
|
|
21137
|
+
// attributes/uniforms maps — must happen before any getAttribute() call.
|
|
21138
|
+
this._shaderPathShader.sync();
|
|
21139
|
+
this._geoPathShader.sync();
|
|
21140
|
+
const conn = this._createConnection(gl);
|
|
21141
|
+
this._connection = conn;
|
|
21142
|
+
// Shader-path VAO (uses float4 uvParams, not packed unorm16)
|
|
21143
|
+
this._shaderBuf = new WebGl2RenderBuffer(BufferTypes.ArrayBuffer, this._shaderData, BufferUsage.DynamicDraw)
|
|
21144
|
+
.connect(this._createBufRuntime(conn, 'shader'));
|
|
21145
|
+
this._shaderVao = new WebGl2VertexArrayObject(RenderingPrimitives.TriangleStrip)
|
|
21146
|
+
.addAttribute(this._shaderBuf, this._shaderPathShader.getAttribute('a_quadBounds'), gl.FLOAT, false, shaderStrideBytes$1, 0, false, 1)
|
|
21147
|
+
.addAttribute(this._shaderBuf, this._shaderPathShader.getAttribute('a_uvParams'), gl.FLOAT, false, shaderStrideBytes$1, 16, false, 1)
|
|
21148
|
+
.addAttribute(this._shaderBuf, this._shaderPathShader.getAttribute('a_color'), gl.UNSIGNED_BYTE, true, shaderStrideBytes$1, 32, false, 1)
|
|
21149
|
+
.addAttribute(this._shaderBuf, this._shaderPathShader.getAttribute('a_nodeIndex'), gl.UNSIGNED_INT, false, shaderStrideBytes$1, 36, true, 1)
|
|
21150
|
+
.connect(this._createVaoRuntime(conn, 'shader'));
|
|
21151
|
+
// Geometry-path VAO (packed unorm16 UVs, same layout as NineSlice)
|
|
21152
|
+
this._geoBuf = new WebGl2RenderBuffer(BufferTypes.ArrayBuffer, this._geoData, BufferUsage.DynamicDraw)
|
|
21153
|
+
.connect(this._createBufRuntime(conn, 'geo'));
|
|
21154
|
+
this._geoVao = new WebGl2VertexArrayObject(RenderingPrimitives.TriangleStrip)
|
|
21155
|
+
.addAttribute(this._geoBuf, this._geoPathShader.getAttribute('a_quadBounds'), gl.FLOAT, false, geoStrideBytes$1, 0, false, 1)
|
|
21156
|
+
.addAttribute(this._geoBuf, this._geoPathShader.getAttribute('a_uvBounds'), gl.UNSIGNED_SHORT, true, geoStrideBytes$1, 16, false, 1)
|
|
21157
|
+
.addAttribute(this._geoBuf, this._geoPathShader.getAttribute('a_color'), gl.UNSIGNED_BYTE, true, geoStrideBytes$1, 24, false, 1)
|
|
21158
|
+
.addAttribute(this._geoBuf, this._geoPathShader.getAttribute('a_nodeIndex'), gl.UNSIGNED_INT, false, geoStrideBytes$1, 28, true, 1)
|
|
21159
|
+
.connect(this._createVaoRuntime(conn, 'geo'));
|
|
21160
|
+
}
|
|
21161
|
+
onDisconnect() {
|
|
21162
|
+
const gl = this._connection?.gl;
|
|
21163
|
+
if (gl !== undefined) {
|
|
21164
|
+
for (const sampler of this._samplers.values()) {
|
|
21165
|
+
gl.deleteSampler(sampler);
|
|
21166
|
+
}
|
|
21167
|
+
}
|
|
21168
|
+
this._samplers.clear();
|
|
21169
|
+
this._shaderPathShader.disconnect();
|
|
21170
|
+
this._geoPathShader.disconnect();
|
|
21171
|
+
this._shaderBuf?.destroy();
|
|
21172
|
+
this._shaderBuf = null;
|
|
21173
|
+
this._shaderVao?.destroy();
|
|
21174
|
+
this._shaderVao = null;
|
|
21175
|
+
this._geoBuf?.destroy();
|
|
21176
|
+
this._geoBuf = null;
|
|
21177
|
+
this._geoVao?.destroy();
|
|
21178
|
+
this._geoVao = null;
|
|
21179
|
+
this._connection = null;
|
|
21180
|
+
this._currentView = null;
|
|
21181
|
+
this._currentViewId = -1;
|
|
21182
|
+
this._resetBatchState();
|
|
21183
|
+
}
|
|
21184
|
+
destroy() {
|
|
21185
|
+
this.disconnect();
|
|
21186
|
+
this._shaderPathShader.destroy();
|
|
21187
|
+
this._geoPathShader.destroy();
|
|
21188
|
+
}
|
|
21189
|
+
// -----------------------------------------------------------------------
|
|
21190
|
+
// Private GL helpers
|
|
21191
|
+
// -----------------------------------------------------------------------
|
|
21192
|
+
_createConnection(gl) {
|
|
21193
|
+
const shaderVaoHandle = gl.createVertexArray();
|
|
21194
|
+
const geoVaoHandle = gl.createVertexArray();
|
|
21195
|
+
if (shaderVaoHandle === null || geoVaoHandle === null) {
|
|
21196
|
+
throw new Error('WebGl2RepeatingSpriteRenderer: could not create vertex array object.');
|
|
21197
|
+
}
|
|
21198
|
+
return { gl, buffers: new Map(), shaderVaoHandle, geoVaoHandle };
|
|
21199
|
+
}
|
|
21200
|
+
_createBufRuntime(conn, _kind) {
|
|
21201
|
+
const handle = conn.gl.createBuffer();
|
|
21202
|
+
if (handle === null)
|
|
21203
|
+
throw new Error('WebGl2RepeatingSpriteRenderer: could not create render buffer.');
|
|
21204
|
+
return {
|
|
21205
|
+
bind: (buffer) => { conn.gl.bindBuffer(buffer.type, handle); },
|
|
21206
|
+
upload: (buffer, offset) => {
|
|
21207
|
+
const gl = conn.gl;
|
|
21208
|
+
const data = buffer.data;
|
|
21209
|
+
const state = conn.buffers.get(buffer);
|
|
21210
|
+
gl.bindBuffer(buffer.type, handle);
|
|
21211
|
+
if (state && state.dataByteLength >= data.byteLength) {
|
|
21212
|
+
gl.bufferSubData(buffer.type, offset, data);
|
|
21213
|
+
state.dataByteLength = data.byteLength;
|
|
21214
|
+
}
|
|
21215
|
+
else {
|
|
21216
|
+
gl.bufferData(buffer.type, data, buffer.usage);
|
|
21217
|
+
conn.buffers.set(buffer, { handle, dataByteLength: data.byteLength });
|
|
21218
|
+
}
|
|
21219
|
+
},
|
|
21220
|
+
destroy: (buffer) => {
|
|
21221
|
+
conn.gl.deleteBuffer(handle);
|
|
21222
|
+
conn.buffers.delete(buffer);
|
|
21223
|
+
buffer.disconnect();
|
|
21224
|
+
},
|
|
21225
|
+
};
|
|
21226
|
+
}
|
|
21227
|
+
_createVaoRuntime(conn, kind) {
|
|
21228
|
+
const vaoHandle = kind === 'shader' ? conn.shaderVaoHandle : conn.geoVaoHandle;
|
|
21229
|
+
let appliedVersion = -1;
|
|
21230
|
+
return {
|
|
21231
|
+
bind: (vao) => {
|
|
21232
|
+
const gl = conn.gl;
|
|
21233
|
+
gl.bindVertexArray(vaoHandle);
|
|
21234
|
+
if (appliedVersion !== vao.version) {
|
|
21235
|
+
let lastBuffer = null;
|
|
21236
|
+
for (const attr of vao.attributes) {
|
|
21237
|
+
if (lastBuffer !== attr.buffer) {
|
|
21238
|
+
attr.buffer.bind();
|
|
21239
|
+
lastBuffer = attr.buffer;
|
|
21240
|
+
}
|
|
21241
|
+
if (attr.integer) {
|
|
21242
|
+
gl.vertexAttribIPointer(attr.location, attr.size, attr.type, attr.stride, attr.start);
|
|
21243
|
+
}
|
|
21244
|
+
else {
|
|
21245
|
+
gl.vertexAttribPointer(attr.location, attr.size, attr.type, attr.normalized, attr.stride, attr.start);
|
|
21246
|
+
}
|
|
21247
|
+
gl.enableVertexAttribArray(attr.location);
|
|
21248
|
+
gl.vertexAttribDivisor(attr.location, attr.divisor);
|
|
21249
|
+
}
|
|
21250
|
+
appliedVersion = vao.version;
|
|
21251
|
+
}
|
|
21252
|
+
},
|
|
21253
|
+
unbind: () => { conn.gl.bindVertexArray(null); },
|
|
21254
|
+
draw: (_vao, size, start, type) => { conn.gl.drawArrays(type, start, size); },
|
|
21255
|
+
drawInstanced: (_vao, count, start, instanceCount, type) => {
|
|
21256
|
+
conn.gl.drawArraysInstanced(type, start, count, instanceCount);
|
|
21257
|
+
},
|
|
21258
|
+
destroy: (vao) => {
|
|
21259
|
+
conn.gl.deleteVertexArray(vaoHandle);
|
|
21260
|
+
vao.disconnect();
|
|
21261
|
+
},
|
|
21262
|
+
};
|
|
21263
|
+
}
|
|
21264
|
+
}
|
|
21265
|
+
|
|
18898
21266
|
/**
|
|
18899
21267
|
* Canonical engine vertex stage for the custom {@link SpriteMaterial} path.
|
|
18900
21268
|
*
|
|
@@ -19087,8 +21455,8 @@ const maxBatchTextures$1 = 8;
|
|
|
19087
21455
|
// Sprite base textures occupy units 0..7; the shared transform buffer texture
|
|
19088
21456
|
// binds on unit 8, matching the mesh renderer's convention.
|
|
19089
21457
|
const transformTextureUnit = 8;
|
|
19090
|
-
const instanceStrideBytes$
|
|
19091
|
-
const wordsPerInstance$
|
|
21458
|
+
const instanceStrideBytes$2 = 36;
|
|
21459
|
+
const wordsPerInstance$2 = instanceStrideBytes$2 / Uint32Array.BYTES_PER_ELEMENT;
|
|
19092
21460
|
class WebGl2SpriteRenderer extends AbstractWebGl2Renderer {
|
|
19093
21461
|
_shader;
|
|
19094
21462
|
_batchSize;
|
|
@@ -19108,6 +21476,10 @@ class WebGl2SpriteRenderer extends AbstractWebGl2Renderer {
|
|
|
19108
21476
|
_transformUnitScratch = new Int32Array([transformTextureUnit]);
|
|
19109
21477
|
_currentMaterial = null;
|
|
19110
21478
|
_currentBaseTexture = null;
|
|
21479
|
+
// Reusable scratch for device-snapped local bounds ('geometry' mode), and the
|
|
21480
|
+
// bounds resolved for the sprite currently being packed (snapped or logical).
|
|
21481
|
+
_snapBounds = new Rectangle();
|
|
21482
|
+
_activeBounds = null;
|
|
19111
21483
|
_instanceCount = 0;
|
|
19112
21484
|
// Highest transform-buffer row referenced by the pending batch; drives the
|
|
19113
21485
|
// minimum row count uploaded for the transform texture at flush time.
|
|
@@ -19122,7 +21494,7 @@ class WebGl2SpriteRenderer extends AbstractWebGl2Renderer {
|
|
|
19122
21494
|
super();
|
|
19123
21495
|
this._batchSize = batchSize;
|
|
19124
21496
|
this._shader = new Shader(vertexSource$2, fragmentSource$2);
|
|
19125
|
-
this._instanceData = new ArrayBuffer(batchSize * instanceStrideBytes$
|
|
21497
|
+
this._instanceData = new ArrayBuffer(batchSize * instanceStrideBytes$2);
|
|
19126
21498
|
this._instanceFloat32 = new Float32Array(this._instanceData);
|
|
19127
21499
|
this._instanceUint32 = new Uint32Array(this._instanceData);
|
|
19128
21500
|
}
|
|
@@ -19139,6 +21511,7 @@ class WebGl2SpriteRenderer extends AbstractWebGl2Renderer {
|
|
|
19139
21511
|
// sprite's transform into the buffer and use the freshly-allocated slot.
|
|
19140
21512
|
const command = backend.activeDrawCommand;
|
|
19141
21513
|
const nodeIndex = command !== null ? command.nodeIndex : backend._pushTransform(sprite);
|
|
21514
|
+
this._activeBounds = this._resolveBounds(sprite, backend);
|
|
19142
21515
|
if (material === null) {
|
|
19143
21516
|
this._renderDefault(sprite, texture, backend, nodeIndex);
|
|
19144
21517
|
}
|
|
@@ -19147,6 +21520,19 @@ class WebGl2SpriteRenderer extends AbstractWebGl2Renderer {
|
|
|
19147
21520
|
}
|
|
19148
21521
|
return this;
|
|
19149
21522
|
}
|
|
21523
|
+
/**
|
|
21524
|
+
* Local bounds to upload for `sprite` this draw: device-pixel-snapped in
|
|
21525
|
+
* `'geometry'` pixel-snap mode (axis-aligned only), otherwise the sprite's
|
|
21526
|
+
* logical local bounds. Reuses a scratch rectangle and never mutates logical
|
|
21527
|
+
* state. Consumed synchronously by {@link _packInstance}.
|
|
21528
|
+
*/
|
|
21529
|
+
_resolveBounds(sprite, backend) {
|
|
21530
|
+
if (sprite.pixelSnapMode !== 'geometry') {
|
|
21531
|
+
return sprite.getLocalBounds();
|
|
21532
|
+
}
|
|
21533
|
+
const snap = backend._getSnapPixelSize();
|
|
21534
|
+
return sprite.getRenderBounds(backend.view, snap.width, snap.height, this._snapBounds);
|
|
21535
|
+
}
|
|
19150
21536
|
flush() {
|
|
19151
21537
|
const backend = this.getBackendOrNull();
|
|
19152
21538
|
const instanceBuffer = this._instanceBuffer;
|
|
@@ -19190,7 +21576,7 @@ class WebGl2SpriteRenderer extends AbstractWebGl2Renderer {
|
|
|
19190
21576
|
}
|
|
19191
21577
|
shader.sync();
|
|
19192
21578
|
backend.bindVertexArrayObject(vao);
|
|
19193
|
-
instanceBuffer.upload(this._instanceFloat32.subarray(0, this._instanceCount * wordsPerInstance$
|
|
21579
|
+
instanceBuffer.upload(this._instanceFloat32.subarray(0, this._instanceCount * wordsPerInstance$2));
|
|
19194
21580
|
vao.drawInstanced(4, 0, this._instanceCount, RenderingPrimitives.TriangleStrip);
|
|
19195
21581
|
backend.stats.batches++;
|
|
19196
21582
|
backend.stats.drawCalls++;
|
|
@@ -19205,11 +21591,11 @@ class WebGl2SpriteRenderer extends AbstractWebGl2Renderer {
|
|
|
19205
21591
|
this._instanceBuffer = new WebGl2RenderBuffer(BufferTypes.ArrayBuffer, this._instanceData, BufferUsage.DynamicDraw).connect(this._createBufferRuntime(this._connection));
|
|
19206
21592
|
this._shader.sync();
|
|
19207
21593
|
this._vao = new WebGl2VertexArrayObject(RenderingPrimitives.TriangleStrip)
|
|
19208
|
-
.addAttribute(this._instanceBuffer, this._shader.getAttribute('a_localBounds'), gl.FLOAT, false, instanceStrideBytes$
|
|
19209
|
-
.addAttribute(this._instanceBuffer, this._shader.getAttribute('a_uvBounds'), gl.UNSIGNED_SHORT, true, instanceStrideBytes$
|
|
19210
|
-
.addAttribute(this._instanceBuffer, this._shader.getAttribute('a_color'), gl.UNSIGNED_BYTE, true, instanceStrideBytes$
|
|
19211
|
-
.addAttribute(this._instanceBuffer, this._shader.getAttribute('a_textureSlot'), gl.UNSIGNED_INT, false, instanceStrideBytes$
|
|
19212
|
-
.addAttribute(this._instanceBuffer, this._shader.getAttribute('a_nodeIndex'), gl.UNSIGNED_INT, false, instanceStrideBytes$
|
|
21594
|
+
.addAttribute(this._instanceBuffer, this._shader.getAttribute('a_localBounds'), gl.FLOAT, false, instanceStrideBytes$2, 0, false, 1)
|
|
21595
|
+
.addAttribute(this._instanceBuffer, this._shader.getAttribute('a_uvBounds'), gl.UNSIGNED_SHORT, true, instanceStrideBytes$2, 16, false, 1)
|
|
21596
|
+
.addAttribute(this._instanceBuffer, this._shader.getAttribute('a_color'), gl.UNSIGNED_BYTE, true, instanceStrideBytes$2, 24, false, 1)
|
|
21597
|
+
.addAttribute(this._instanceBuffer, this._shader.getAttribute('a_textureSlot'), gl.UNSIGNED_INT, false, instanceStrideBytes$2, 28, true, 1)
|
|
21598
|
+
.addAttribute(this._instanceBuffer, this._shader.getAttribute('a_nodeIndex'), gl.UNSIGNED_INT, false, instanceStrideBytes$2, 32, true, 1)
|
|
19213
21599
|
.connect(this._createVaoRuntime(this._connection));
|
|
19214
21600
|
// Pin the per-slot sampler uniforms to texture units 0..N-1.
|
|
19215
21601
|
const samplerUnit = new Int32Array(1);
|
|
@@ -19290,11 +21676,12 @@ class WebGl2SpriteRenderer extends AbstractWebGl2Renderer {
|
|
|
19290
21676
|
this._instanceCount++;
|
|
19291
21677
|
}
|
|
19292
21678
|
_packInstance(sprite, texture, slot, nodeIndex) {
|
|
19293
|
-
const offset = this._instanceCount * wordsPerInstance$
|
|
21679
|
+
const offset = this._instanceCount * wordsPerInstance$2;
|
|
19294
21680
|
const f32 = this._instanceFloat32;
|
|
19295
21681
|
const u32 = this._instanceUint32;
|
|
19296
|
-
// localBounds: left, top, right, bottom (offset 0..3)
|
|
19297
|
-
|
|
21682
|
+
// localBounds: left, top, right, bottom (offset 0..3) — device-snapped in
|
|
21683
|
+
// 'geometry' pixel-snap mode, otherwise the logical local bounds.
|
|
21684
|
+
const bounds = this._activeBounds ?? sprite.getLocalBounds();
|
|
19298
21685
|
f32[offset + 0] = bounds.left;
|
|
19299
21686
|
f32[offset + 1] = bounds.top;
|
|
19300
21687
|
f32[offset + 2] = bounds.right;
|
|
@@ -21814,6 +24201,902 @@ function collectTextureBindings$1(material) {
|
|
|
21814
24201
|
return result;
|
|
21815
24202
|
}
|
|
21816
24203
|
|
|
24204
|
+
/// <reference types="@webgpu/types" />
|
|
24205
|
+
const nineSliceShaderSource = `
|
|
24206
|
+
struct ProjectionUniforms {
|
|
24207
|
+
matrix: mat4x4<f32>,
|
|
24208
|
+
};
|
|
24209
|
+
|
|
24210
|
+
struct TransformSlot {
|
|
24211
|
+
m0: vec4<f32>,
|
|
24212
|
+
m1: vec4<f32>,
|
|
24213
|
+
m2: vec4<f32>,
|
|
24214
|
+
};
|
|
24215
|
+
|
|
24216
|
+
@group(0) @binding(0)
|
|
24217
|
+
var<uniform> projection: ProjectionUniforms;
|
|
24218
|
+
@group(0) @binding(1)
|
|
24219
|
+
var<storage, read> transforms: array<TransformSlot>;
|
|
24220
|
+
|
|
24221
|
+
@group(1) @binding(0)
|
|
24222
|
+
var nineSliceTexture: texture_2d<f32>;
|
|
24223
|
+
@group(1) @binding(1)
|
|
24224
|
+
var nineSliceSampler: sampler;
|
|
24225
|
+
|
|
24226
|
+
struct VertexInput {
|
|
24227
|
+
@location(0) quadBounds: vec4<f32>, // x0, y0, x1, y1
|
|
24228
|
+
@location(1) uvBounds: vec4<f32>, // u0, v0, u1, v1 (normalised, flipY pre-applied)
|
|
24229
|
+
@location(2) color: vec4<f32>, // RGBA tint
|
|
24230
|
+
@location(3) nodeIndex: u32, // transform buffer row
|
|
24231
|
+
};
|
|
24232
|
+
|
|
24233
|
+
struct VertexOutput {
|
|
24234
|
+
@builtin(position) position: vec4<f32>,
|
|
24235
|
+
@location(0) texcoord: vec2<f32>,
|
|
24236
|
+
@location(1) color: vec4<f32>,
|
|
24237
|
+
};
|
|
24238
|
+
|
|
24239
|
+
@vertex
|
|
24240
|
+
fn vertexMain(input: VertexInput, @builtin(vertex_index) vid: u32) -> VertexOutput {
|
|
24241
|
+
var output: VertexOutput;
|
|
24242
|
+
|
|
24243
|
+
// vid 0..3 → TL, TR, BR, BL (matches static index buffer [0,1,2,0,2,3])
|
|
24244
|
+
let cornerX = ((vid + 1u) >> 1u) & 1u;
|
|
24245
|
+
let cornerY = vid >> 1u;
|
|
24246
|
+
|
|
24247
|
+
let localX = select(input.quadBounds.x, input.quadBounds.z, cornerX == 1u);
|
|
24248
|
+
let localY = select(input.quadBounds.y, input.quadBounds.w, cornerY == 1u);
|
|
24249
|
+
|
|
24250
|
+
let slot = transforms[input.nodeIndex];
|
|
24251
|
+
let worldX = slot.m0.x * localX + slot.m0.y * localY + slot.m1.x;
|
|
24252
|
+
let worldY = slot.m0.z * localX + slot.m0.w * localY + slot.m1.y;
|
|
24253
|
+
|
|
24254
|
+
output.position = projection.matrix * vec4<f32>(worldX, worldY, 0.0, 1.0);
|
|
24255
|
+
|
|
24256
|
+
let u = select(input.uvBounds.x, input.uvBounds.z, cornerX == 1u);
|
|
24257
|
+
let v = select(input.uvBounds.y, input.uvBounds.w, cornerY == 1u);
|
|
24258
|
+
output.texcoord = vec2<f32>(u, v);
|
|
24259
|
+
|
|
24260
|
+
output.color = vec4(input.color.rgb * input.color.a, input.color.a);
|
|
24261
|
+
|
|
24262
|
+
return output;
|
|
24263
|
+
}
|
|
24264
|
+
|
|
24265
|
+
@fragment
|
|
24266
|
+
fn fragmentMain(input: VertexOutput) -> @location(0) vec4<f32> {
|
|
24267
|
+
let sample = textureSample(nineSliceTexture, nineSliceSampler, input.texcoord);
|
|
24268
|
+
return sample * input.color;
|
|
24269
|
+
}
|
|
24270
|
+
`;
|
|
24271
|
+
const instanceStrideBytes$1 = 32;
|
|
24272
|
+
const wordsPerInstance$1 = instanceStrideBytes$1 / Uint32Array.BYTES_PER_ELEMENT; // = 8
|
|
24273
|
+
const projectionByteLength$2 = 64;
|
|
24274
|
+
const initialBatchCapacity$2 = 32;
|
|
24275
|
+
const indicesPerInstance$1 = 6;
|
|
24276
|
+
const quadIndices$3 = new Uint16Array([0, 1, 2, 0, 2, 3]);
|
|
24277
|
+
/** Instanced renderer for {@link NineSliceSprite} using WebGPU. */
|
|
24278
|
+
class WebGpuNineSliceSpriteRenderer extends AbstractWebGpuRenderer {
|
|
24279
|
+
_projectionData = new Float32Array(projectionByteLength$2 / Float32Array.BYTES_PER_ELEMENT);
|
|
24280
|
+
_device = null;
|
|
24281
|
+
_shaderModule = null;
|
|
24282
|
+
_uniformBindGroupLayout = null;
|
|
24283
|
+
_textureBindGroupLayout = null;
|
|
24284
|
+
_pipelineLayout = null;
|
|
24285
|
+
_uniformBuffer = null;
|
|
24286
|
+
_transformBindGroup = null;
|
|
24287
|
+
_transformStorageBuffer = null;
|
|
24288
|
+
_indexBuffer = null;
|
|
24289
|
+
_instanceBuffer = null;
|
|
24290
|
+
_instanceCapacity = 0;
|
|
24291
|
+
_instanceData = new ArrayBuffer(0);
|
|
24292
|
+
_instanceFloat32 = new Float32Array(this._instanceData);
|
|
24293
|
+
_instanceUint32 = new Uint32Array(this._instanceData);
|
|
24294
|
+
_pipelines = new Map();
|
|
24295
|
+
_quadIndex = 0;
|
|
24296
|
+
_maxNodeIndex = 0;
|
|
24297
|
+
_currentBlendMode = null;
|
|
24298
|
+
_currentTexture = null;
|
|
24299
|
+
onConnect(backend) {
|
|
24300
|
+
if (this._device) {
|
|
24301
|
+
return;
|
|
24302
|
+
}
|
|
24303
|
+
this._device = backend.device;
|
|
24304
|
+
this._shaderModule = this._device.createShaderModule({ code: nineSliceShaderSource });
|
|
24305
|
+
this._uniformBindGroupLayout = this._device.createBindGroupLayout({
|
|
24306
|
+
entries: [
|
|
24307
|
+
{ binding: 0, visibility: GPUShaderStage.VERTEX, buffer: { type: 'uniform' } },
|
|
24308
|
+
{ binding: 1, visibility: GPUShaderStage.VERTEX, buffer: { type: 'read-only-storage' } },
|
|
24309
|
+
],
|
|
24310
|
+
});
|
|
24311
|
+
this._textureBindGroupLayout = this._device.createBindGroupLayout({
|
|
24312
|
+
entries: [
|
|
24313
|
+
{ binding: 0, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: 'float' } },
|
|
24314
|
+
{ binding: 1, visibility: GPUShaderStage.FRAGMENT, sampler: { type: 'filtering' } },
|
|
24315
|
+
],
|
|
24316
|
+
});
|
|
24317
|
+
this._pipelineLayout = this._device.createPipelineLayout({
|
|
24318
|
+
bindGroupLayouts: [this._uniformBindGroupLayout, this._textureBindGroupLayout],
|
|
24319
|
+
});
|
|
24320
|
+
this._uniformBuffer = this._device.createBuffer({
|
|
24321
|
+
size: projectionByteLength$2,
|
|
24322
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
24323
|
+
});
|
|
24324
|
+
this._indexBuffer = this._device.createBuffer({
|
|
24325
|
+
size: quadIndices$3.byteLength,
|
|
24326
|
+
usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST,
|
|
24327
|
+
});
|
|
24328
|
+
this._device.queue.writeBuffer(this._indexBuffer, 0, quadIndices$3.buffer, quadIndices$3.byteOffset, quadIndices$3.byteLength);
|
|
24329
|
+
}
|
|
24330
|
+
onDisconnect() {
|
|
24331
|
+
this._instanceBuffer?.destroy();
|
|
24332
|
+
this._indexBuffer?.destroy();
|
|
24333
|
+
this._uniformBuffer?.destroy();
|
|
24334
|
+
this._pipelines.clear();
|
|
24335
|
+
this._instanceBuffer = null;
|
|
24336
|
+
this._indexBuffer = null;
|
|
24337
|
+
this._transformBindGroup = null;
|
|
24338
|
+
this._transformStorageBuffer = null;
|
|
24339
|
+
this._uniformBuffer = null;
|
|
24340
|
+
this._pipelineLayout = null;
|
|
24341
|
+
this._textureBindGroupLayout = null;
|
|
24342
|
+
this._uniformBindGroupLayout = null;
|
|
24343
|
+
this._shaderModule = null;
|
|
24344
|
+
this._device = null;
|
|
24345
|
+
this._backend = null;
|
|
24346
|
+
this._instanceCapacity = 0;
|
|
24347
|
+
this._instanceData = new ArrayBuffer(0);
|
|
24348
|
+
this._instanceFloat32 = new Float32Array(this._instanceData);
|
|
24349
|
+
this._instanceUint32 = new Uint32Array(this._instanceData);
|
|
24350
|
+
this._quadIndex = 0;
|
|
24351
|
+
this._maxNodeIndex = 0;
|
|
24352
|
+
this._currentBlendMode = null;
|
|
24353
|
+
this._currentTexture = null;
|
|
24354
|
+
}
|
|
24355
|
+
render(sprite) {
|
|
24356
|
+
const backend = this._backend;
|
|
24357
|
+
if (backend === null) {
|
|
24358
|
+
return;
|
|
24359
|
+
}
|
|
24360
|
+
let quads = sprite.quads;
|
|
24361
|
+
if (sprite.pixelSnapMode === 'geometry') {
|
|
24362
|
+
const snap = backend._getSnapPixelSize();
|
|
24363
|
+
quads = sprite.getRenderQuads(backend.view, snap.width, snap.height);
|
|
24364
|
+
}
|
|
24365
|
+
if (quads.length === 0) {
|
|
24366
|
+
return;
|
|
24367
|
+
}
|
|
24368
|
+
const texture = sprite.texture;
|
|
24369
|
+
if (texture.width === 0 || texture.height === 0) {
|
|
24370
|
+
return;
|
|
24371
|
+
}
|
|
24372
|
+
if (texture instanceof Texture && texture.source === null) {
|
|
24373
|
+
return;
|
|
24374
|
+
}
|
|
24375
|
+
const blendMode = sprite.blendMode;
|
|
24376
|
+
const command = backend.activeDrawCommand;
|
|
24377
|
+
const nodeIndex = command !== null ? command.nodeIndex : backend._pushTransform(sprite);
|
|
24378
|
+
const blendModeChanged = this._currentBlendMode !== null && blendMode !== this._currentBlendMode;
|
|
24379
|
+
const textureChanged = this._currentTexture !== null && texture !== this._currentTexture;
|
|
24380
|
+
const willExceed = this._quadIndex + quads.length > this._instanceCapacity && this._instanceCapacity > 0;
|
|
24381
|
+
if ((blendModeChanged || textureChanged || willExceed) && this._quadIndex > 0) {
|
|
24382
|
+
this.flush();
|
|
24383
|
+
}
|
|
24384
|
+
this._currentBlendMode = blendMode;
|
|
24385
|
+
this._currentTexture = texture;
|
|
24386
|
+
backend.setBlendMode(blendMode);
|
|
24387
|
+
this._ensureInstanceCapacity(this._quadIndex + quads.length);
|
|
24388
|
+
const f32 = this._instanceFloat32;
|
|
24389
|
+
const u32 = this._instanceUint32;
|
|
24390
|
+
const flipY = texture instanceof Texture && texture.flipY;
|
|
24391
|
+
for (const q of quads) {
|
|
24392
|
+
const offset = this._quadIndex * wordsPerInstance$1;
|
|
24393
|
+
f32[offset + 0] = q.x0;
|
|
24394
|
+
f32[offset + 1] = q.y0;
|
|
24395
|
+
f32[offset + 2] = q.x1;
|
|
24396
|
+
f32[offset + 3] = q.y1;
|
|
24397
|
+
const uMin = (q.u0 * 0xffff) & 0xffff;
|
|
24398
|
+
const uMax = (q.u1 * 0xffff) & 0xffff;
|
|
24399
|
+
const v0Raw = (q.v0 * 0xffff) & 0xffff;
|
|
24400
|
+
const v1Raw = (q.v1 * 0xffff) & 0xffff;
|
|
24401
|
+
const vMin = flipY ? v1Raw : v0Raw;
|
|
24402
|
+
const vMax = flipY ? v0Raw : v1Raw;
|
|
24403
|
+
u32[offset + 4] = uMin | (vMin << 16);
|
|
24404
|
+
u32[offset + 5] = uMax | (vMax << 16);
|
|
24405
|
+
u32[offset + 6] = sprite.tint.toRgba();
|
|
24406
|
+
u32[offset + 7] = nodeIndex >>> 0;
|
|
24407
|
+
this._quadIndex++;
|
|
24408
|
+
if (nodeIndex > this._maxNodeIndex) {
|
|
24409
|
+
this._maxNodeIndex = nodeIndex;
|
|
24410
|
+
}
|
|
24411
|
+
}
|
|
24412
|
+
}
|
|
24413
|
+
flush() {
|
|
24414
|
+
const backend = this._backend;
|
|
24415
|
+
const device = this._device;
|
|
24416
|
+
const uniformBuffer = this._uniformBuffer;
|
|
24417
|
+
if (!backend || !device || !uniformBuffer) {
|
|
24418
|
+
return;
|
|
24419
|
+
}
|
|
24420
|
+
if (this._quadIndex === 0 && !backend.clearRequested) {
|
|
24421
|
+
return;
|
|
24422
|
+
}
|
|
24423
|
+
const viewMatrix = backend.view.getTransform();
|
|
24424
|
+
this._projectionData.set([viewMatrix.a, viewMatrix.c, 0, 0, viewMatrix.b, viewMatrix.d, 0, 0, 0, 0, 1, 0, viewMatrix.x, viewMatrix.y, 0, viewMatrix.z]);
|
|
24425
|
+
device.queue.writeBuffer(uniformBuffer, 0, this._projectionData.buffer, this._projectionData.byteOffset, this._projectionData.byteLength);
|
|
24426
|
+
const scissor = backend.getScissorRect();
|
|
24427
|
+
const maskClipsAll = scissor !== null && (scissor.width <= 0 || scissor.height <= 0);
|
|
24428
|
+
const pass = backend._passCoordinator.acquirePass().pass;
|
|
24429
|
+
if (this._quadIndex > 0 && !maskClipsAll && this._instanceBuffer !== null && this._indexBuffer !== null && this._currentBlendMode !== null && this._currentTexture !== null) {
|
|
24430
|
+
device.queue.writeBuffer(this._instanceBuffer, 0, this._instanceData, 0, this._quadIndex * instanceStrideBytes$1);
|
|
24431
|
+
const storage = backend.getTransformStorageBuffer(this._maxNodeIndex + 1);
|
|
24432
|
+
const transformBindGroup = this._getOrCreateTransformBindGroup(device, uniformBuffer, storage.buffer);
|
|
24433
|
+
const textureBindGroup = this._createTextureBindGroup(device, backend, this._currentTexture);
|
|
24434
|
+
const stencil = backend._passCoordinator.stencilActive;
|
|
24435
|
+
const pipeline = this._getPipeline(this._currentBlendMode, backend.renderTargetFormat, stencil);
|
|
24436
|
+
pass.setPipeline(pipeline);
|
|
24437
|
+
pass.setBindGroup(0, transformBindGroup);
|
|
24438
|
+
pass.setBindGroup(1, textureBindGroup);
|
|
24439
|
+
pass.setVertexBuffer(0, this._instanceBuffer);
|
|
24440
|
+
pass.setIndexBuffer(this._indexBuffer, 'uint16');
|
|
24441
|
+
pass.drawIndexed(indicesPerInstance$1, this._quadIndex, 0, 0, 0);
|
|
24442
|
+
backend.stats.batches++;
|
|
24443
|
+
backend.stats.drawCalls++;
|
|
24444
|
+
}
|
|
24445
|
+
backend._passCoordinator.endPass();
|
|
24446
|
+
this._quadIndex = 0;
|
|
24447
|
+
this._maxNodeIndex = 0;
|
|
24448
|
+
this._currentBlendMode = null;
|
|
24449
|
+
this._currentTexture = null;
|
|
24450
|
+
}
|
|
24451
|
+
destroy() {
|
|
24452
|
+
this.disconnect();
|
|
24453
|
+
}
|
|
24454
|
+
_getOrCreateTransformBindGroup(device, uniformBuffer, storageBuffer) {
|
|
24455
|
+
if (this._transformBindGroup !== null && this._transformStorageBuffer === storageBuffer) {
|
|
24456
|
+
return this._transformBindGroup;
|
|
24457
|
+
}
|
|
24458
|
+
this._transformStorageBuffer = storageBuffer;
|
|
24459
|
+
this._transformBindGroup = device.createBindGroup({
|
|
24460
|
+
layout: this._uniformBindGroupLayout,
|
|
24461
|
+
entries: [
|
|
24462
|
+
{ binding: 0, resource: { buffer: uniformBuffer } },
|
|
24463
|
+
{ binding: 1, resource: { buffer: storageBuffer } },
|
|
24464
|
+
],
|
|
24465
|
+
});
|
|
24466
|
+
return this._transformBindGroup;
|
|
24467
|
+
}
|
|
24468
|
+
_createTextureBindGroup(device, backend, texture) {
|
|
24469
|
+
const binding = backend.getTextureBinding(texture);
|
|
24470
|
+
return device.createBindGroup({
|
|
24471
|
+
layout: this._textureBindGroupLayout,
|
|
24472
|
+
entries: [
|
|
24473
|
+
{ binding: 0, resource: binding.view },
|
|
24474
|
+
{ binding: 1, resource: binding.sampler },
|
|
24475
|
+
],
|
|
24476
|
+
});
|
|
24477
|
+
}
|
|
24478
|
+
_getPipeline(blendMode, format, stencil) {
|
|
24479
|
+
const key = `${blendMode}:${format}:${stencil ? 's' : 'n'}`;
|
|
24480
|
+
const existing = this._pipelines.get(key);
|
|
24481
|
+
if (existing) {
|
|
24482
|
+
return existing;
|
|
24483
|
+
}
|
|
24484
|
+
if (!this._device || !this._shaderModule || !this._pipelineLayout) {
|
|
24485
|
+
throw new Error('WebGpuNineSliceSpriteRenderer: renderer must be connected first.');
|
|
24486
|
+
}
|
|
24487
|
+
const descriptor = {
|
|
24488
|
+
layout: this._pipelineLayout,
|
|
24489
|
+
vertex: {
|
|
24490
|
+
module: this._shaderModule,
|
|
24491
|
+
entryPoint: 'vertexMain',
|
|
24492
|
+
buffers: [
|
|
24493
|
+
{
|
|
24494
|
+
arrayStride: instanceStrideBytes$1,
|
|
24495
|
+
stepMode: 'instance',
|
|
24496
|
+
attributes: [
|
|
24497
|
+
{ shaderLocation: 0, offset: 0, format: 'float32x4' },
|
|
24498
|
+
{ shaderLocation: 1, offset: 16, format: 'unorm16x4' },
|
|
24499
|
+
{ shaderLocation: 2, offset: 24, format: 'unorm8x4' },
|
|
24500
|
+
{ shaderLocation: 3, offset: 28, format: 'uint32' },
|
|
24501
|
+
],
|
|
24502
|
+
},
|
|
24503
|
+
],
|
|
24504
|
+
},
|
|
24505
|
+
fragment: {
|
|
24506
|
+
module: this._shaderModule,
|
|
24507
|
+
entryPoint: 'fragmentMain',
|
|
24508
|
+
targets: [
|
|
24509
|
+
{
|
|
24510
|
+
format,
|
|
24511
|
+
blend: getWebGpuBlendState(blendMode),
|
|
24512
|
+
writeMask: GPUColorWrite.ALL,
|
|
24513
|
+
},
|
|
24514
|
+
],
|
|
24515
|
+
},
|
|
24516
|
+
primitive: {
|
|
24517
|
+
topology: 'triangle-list',
|
|
24518
|
+
},
|
|
24519
|
+
};
|
|
24520
|
+
if (stencil) {
|
|
24521
|
+
descriptor.depthStencil = stencilContentDepthStencilState();
|
|
24522
|
+
}
|
|
24523
|
+
const pipeline = this._device.createRenderPipeline(descriptor);
|
|
24524
|
+
this._pipelines.set(key, pipeline);
|
|
24525
|
+
return pipeline;
|
|
24526
|
+
}
|
|
24527
|
+
_ensureInstanceCapacity(instanceCount) {
|
|
24528
|
+
if (!this._device || instanceCount <= this._instanceCapacity) {
|
|
24529
|
+
return;
|
|
24530
|
+
}
|
|
24531
|
+
let nextCapacity = Math.max(this._instanceCapacity, initialBatchCapacity$2);
|
|
24532
|
+
while (nextCapacity < instanceCount) {
|
|
24533
|
+
nextCapacity *= 2;
|
|
24534
|
+
}
|
|
24535
|
+
const oldData = this._instanceData;
|
|
24536
|
+
const carryBytes = Math.min(this._quadIndex * instanceStrideBytes$1, oldData.byteLength);
|
|
24537
|
+
const instanceData = new ArrayBuffer(nextCapacity * instanceStrideBytes$1);
|
|
24538
|
+
if (carryBytes > 0) {
|
|
24539
|
+
new Uint8Array(instanceData).set(new Uint8Array(oldData, 0, carryBytes));
|
|
24540
|
+
}
|
|
24541
|
+
const instanceBuffer = this._device.createBuffer({
|
|
24542
|
+
size: instanceData.byteLength,
|
|
24543
|
+
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
|
|
24544
|
+
});
|
|
24545
|
+
this._instanceBuffer?.destroy();
|
|
24546
|
+
this._instanceCapacity = nextCapacity;
|
|
24547
|
+
this._instanceData = instanceData;
|
|
24548
|
+
this._instanceFloat32 = new Float32Array(instanceData);
|
|
24549
|
+
this._instanceUint32 = new Uint32Array(instanceData);
|
|
24550
|
+
this._instanceBuffer = instanceBuffer;
|
|
24551
|
+
}
|
|
24552
|
+
}
|
|
24553
|
+
|
|
24554
|
+
/// <reference types="@webgpu/types" />
|
|
24555
|
+
// ---------------------------------------------------------------------------
|
|
24556
|
+
// Shared WGSL declarations — structs, bindings, and output struct used by
|
|
24557
|
+
// both the shader path and the geometry path entry points.
|
|
24558
|
+
// ---------------------------------------------------------------------------
|
|
24559
|
+
const commonWgsl = `
|
|
24560
|
+
struct ProjectionUniforms {
|
|
24561
|
+
matrix: mat4x4<f32>,
|
|
24562
|
+
};
|
|
24563
|
+
struct TransformSlot {
|
|
24564
|
+
m0: vec4<f32>,
|
|
24565
|
+
m1: vec4<f32>,
|
|
24566
|
+
m2: vec4<f32>,
|
|
24567
|
+
};
|
|
24568
|
+
|
|
24569
|
+
@group(0) @binding(0) var<uniform> projection: ProjectionUniforms;
|
|
24570
|
+
@group(0) @binding(1) var<storage, read> transforms: array<TransformSlot>;
|
|
24571
|
+
@group(1) @binding(0) var spriteTexture: texture_2d<f32>;
|
|
24572
|
+
@group(1) @binding(1) var spriteSampler: sampler;
|
|
24573
|
+
|
|
24574
|
+
struct VOut {
|
|
24575
|
+
@builtin(position) pos: vec4<f32>,
|
|
24576
|
+
@location(0) uv: vec2<f32>,
|
|
24577
|
+
@location(1) color: vec4<f32>,
|
|
24578
|
+
};
|
|
24579
|
+
`;
|
|
24580
|
+
// ---------------------------------------------------------------------------
|
|
24581
|
+
// Shader path WGSL — one quad per sprite, UVs computed in vertex shader.
|
|
24582
|
+
// ---------------------------------------------------------------------------
|
|
24583
|
+
const shaderPathEntries = `
|
|
24584
|
+
struct ShaderVIn {
|
|
24585
|
+
@location(0) quadBounds: vec4<f32>, // x0,y0,x1,y1
|
|
24586
|
+
@location(1) uvParams: vec4<f32>, // tilingX, tilingY, offsetU, offsetV
|
|
24587
|
+
@location(2) color: vec4<f32>, // RGBA tint
|
|
24588
|
+
@location(3) nodeIndex: u32,
|
|
24589
|
+
};
|
|
24590
|
+
|
|
24591
|
+
@vertex
|
|
24592
|
+
fn shaderVert(input: ShaderVIn, @builtin(vertex_index) vid: u32) -> VOut {
|
|
24593
|
+
var out: VOut;
|
|
24594
|
+
let cx = ((vid + 1u) >> 1u) & 1u;
|
|
24595
|
+
let cy = vid >> 1u;
|
|
24596
|
+
let lx = select(input.quadBounds.x, input.quadBounds.z, cx == 1u);
|
|
24597
|
+
let ly = select(input.quadBounds.y, input.quadBounds.w, cy == 1u);
|
|
24598
|
+
|
|
24599
|
+
let destW = input.quadBounds.z - input.quadBounds.x;
|
|
24600
|
+
let destH = input.quadBounds.w - input.quadBounds.y;
|
|
24601
|
+
|
|
24602
|
+
let slot = transforms[input.nodeIndex];
|
|
24603
|
+
let wx = slot.m0.x * lx + slot.m0.y * ly + slot.m1.x;
|
|
24604
|
+
let wy = slot.m0.z * lx + slot.m0.w * ly + slot.m1.y;
|
|
24605
|
+
out.pos = projection.matrix * vec4<f32>(wx, wy, 0.0, 1.0);
|
|
24606
|
+
|
|
24607
|
+
let u = select(input.uvParams.z, ((lx - input.quadBounds.x) / destW) * input.uvParams.x + input.uvParams.z, destW > 0.0);
|
|
24608
|
+
let v = select(input.uvParams.w, ((ly - input.quadBounds.y) / destH) * input.uvParams.y + input.uvParams.w, destH > 0.0);
|
|
24609
|
+
out.uv = vec2<f32>(u, v);
|
|
24610
|
+
out.color = vec4<f32>(input.color.rgb * input.color.a, input.color.a);
|
|
24611
|
+
return out;
|
|
24612
|
+
}
|
|
24613
|
+
|
|
24614
|
+
@fragment
|
|
24615
|
+
fn shaderFrag(input: VOut) -> @location(0) vec4<f32> {
|
|
24616
|
+
return textureSample(spriteTexture, spriteSampler, input.uv) * input.color;
|
|
24617
|
+
}
|
|
24618
|
+
`;
|
|
24619
|
+
// ---------------------------------------------------------------------------
|
|
24620
|
+
// Geometry path WGSL — N quads per sprite, UVs pre-computed in CPU.
|
|
24621
|
+
// ---------------------------------------------------------------------------
|
|
24622
|
+
const geoPathEntries = `
|
|
24623
|
+
struct GeoVIn {
|
|
24624
|
+
@location(0) quadBounds: vec4<f32>, // x0,y0,x1,y1
|
|
24625
|
+
@location(1) uvBounds: vec4<f32>, // u0,v0,u1,v1 (normalised, flipY pre-applied)
|
|
24626
|
+
@location(2) color: vec4<f32>, // RGBA tint
|
|
24627
|
+
@location(3) nodeIndex: u32,
|
|
24628
|
+
};
|
|
24629
|
+
|
|
24630
|
+
@vertex
|
|
24631
|
+
fn geoVert(input: GeoVIn, @builtin(vertex_index) vid: u32) -> VOut {
|
|
24632
|
+
var out: VOut;
|
|
24633
|
+
let cx = ((vid + 1u) >> 1u) & 1u;
|
|
24634
|
+
let cy = vid >> 1u;
|
|
24635
|
+
let lx = select(input.quadBounds.x, input.quadBounds.z, cx == 1u);
|
|
24636
|
+
let ly = select(input.quadBounds.y, input.quadBounds.w, cy == 1u);
|
|
24637
|
+
|
|
24638
|
+
let slot = transforms[input.nodeIndex];
|
|
24639
|
+
let wx = slot.m0.x * lx + slot.m0.y * ly + slot.m1.x;
|
|
24640
|
+
let wy = slot.m0.z * lx + slot.m0.w * ly + slot.m1.y;
|
|
24641
|
+
out.pos = projection.matrix * vec4<f32>(wx, wy, 0.0, 1.0);
|
|
24642
|
+
|
|
24643
|
+
let u = select(input.uvBounds.x, input.uvBounds.z, cx == 1u);
|
|
24644
|
+
let v = select(input.uvBounds.y, input.uvBounds.w, cy == 1u);
|
|
24645
|
+
out.uv = vec2<f32>(u, v);
|
|
24646
|
+
out.color = vec4<f32>(input.color.rgb * input.color.a, input.color.a);
|
|
24647
|
+
return out;
|
|
24648
|
+
}
|
|
24649
|
+
|
|
24650
|
+
@fragment
|
|
24651
|
+
fn geoFrag(input: VOut) -> @location(0) vec4<f32> {
|
|
24652
|
+
return textureSample(spriteTexture, spriteSampler, input.uv) * input.color;
|
|
24653
|
+
}
|
|
24654
|
+
`;
|
|
24655
|
+
// ---------------------------------------------------------------------------
|
|
24656
|
+
// Layout constants
|
|
24657
|
+
// ---------------------------------------------------------------------------
|
|
24658
|
+
const shaderStrideBytes = 40; // float32x4 bounds + float32x4 uvParams + unorm8x4 + uint32
|
|
24659
|
+
const geoStrideBytes = 32; // float32x4 bounds + unorm16x4 + unorm8x4 + uint32 (NineSlice layout)
|
|
24660
|
+
const projectionByteLength$1 = 64;
|
|
24661
|
+
const initialBatchCapacity$1 = 32;
|
|
24662
|
+
const indicesPerInstance = 6;
|
|
24663
|
+
const quadIndices$2 = new Uint16Array([0, 1, 2, 0, 2, 3]);
|
|
24664
|
+
// ---------------------------------------------------------------------------
|
|
24665
|
+
// Sampler address mode helper
|
|
24666
|
+
// ---------------------------------------------------------------------------
|
|
24667
|
+
function repeatModeToAddressMode(mode) {
|
|
24668
|
+
if (mode === 'repeat')
|
|
24669
|
+
return 'repeat';
|
|
24670
|
+
if (mode === 'mirror-repeat')
|
|
24671
|
+
return 'mirror-repeat';
|
|
24672
|
+
return 'clamp-to-edge';
|
|
24673
|
+
}
|
|
24674
|
+
/** Instanced renderer for {@link RepeatingSprite} using WebGPU. */
|
|
24675
|
+
class WebGpuRepeatingSpriteRenderer extends AbstractWebGpuRenderer {
|
|
24676
|
+
_projData = new Float32Array(projectionByteLength$1 / Float32Array.BYTES_PER_ELEMENT);
|
|
24677
|
+
// Shared GPU objects
|
|
24678
|
+
_device = null;
|
|
24679
|
+
_uniformBindGroupLayout = null;
|
|
24680
|
+
_textureBindGroupLayout = null;
|
|
24681
|
+
_shaderModule = null;
|
|
24682
|
+
_uniformBuffer = null;
|
|
24683
|
+
_indexBuffer = null;
|
|
24684
|
+
_transformBindGroup = null;
|
|
24685
|
+
_transformStorageBuf = null;
|
|
24686
|
+
_pipelines = new Map();
|
|
24687
|
+
_samplers = new Map();
|
|
24688
|
+
// Shader-path instance buffer
|
|
24689
|
+
_shaderInstBuf = null;
|
|
24690
|
+
_shaderInstCapacity = 0;
|
|
24691
|
+
_shaderInstData = new ArrayBuffer(0);
|
|
24692
|
+
_shaderInstF32 = new Float32Array(this._shaderInstData);
|
|
24693
|
+
_shaderInstU32 = new Uint32Array(this._shaderInstData);
|
|
24694
|
+
_shaderQuadCount = 0;
|
|
24695
|
+
// Geometry-path instance buffer
|
|
24696
|
+
_geoInstBuf = null;
|
|
24697
|
+
_geoInstCapacity = 0;
|
|
24698
|
+
_geoInstData = new ArrayBuffer(0);
|
|
24699
|
+
_geoInstF32 = new Float32Array(this._geoInstData);
|
|
24700
|
+
_geoInstU32 = new Uint32Array(this._geoInstData);
|
|
24701
|
+
_geoQuadCount = 0;
|
|
24702
|
+
// Shared batch state
|
|
24703
|
+
_maxNodeIndex = 0;
|
|
24704
|
+
_currentTexture = null;
|
|
24705
|
+
_currentBlendMode = null;
|
|
24706
|
+
_currentModeX = null;
|
|
24707
|
+
_currentModeY = null;
|
|
24708
|
+
_currentPath = null;
|
|
24709
|
+
// Reusable scratch for device-snapped bounds ('geometry' mode).
|
|
24710
|
+
_snapBounds = new Rectangle();
|
|
24711
|
+
onConnect(backend) {
|
|
24712
|
+
if (this._device)
|
|
24713
|
+
return;
|
|
24714
|
+
const device = backend.device;
|
|
24715
|
+
this._device = device;
|
|
24716
|
+
this._shaderModule = device.createShaderModule({ code: commonWgsl + shaderPathEntries + geoPathEntries });
|
|
24717
|
+
this._uniformBindGroupLayout = device.createBindGroupLayout({
|
|
24718
|
+
entries: [
|
|
24719
|
+
{ binding: 0, visibility: GPUShaderStage.VERTEX, buffer: { type: 'uniform' } },
|
|
24720
|
+
{ binding: 1, visibility: GPUShaderStage.VERTEX, buffer: { type: 'read-only-storage' } },
|
|
24721
|
+
],
|
|
24722
|
+
});
|
|
24723
|
+
this._textureBindGroupLayout = device.createBindGroupLayout({
|
|
24724
|
+
entries: [
|
|
24725
|
+
{ binding: 0, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: 'float' } },
|
|
24726
|
+
{ binding: 1, visibility: GPUShaderStage.FRAGMENT, sampler: { type: 'filtering' } },
|
|
24727
|
+
],
|
|
24728
|
+
});
|
|
24729
|
+
this._uniformBuffer = device.createBuffer({
|
|
24730
|
+
size: projectionByteLength$1,
|
|
24731
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
24732
|
+
});
|
|
24733
|
+
this._indexBuffer = device.createBuffer({
|
|
24734
|
+
size: quadIndices$2.byteLength,
|
|
24735
|
+
usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST,
|
|
24736
|
+
});
|
|
24737
|
+
device.queue.writeBuffer(this._indexBuffer, 0, quadIndices$2.buffer, quadIndices$2.byteOffset, quadIndices$2.byteLength);
|
|
24738
|
+
}
|
|
24739
|
+
onDisconnect() {
|
|
24740
|
+
this._shaderInstBuf?.destroy();
|
|
24741
|
+
this._geoInstBuf?.destroy();
|
|
24742
|
+
this._indexBuffer?.destroy();
|
|
24743
|
+
this._uniformBuffer?.destroy();
|
|
24744
|
+
this._pipelines.clear();
|
|
24745
|
+
this._samplers.clear();
|
|
24746
|
+
this._shaderInstBuf = null;
|
|
24747
|
+
this._geoInstBuf = null;
|
|
24748
|
+
this._indexBuffer = null;
|
|
24749
|
+
this._uniformBuffer = null;
|
|
24750
|
+
this._transformBindGroup = null;
|
|
24751
|
+
this._transformStorageBuf = null;
|
|
24752
|
+
this._textureBindGroupLayout = null;
|
|
24753
|
+
this._uniformBindGroupLayout = null;
|
|
24754
|
+
this._shaderModule = null;
|
|
24755
|
+
this._device = null;
|
|
24756
|
+
this._backend = null;
|
|
24757
|
+
this._shaderInstCapacity = 0;
|
|
24758
|
+
this._shaderInstData = new ArrayBuffer(0);
|
|
24759
|
+
this._shaderInstF32 = new Float32Array(this._shaderInstData);
|
|
24760
|
+
this._shaderInstU32 = new Uint32Array(this._shaderInstData);
|
|
24761
|
+
this._geoInstCapacity = 0;
|
|
24762
|
+
this._geoInstData = new ArrayBuffer(0);
|
|
24763
|
+
this._geoInstF32 = new Float32Array(this._geoInstData);
|
|
24764
|
+
this._geoInstU32 = new Uint32Array(this._geoInstData);
|
|
24765
|
+
this._shaderQuadCount = 0;
|
|
24766
|
+
this._geoQuadCount = 0;
|
|
24767
|
+
this._maxNodeIndex = 0;
|
|
24768
|
+
this._currentTexture = null;
|
|
24769
|
+
this._currentBlendMode = null;
|
|
24770
|
+
this._currentModeX = null;
|
|
24771
|
+
this._currentModeY = null;
|
|
24772
|
+
this._currentPath = null;
|
|
24773
|
+
}
|
|
24774
|
+
render(sprite) {
|
|
24775
|
+
const backend = this._backend;
|
|
24776
|
+
if (!backend)
|
|
24777
|
+
return;
|
|
24778
|
+
const texture = sprite.texture;
|
|
24779
|
+
if (texture instanceof Texture && texture.source === null)
|
|
24780
|
+
return;
|
|
24781
|
+
if (texture.width === 0 || texture.height === 0)
|
|
24782
|
+
return;
|
|
24783
|
+
const strategy = sprite.resolvedStrategy;
|
|
24784
|
+
const blendMode = sprite.blendMode;
|
|
24785
|
+
const modeX = sprite.modeX;
|
|
24786
|
+
const modeY = sprite.modeY;
|
|
24787
|
+
const hasData = this._shaderQuadCount > 0 || this._geoQuadCount > 0;
|
|
24788
|
+
if (hasData) {
|
|
24789
|
+
const pathChanged = this._currentPath !== strategy;
|
|
24790
|
+
const texChanged = this._currentTexture !== texture;
|
|
24791
|
+
const blendChanged = this._currentBlendMode !== blendMode;
|
|
24792
|
+
const modeChanged = strategy === 'shader'
|
|
24793
|
+
&& (this._currentModeX !== modeX || this._currentModeY !== modeY);
|
|
24794
|
+
if (pathChanged || texChanged || blendChanged || modeChanged) {
|
|
24795
|
+
this.flush();
|
|
24796
|
+
}
|
|
24797
|
+
}
|
|
24798
|
+
this._currentTexture = texture;
|
|
24799
|
+
this._currentBlendMode = blendMode;
|
|
24800
|
+
this._currentPath = strategy;
|
|
24801
|
+
backend.setBlendMode(blendMode);
|
|
24802
|
+
const command = backend.activeDrawCommand;
|
|
24803
|
+
const nodeIndex = command !== null ? command.nodeIndex : backend._pushTransform(sprite);
|
|
24804
|
+
if (nodeIndex > this._maxNodeIndex)
|
|
24805
|
+
this._maxNodeIndex = nodeIndex;
|
|
24806
|
+
if (strategy === 'shader') {
|
|
24807
|
+
this._currentModeX = modeX;
|
|
24808
|
+
this._currentModeY = modeY;
|
|
24809
|
+
this._writeShaderInstance(sprite, nodeIndex);
|
|
24810
|
+
}
|
|
24811
|
+
else {
|
|
24812
|
+
this._writeGeoQuads(sprite, nodeIndex);
|
|
24813
|
+
}
|
|
24814
|
+
}
|
|
24815
|
+
_writeShaderInstance(sprite, nodeIndex) {
|
|
24816
|
+
const texture = sprite.texture;
|
|
24817
|
+
const srcW = sprite.region.width;
|
|
24818
|
+
const srcH = sprite.region.height;
|
|
24819
|
+
let destW = sprite.width;
|
|
24820
|
+
let destH = sprite.height;
|
|
24821
|
+
const flipY = texture instanceof Texture && texture.flipY;
|
|
24822
|
+
// 'geometry' mode: snap the destination quad to the device grid. Repetition
|
|
24823
|
+
// stays shader-based; only the outer rectangle (and the tiling derived from
|
|
24824
|
+
// it) moves. Position/none leave the destination unchanged.
|
|
24825
|
+
if (sprite.pixelSnapMode === 'geometry') {
|
|
24826
|
+
const backend = this.getBackend();
|
|
24827
|
+
const snap = backend._getSnapPixelSize();
|
|
24828
|
+
const rb = sprite.getRenderBounds(backend.view, snap.width, snap.height, this._snapBounds);
|
|
24829
|
+
destW = rb.width;
|
|
24830
|
+
destH = rb.height;
|
|
24831
|
+
}
|
|
24832
|
+
const tilingX = computeShaderTiling(srcW, destW, sprite.modeX, sprite.fitX);
|
|
24833
|
+
const tilingY = computeShaderTiling(srcH, destH, sprite.modeY, sprite.fitY);
|
|
24834
|
+
const offsetU = sprite.offsetX / (srcW > 0 ? srcW : 1);
|
|
24835
|
+
const offsetV = sprite.offsetY / (srcH > 0 ? srcH : 1);
|
|
24836
|
+
const uvParamY = flipY ? -tilingY : tilingY;
|
|
24837
|
+
const uvParamW = flipY ? tilingY + offsetV : offsetV;
|
|
24838
|
+
this._ensureShaderCapacity(this._shaderQuadCount + 1);
|
|
24839
|
+
const words = shaderStrideBytes / 4;
|
|
24840
|
+
const offset = this._shaderQuadCount * words;
|
|
24841
|
+
const f32 = this._shaderInstF32;
|
|
24842
|
+
const u32 = this._shaderInstU32;
|
|
24843
|
+
f32[offset + 0] = 0;
|
|
24844
|
+
f32[offset + 1] = 0;
|
|
24845
|
+
f32[offset + 2] = destW;
|
|
24846
|
+
f32[offset + 3] = destH;
|
|
24847
|
+
f32[offset + 4] = tilingX;
|
|
24848
|
+
f32[offset + 5] = uvParamY;
|
|
24849
|
+
f32[offset + 6] = offsetU;
|
|
24850
|
+
f32[offset + 7] = uvParamW;
|
|
24851
|
+
u32[offset + 8] = sprite.tint.toRgba();
|
|
24852
|
+
u32[offset + 9] = nodeIndex >>> 0;
|
|
24853
|
+
this._shaderQuadCount++;
|
|
24854
|
+
}
|
|
24855
|
+
_writeGeoQuads(sprite, nodeIndex) {
|
|
24856
|
+
let quads = sprite.quads;
|
|
24857
|
+
// 'geometry' mode: snap shared segment boundaries once (gap-free), like NineSlice.
|
|
24858
|
+
if (sprite.pixelSnapMode === 'geometry') {
|
|
24859
|
+
const backend = this.getBackend();
|
|
24860
|
+
const snap = backend._getSnapPixelSize();
|
|
24861
|
+
quads = sprite.getRenderQuads(backend.view, snap.width, snap.height);
|
|
24862
|
+
}
|
|
24863
|
+
if (quads.length === 0)
|
|
24864
|
+
return;
|
|
24865
|
+
const flipY = (sprite.texture instanceof Texture) && sprite.texture.flipY;
|
|
24866
|
+
const tint = sprite.tint.toRgba();
|
|
24867
|
+
const words = geoStrideBytes / 4;
|
|
24868
|
+
this._ensureGeoCapacity(this._geoQuadCount + quads.length);
|
|
24869
|
+
const f32 = this._geoInstF32;
|
|
24870
|
+
const u32 = this._geoInstU32;
|
|
24871
|
+
for (let i = 0; i < quads.length; i++) {
|
|
24872
|
+
const q = quads[i];
|
|
24873
|
+
const offset = (this._geoQuadCount + i) * words;
|
|
24874
|
+
f32[offset + 0] = q.x0;
|
|
24875
|
+
f32[offset + 1] = q.y0;
|
|
24876
|
+
f32[offset + 2] = q.x1;
|
|
24877
|
+
f32[offset + 3] = q.y1;
|
|
24878
|
+
const uMin = (q.u0 * 0xffff) & 0xffff;
|
|
24879
|
+
const uMax = (q.u1 * 0xffff) & 0xffff;
|
|
24880
|
+
const v0Raw = (q.v0 * 0xffff) & 0xffff;
|
|
24881
|
+
const v1Raw = (q.v1 * 0xffff) & 0xffff;
|
|
24882
|
+
const vMin = flipY ? v1Raw : v0Raw;
|
|
24883
|
+
const vMax = flipY ? v0Raw : v1Raw;
|
|
24884
|
+
u32[offset + 4] = uMin | (vMin << 16);
|
|
24885
|
+
u32[offset + 5] = uMax | (vMax << 16);
|
|
24886
|
+
u32[offset + 6] = tint;
|
|
24887
|
+
u32[offset + 7] = nodeIndex >>> 0;
|
|
24888
|
+
}
|
|
24889
|
+
this._geoQuadCount += quads.length;
|
|
24890
|
+
}
|
|
24891
|
+
flush() {
|
|
24892
|
+
const backend = this._backend;
|
|
24893
|
+
const device = this._device;
|
|
24894
|
+
const uniform = this._uniformBuffer;
|
|
24895
|
+
if (!backend || !device || !uniform)
|
|
24896
|
+
return;
|
|
24897
|
+
if (this._shaderQuadCount === 0 && this._geoQuadCount === 0 && !backend.clearRequested) {
|
|
24898
|
+
return;
|
|
24899
|
+
}
|
|
24900
|
+
const vm = backend.view.getTransform();
|
|
24901
|
+
this._projData.set([vm.a, vm.c, 0, 0, vm.b, vm.d, 0, 0, 0, 0, 1, 0, vm.x, vm.y, 0, vm.z]);
|
|
24902
|
+
device.queue.writeBuffer(uniform, 0, this._projData.buffer, this._projData.byteOffset, this._projData.byteLength);
|
|
24903
|
+
const scissor = backend.getScissorRect();
|
|
24904
|
+
const maskClipsAll = scissor !== null && (scissor.width <= 0 || scissor.height <= 0);
|
|
24905
|
+
const pass = backend._passCoordinator.acquirePass().pass;
|
|
24906
|
+
const stencil = backend._passCoordinator.stencilActive;
|
|
24907
|
+
if (this._shaderQuadCount > 0 && !maskClipsAll) {
|
|
24908
|
+
this._drawShaderBatch(device, backend, pass, stencil);
|
|
24909
|
+
}
|
|
24910
|
+
if (this._geoQuadCount > 0 && !maskClipsAll) {
|
|
24911
|
+
this._drawGeoBatch(device, backend, pass, stencil);
|
|
24912
|
+
}
|
|
24913
|
+
backend._passCoordinator.endPass();
|
|
24914
|
+
this._shaderQuadCount = 0;
|
|
24915
|
+
this._geoQuadCount = 0;
|
|
24916
|
+
this._maxNodeIndex = 0;
|
|
24917
|
+
this._currentTexture = null;
|
|
24918
|
+
this._currentBlendMode = null;
|
|
24919
|
+
this._currentModeX = null;
|
|
24920
|
+
this._currentModeY = null;
|
|
24921
|
+
this._currentPath = null;
|
|
24922
|
+
}
|
|
24923
|
+
_drawShaderBatch(device, backend, pass, stencil) {
|
|
24924
|
+
if (!this._shaderInstBuf || !this._indexBuffer || this._currentBlendMode === null || this._currentTexture === null)
|
|
24925
|
+
return;
|
|
24926
|
+
device.queue.writeBuffer(this._shaderInstBuf, 0, this._shaderInstData, 0, this._shaderQuadCount * shaderStrideBytes);
|
|
24927
|
+
const storage = backend.getTransformStorageBuffer(this._maxNodeIndex + 1);
|
|
24928
|
+
const uniformBindGroup = this._getOrCreateTransformBindGroup(device, this._uniformBuffer, storage.buffer);
|
|
24929
|
+
const modeX = this._currentModeX ?? 'repeat';
|
|
24930
|
+
const modeY = this._currentModeY ?? 'repeat';
|
|
24931
|
+
const sampler = this._getOrCreateSampler(device, modeX, modeY);
|
|
24932
|
+
const texView = backend.getTextureBinding(this._currentTexture).view;
|
|
24933
|
+
const textureBindGroup = device.createBindGroup({
|
|
24934
|
+
layout: this._textureBindGroupLayout,
|
|
24935
|
+
entries: [{ binding: 0, resource: texView }, { binding: 1, resource: sampler }],
|
|
24936
|
+
});
|
|
24937
|
+
const pipeline = this._getPipeline('shader', this._currentBlendMode, backend.renderTargetFormat, stencil);
|
|
24938
|
+
pass.setPipeline(pipeline);
|
|
24939
|
+
pass.setBindGroup(0, uniformBindGroup);
|
|
24940
|
+
pass.setBindGroup(1, textureBindGroup);
|
|
24941
|
+
pass.setVertexBuffer(0, this._shaderInstBuf);
|
|
24942
|
+
pass.setIndexBuffer(this._indexBuffer, 'uint16');
|
|
24943
|
+
pass.drawIndexed(indicesPerInstance, this._shaderQuadCount, 0, 0, 0);
|
|
24944
|
+
backend.stats.batches++;
|
|
24945
|
+
backend.stats.drawCalls++;
|
|
24946
|
+
}
|
|
24947
|
+
_drawGeoBatch(device, backend, pass, stencil) {
|
|
24948
|
+
if (!this._geoInstBuf || !this._indexBuffer || this._currentBlendMode === null || this._currentTexture === null)
|
|
24949
|
+
return;
|
|
24950
|
+
device.queue.writeBuffer(this._geoInstBuf, 0, this._geoInstData, 0, this._geoQuadCount * geoStrideBytes);
|
|
24951
|
+
const storage = backend.getTransformStorageBuffer(this._maxNodeIndex + 1);
|
|
24952
|
+
const uniformBindGroup = this._getOrCreateTransformBindGroup(device, this._uniformBuffer, storage.buffer);
|
|
24953
|
+
// Geometry path: use backend's default (clamp) sampler.
|
|
24954
|
+
const binding = backend.getTextureBinding(this._currentTexture);
|
|
24955
|
+
const textureBindGroup = device.createBindGroup({
|
|
24956
|
+
layout: this._textureBindGroupLayout,
|
|
24957
|
+
entries: [{ binding: 0, resource: binding.view }, { binding: 1, resource: binding.sampler }],
|
|
24958
|
+
});
|
|
24959
|
+
const pipeline = this._getPipeline('geo', this._currentBlendMode, backend.renderTargetFormat, stencil);
|
|
24960
|
+
pass.setPipeline(pipeline);
|
|
24961
|
+
pass.setBindGroup(0, uniformBindGroup);
|
|
24962
|
+
pass.setBindGroup(1, textureBindGroup);
|
|
24963
|
+
pass.setVertexBuffer(0, this._geoInstBuf);
|
|
24964
|
+
pass.setIndexBuffer(this._indexBuffer, 'uint16');
|
|
24965
|
+
pass.drawIndexed(indicesPerInstance, this._geoQuadCount, 0, 0, 0);
|
|
24966
|
+
backend.stats.batches++;
|
|
24967
|
+
backend.stats.drawCalls++;
|
|
24968
|
+
}
|
|
24969
|
+
destroy() {
|
|
24970
|
+
this.disconnect();
|
|
24971
|
+
}
|
|
24972
|
+
// -----------------------------------------------------------------------
|
|
24973
|
+
// Private helpers
|
|
24974
|
+
// -----------------------------------------------------------------------
|
|
24975
|
+
_getOrCreateSampler(device, modeX, modeY) {
|
|
24976
|
+
const key = `${modeX}:${modeY}`;
|
|
24977
|
+
const existing = this._samplers.get(key);
|
|
24978
|
+
if (existing)
|
|
24979
|
+
return existing;
|
|
24980
|
+
const sampler = device.createSampler({
|
|
24981
|
+
addressModeU: repeatModeToAddressMode(modeX),
|
|
24982
|
+
addressModeV: repeatModeToAddressMode(modeY),
|
|
24983
|
+
magFilter: 'linear',
|
|
24984
|
+
minFilter: 'linear',
|
|
24985
|
+
});
|
|
24986
|
+
this._samplers.set(key, sampler);
|
|
24987
|
+
return sampler;
|
|
24988
|
+
}
|
|
24989
|
+
_getOrCreateTransformBindGroup(device, uniformBuf, storageBuf) {
|
|
24990
|
+
if (this._transformBindGroup !== null && this._transformStorageBuf === storageBuf) {
|
|
24991
|
+
return this._transformBindGroup;
|
|
24992
|
+
}
|
|
24993
|
+
this._transformStorageBuf = storageBuf;
|
|
24994
|
+
this._transformBindGroup = device.createBindGroup({
|
|
24995
|
+
layout: this._uniformBindGroupLayout,
|
|
24996
|
+
entries: [
|
|
24997
|
+
{ binding: 0, resource: { buffer: uniformBuf } },
|
|
24998
|
+
{ binding: 1, resource: { buffer: storageBuf } },
|
|
24999
|
+
],
|
|
25000
|
+
});
|
|
25001
|
+
return this._transformBindGroup;
|
|
25002
|
+
}
|
|
25003
|
+
_getPipeline(kind, blend, format, stencil) {
|
|
25004
|
+
const key = `${kind}:${blend}:${format}:${stencil ? 's' : 'n'}`;
|
|
25005
|
+
const existing = this._pipelines.get(key);
|
|
25006
|
+
if (existing)
|
|
25007
|
+
return existing;
|
|
25008
|
+
if (!this._device || !this._shaderModule || !this._uniformBindGroupLayout || !this._textureBindGroupLayout) {
|
|
25009
|
+
throw new Error('WebGpuRepeatingSpriteRenderer: not connected.');
|
|
25010
|
+
}
|
|
25011
|
+
const layout = this._device.createPipelineLayout({
|
|
25012
|
+
bindGroupLayouts: [this._uniformBindGroupLayout, this._textureBindGroupLayout],
|
|
25013
|
+
});
|
|
25014
|
+
const isShader = kind === 'shader';
|
|
25015
|
+
const strideBytes = isShader ? shaderStrideBytes : geoStrideBytes;
|
|
25016
|
+
const buffers = isShader
|
|
25017
|
+
? [{
|
|
25018
|
+
arrayStride: strideBytes,
|
|
25019
|
+
stepMode: 'instance',
|
|
25020
|
+
attributes: [
|
|
25021
|
+
{ shaderLocation: 0, offset: 0, format: 'float32x4' },
|
|
25022
|
+
{ shaderLocation: 1, offset: 16, format: 'float32x4' },
|
|
25023
|
+
{ shaderLocation: 2, offset: 32, format: 'unorm8x4' },
|
|
25024
|
+
{ shaderLocation: 3, offset: 36, format: 'uint32' },
|
|
25025
|
+
],
|
|
25026
|
+
}]
|
|
25027
|
+
: [{
|
|
25028
|
+
arrayStride: strideBytes,
|
|
25029
|
+
stepMode: 'instance',
|
|
25030
|
+
attributes: [
|
|
25031
|
+
{ shaderLocation: 0, offset: 0, format: 'float32x4' },
|
|
25032
|
+
{ shaderLocation: 1, offset: 16, format: 'unorm16x4' },
|
|
25033
|
+
{ shaderLocation: 2, offset: 24, format: 'unorm8x4' },
|
|
25034
|
+
{ shaderLocation: 3, offset: 28, format: 'uint32' },
|
|
25035
|
+
],
|
|
25036
|
+
}];
|
|
25037
|
+
const desc = {
|
|
25038
|
+
layout,
|
|
25039
|
+
vertex: {
|
|
25040
|
+
module: this._shaderModule,
|
|
25041
|
+
entryPoint: isShader ? 'shaderVert' : 'geoVert',
|
|
25042
|
+
buffers,
|
|
25043
|
+
},
|
|
25044
|
+
fragment: {
|
|
25045
|
+
module: this._shaderModule,
|
|
25046
|
+
entryPoint: isShader ? 'shaderFrag' : 'geoFrag',
|
|
25047
|
+
targets: [{ format, blend: getWebGpuBlendState(blend), writeMask: GPUColorWrite.ALL }],
|
|
25048
|
+
},
|
|
25049
|
+
primitive: { topology: 'triangle-list' },
|
|
25050
|
+
};
|
|
25051
|
+
if (stencil) {
|
|
25052
|
+
desc.depthStencil = stencilContentDepthStencilState();
|
|
25053
|
+
}
|
|
25054
|
+
const pipeline = this._device.createRenderPipeline(desc);
|
|
25055
|
+
this._pipelines.set(key, pipeline);
|
|
25056
|
+
return pipeline;
|
|
25057
|
+
}
|
|
25058
|
+
_ensureShaderCapacity(needed) {
|
|
25059
|
+
if (!this._device || needed <= this._shaderInstCapacity)
|
|
25060
|
+
return;
|
|
25061
|
+
this._shaderInstCapacity = this._growCapacity(this._shaderInstCapacity, needed);
|
|
25062
|
+
const oldData = this._shaderInstData;
|
|
25063
|
+
const carry = this._shaderQuadCount * shaderStrideBytes;
|
|
25064
|
+
this._shaderInstData = new ArrayBuffer(this._shaderInstCapacity * shaderStrideBytes);
|
|
25065
|
+
if (carry > 0)
|
|
25066
|
+
new Uint8Array(this._shaderInstData).set(new Uint8Array(oldData, 0, carry));
|
|
25067
|
+
this._shaderInstF32 = new Float32Array(this._shaderInstData);
|
|
25068
|
+
this._shaderInstU32 = new Uint32Array(this._shaderInstData);
|
|
25069
|
+
this._shaderInstBuf?.destroy();
|
|
25070
|
+
this._shaderInstBuf = this._device.createBuffer({
|
|
25071
|
+
size: this._shaderInstData.byteLength,
|
|
25072
|
+
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
|
|
25073
|
+
});
|
|
25074
|
+
}
|
|
25075
|
+
_ensureGeoCapacity(needed) {
|
|
25076
|
+
if (!this._device || needed <= this._geoInstCapacity)
|
|
25077
|
+
return;
|
|
25078
|
+
this._geoInstCapacity = this._growCapacity(this._geoInstCapacity, needed);
|
|
25079
|
+
const oldData = this._geoInstData;
|
|
25080
|
+
const carry = this._geoQuadCount * geoStrideBytes;
|
|
25081
|
+
this._geoInstData = new ArrayBuffer(this._geoInstCapacity * geoStrideBytes);
|
|
25082
|
+
if (carry > 0)
|
|
25083
|
+
new Uint8Array(this._geoInstData).set(new Uint8Array(oldData, 0, carry));
|
|
25084
|
+
this._geoInstF32 = new Float32Array(this._geoInstData);
|
|
25085
|
+
this._geoInstU32 = new Uint32Array(this._geoInstData);
|
|
25086
|
+
this._geoInstBuf?.destroy();
|
|
25087
|
+
this._geoInstBuf = this._device.createBuffer({
|
|
25088
|
+
size: this._geoInstData.byteLength,
|
|
25089
|
+
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
|
|
25090
|
+
});
|
|
25091
|
+
}
|
|
25092
|
+
_growCapacity(current, needed) {
|
|
25093
|
+
let cap = Math.max(current, initialBatchCapacity$1);
|
|
25094
|
+
while (cap < needed)
|
|
25095
|
+
cap *= 2;
|
|
25096
|
+
return cap;
|
|
25097
|
+
}
|
|
25098
|
+
}
|
|
25099
|
+
|
|
21817
25100
|
/// <reference types="@webgpu/types" />
|
|
21818
25101
|
const spriteShaderSource = `
|
|
21819
25102
|
struct ProjectionUniforms {
|
|
@@ -22006,6 +25289,10 @@ class WebGpuSpriteRenderer extends AbstractWebGpuRenderer {
|
|
|
22006
25289
|
_customBaseTextureLayout = null;
|
|
22007
25290
|
_currentMaterial = null;
|
|
22008
25291
|
_currentBaseTexture = null;
|
|
25292
|
+
// Reusable scratch for device-snapped local bounds ('geometry' mode), and the
|
|
25293
|
+
// bounds resolved for the sprite currently being packed (snapped or logical).
|
|
25294
|
+
_snapBounds = new Rectangle();
|
|
25295
|
+
_activeBounds = null;
|
|
22009
25296
|
onConnect(backend) {
|
|
22010
25297
|
if (this._device) {
|
|
22011
25298
|
return;
|
|
@@ -22126,6 +25413,7 @@ class WebGpuSpriteRenderer extends AbstractWebGpuRenderer {
|
|
|
22126
25413
|
// sprite's transform into the buffer and use the freshly-allocated slot.
|
|
22127
25414
|
const command = backend.activeDrawCommand;
|
|
22128
25415
|
const nodeIndex = command !== null ? command.nodeIndex : backend._pushTransform(sprite);
|
|
25416
|
+
this._activeBounds = this._resolveBounds(sprite, backend);
|
|
22129
25417
|
if (material === null) {
|
|
22130
25418
|
this._renderDefault(sprite, texture, backend, nodeIndex);
|
|
22131
25419
|
}
|
|
@@ -22133,6 +25421,19 @@ class WebGpuSpriteRenderer extends AbstractWebGpuRenderer {
|
|
|
22133
25421
|
this._renderCustom(sprite, texture, material, backend, nodeIndex);
|
|
22134
25422
|
}
|
|
22135
25423
|
}
|
|
25424
|
+
/**
|
|
25425
|
+
* Local bounds to upload for `sprite` this draw: device-pixel-snapped in
|
|
25426
|
+
* `'geometry'` pixel-snap mode (axis-aligned only), otherwise the sprite's
|
|
25427
|
+
* logical local bounds. Reuses a scratch rectangle and never mutates logical
|
|
25428
|
+
* state. Consumed synchronously by {@link _packInstance}.
|
|
25429
|
+
*/
|
|
25430
|
+
_resolveBounds(sprite, backend) {
|
|
25431
|
+
if (sprite.pixelSnapMode !== 'geometry') {
|
|
25432
|
+
return sprite.getLocalBounds();
|
|
25433
|
+
}
|
|
25434
|
+
const snap = backend._getSnapPixelSize();
|
|
25435
|
+
return sprite.getRenderBounds(backend.view, snap.width, snap.height, this._snapBounds);
|
|
25436
|
+
}
|
|
22136
25437
|
/** Default multi-texture path: rotate the base texture through 8 slots. */
|
|
22137
25438
|
_renderDefault(sprite, texture, backend, nodeIndex) {
|
|
22138
25439
|
const blendMode = sprite.blendMode;
|
|
@@ -22311,8 +25612,9 @@ class WebGpuSpriteRenderer extends AbstractWebGpuRenderer {
|
|
|
22311
25612
|
const offset = this._instanceCount * wordsPerInstance;
|
|
22312
25613
|
const f32 = this._instanceFloat32;
|
|
22313
25614
|
const u32 = this._instanceUint32;
|
|
22314
|
-
// localBounds: left, top, right, bottom (words 0..3, offset 0)
|
|
22315
|
-
|
|
25615
|
+
// localBounds: left, top, right, bottom (words 0..3, offset 0) — device-snapped in
|
|
25616
|
+
// 'geometry' pixel-snap mode, otherwise the logical local bounds.
|
|
25617
|
+
const bounds = this._activeBounds ?? sprite.getLocalBounds();
|
|
22316
25618
|
f32[offset + 0] = bounds.left;
|
|
22317
25619
|
f32[offset + 1] = bounds.top;
|
|
22318
25620
|
f32[offset + 2] = bounds.right;
|
|
@@ -23507,6 +26809,14 @@ function buildCoreRendererBindings(options) {
|
|
|
23507
26809
|
[RenderBackendType.WebGl2]: () => new WebGl2TextRenderer(),
|
|
23508
26810
|
[RenderBackendType.WebGpu]: () => new WebGpuTextRenderer(),
|
|
23509
26811
|
};
|
|
26812
|
+
const nineSliceRenderers = {
|
|
26813
|
+
[RenderBackendType.WebGl2]: () => new WebGl2NineSliceSpriteRenderer(spriteRendererBatchSize),
|
|
26814
|
+
[RenderBackendType.WebGpu]: () => new WebGpuNineSliceSpriteRenderer(),
|
|
26815
|
+
};
|
|
26816
|
+
const repeatingSpriteRenderers = {
|
|
26817
|
+
[RenderBackendType.WebGl2]: () => new WebGl2RepeatingSpriteRenderer(spriteRendererBatchSize),
|
|
26818
|
+
[RenderBackendType.WebGpu]: () => new WebGpuRepeatingSpriteRenderer(),
|
|
26819
|
+
};
|
|
23510
26820
|
return [
|
|
23511
26821
|
{
|
|
23512
26822
|
targets: [Sprite],
|
|
@@ -23521,6 +26831,14 @@ function buildCoreRendererBindings(options) {
|
|
|
23521
26831
|
targets: [Text, BitmapText],
|
|
23522
26832
|
create: backend => textRenderers[backend.backendType]?.(),
|
|
23523
26833
|
},
|
|
26834
|
+
{
|
|
26835
|
+
targets: [NineSliceSprite],
|
|
26836
|
+
create: backend => nineSliceRenderers[backend.backendType]?.(),
|
|
26837
|
+
},
|
|
26838
|
+
{
|
|
26839
|
+
targets: [RepeatingSprite],
|
|
26840
|
+
create: backend => repeatingSpriteRenderers[backend.backendType]?.(),
|
|
26841
|
+
},
|
|
23524
26842
|
];
|
|
23525
26843
|
}
|
|
23526
26844
|
|
|
@@ -24551,6 +27869,7 @@ class WebGl2Backend {
|
|
|
24551
27869
|
_canvas;
|
|
24552
27870
|
_contextLost;
|
|
24553
27871
|
_renderTarget;
|
|
27872
|
+
_snapTransform = new Matrix();
|
|
24554
27873
|
_renderer = null;
|
|
24555
27874
|
_shader = null;
|
|
24556
27875
|
_blendMode = null;
|
|
@@ -24680,7 +27999,34 @@ class WebGl2Backend {
|
|
|
24680
27999
|
*/
|
|
24681
28000
|
_writeTransformCommand(command) {
|
|
24682
28001
|
const drawable = command.drawable;
|
|
24683
|
-
this._transformBuffer.write(command.nodeIndex,
|
|
28002
|
+
this._transformBuffer.write(command.nodeIndex, this._resolveSnapTransform(drawable), drawable.tint);
|
|
28003
|
+
}
|
|
28004
|
+
/**
|
|
28005
|
+
* Resolve the world transform to upload for `drawable`, applying render-only
|
|
28006
|
+
* pixel snapping against the active render target's device-pixel grid when the
|
|
28007
|
+
* drawable opts in. Returns the live (unsnapped) global transform for the
|
|
28008
|
+
* `'none'` default; never mutates logical state.
|
|
28009
|
+
* @internal
|
|
28010
|
+
*/
|
|
28011
|
+
_resolveSnapTransform(drawable) {
|
|
28012
|
+
const target = this._renderTarget;
|
|
28013
|
+
const width = target.root ? this._canvas.width : target.width;
|
|
28014
|
+
const height = target.root ? this._canvas.height : target.height;
|
|
28015
|
+
return resolveUploadTransform(drawable, target.view, width, height, this._snapTransform);
|
|
28016
|
+
}
|
|
28017
|
+
/**
|
|
28018
|
+
* Device-pixel dimensions of the active render target — `canvas.width/height`
|
|
28019
|
+
* (css × pixelRatio) for the root, or the target's own size for an offscreen
|
|
28020
|
+
* {@link RenderTexture}. Used by batched renderers to snap shared geometry
|
|
28021
|
+
* boundaries to the same device grid the transform seam snaps the origin to.
|
|
28022
|
+
* @internal
|
|
28023
|
+
*/
|
|
28024
|
+
_getSnapPixelSize() {
|
|
28025
|
+
const target = this._renderTarget;
|
|
28026
|
+
return {
|
|
28027
|
+
width: target.root ? this._canvas.width : target.width,
|
|
28028
|
+
height: target.root ? this._canvas.height : target.height,
|
|
28029
|
+
};
|
|
24684
28030
|
}
|
|
24685
28031
|
/**
|
|
24686
28032
|
* Append a drawable's world transform (+ tint) to the shared transform buffer
|
|
@@ -24693,7 +28039,7 @@ class WebGl2Backend {
|
|
|
24693
28039
|
* @internal
|
|
24694
28040
|
*/
|
|
24695
28041
|
_pushTransform(drawable) {
|
|
24696
|
-
return this._transformBuffer.push(
|
|
28042
|
+
return this._transformBuffer.push(this._resolveSnapTransform(drawable), drawable.tint);
|
|
24697
28043
|
}
|
|
24698
28044
|
/** @internal */
|
|
24699
28045
|
_endDrawPlan() {
|
|
@@ -26033,9 +29379,9 @@ class WebGpuTransformStorage {
|
|
|
26033
29379
|
begin(nodeCount) {
|
|
26034
29380
|
this._buffer.begin(nodeCount);
|
|
26035
29381
|
}
|
|
26036
|
-
writeCommand(command) {
|
|
29382
|
+
writeCommand(command, transform) {
|
|
26037
29383
|
const drawable = command.drawable;
|
|
26038
|
-
this._buffer.write(command.nodeIndex, drawable.getGlobalTransform(), drawable.tint);
|
|
29384
|
+
this._buffer.write(command.nodeIndex, transform ?? drawable.getGlobalTransform(), drawable.tint);
|
|
26039
29385
|
}
|
|
26040
29386
|
/**
|
|
26041
29387
|
* Record that a draw command's transform write was skipped because its
|
|
@@ -26051,8 +29397,8 @@ class WebGpuTransformStorage {
|
|
|
26051
29397
|
* `nodeIndex` — a direct `backend.draw(drawable)` outside the plan player —
|
|
26052
29398
|
* so a batch of synthetic draws does not collide on a single row.
|
|
26053
29399
|
*/
|
|
26054
|
-
push(drawable) {
|
|
26055
|
-
return this._buffer.push(drawable.getGlobalTransform(), drawable.tint);
|
|
29400
|
+
push(drawable, transform) {
|
|
29401
|
+
return this._buffer.push(transform ?? drawable.getGlobalTransform(), drawable.tint);
|
|
26056
29402
|
}
|
|
26057
29403
|
/**
|
|
26058
29404
|
* Pre-allocate (or grow) the GPU storage buffer to hold at least
|
|
@@ -26170,6 +29516,7 @@ class WebGpuBackend {
|
|
|
26170
29516
|
_format = null;
|
|
26171
29517
|
_initializePromise = null;
|
|
26172
29518
|
_renderTarget;
|
|
29519
|
+
_snapTransform = new Matrix();
|
|
26173
29520
|
_renderer = null;
|
|
26174
29521
|
_texture = null;
|
|
26175
29522
|
_clearRequested = false;
|
|
@@ -26291,7 +29638,7 @@ class WebGpuBackend {
|
|
|
26291
29638
|
for (let i = 0; i < instructions.length; i++) {
|
|
26292
29639
|
const command = instructions[i];
|
|
26293
29640
|
if (drawCommandUsesSharedTransform(command, this)) {
|
|
26294
|
-
storage.writeCommand(command);
|
|
29641
|
+
storage.writeCommand(command, this._resolveSnapTransform(command.drawable));
|
|
26295
29642
|
}
|
|
26296
29643
|
else {
|
|
26297
29644
|
storage.recordSkippedWrite();
|
|
@@ -26626,7 +29973,31 @@ class WebGpuBackend {
|
|
|
26626
29973
|
* @internal
|
|
26627
29974
|
*/
|
|
26628
29975
|
_pushTransform(drawable) {
|
|
26629
|
-
return this._getTransformStorage().push(drawable);
|
|
29976
|
+
return this._getTransformStorage().push(drawable, this._resolveSnapTransform(drawable));
|
|
29977
|
+
}
|
|
29978
|
+
/**
|
|
29979
|
+
* Resolve the world transform to upload for `drawable`, applying render-only
|
|
29980
|
+
* pixel snapping against the active render target's device-pixel grid when the
|
|
29981
|
+
* drawable opts in. Returns the live (unsnapped) global transform for the
|
|
29982
|
+
* `'none'` default; never mutates logical state.
|
|
29983
|
+
* @internal
|
|
29984
|
+
*/
|
|
29985
|
+
_resolveSnapTransform(drawable) {
|
|
29986
|
+
const target = this._renderTarget;
|
|
29987
|
+
const root = target === this._rootRenderTarget;
|
|
29988
|
+
const width = root ? this._canvas.width : target.width;
|
|
29989
|
+
const height = root ? this._canvas.height : target.height;
|
|
29990
|
+
return resolveUploadTransform(drawable, target.view, width, height, this._snapTransform);
|
|
29991
|
+
}
|
|
29992
|
+
/**
|
|
29993
|
+
* Device-pixel dimensions of the active render target — `canvas.width/height`
|
|
29994
|
+
* (css × pixelRatio) for the root, or the target's own size for an offscreen
|
|
29995
|
+
* {@link RenderTexture}. Used by batched renderers to snap shared geometry
|
|
29996
|
+
* boundaries to the same device grid the transform seam snaps the origin to.
|
|
29997
|
+
* @internal
|
|
29998
|
+
*/
|
|
29999
|
+
_getSnapPixelSize() {
|
|
30000
|
+
return this._getAttachmentPixelSize(this._renderTarget);
|
|
26630
30001
|
}
|
|
26631
30002
|
_setActiveRenderer(renderer) {
|
|
26632
30003
|
if (this._renderer !== renderer) {
|
|
@@ -29736,6 +33107,10 @@ class Loader {
|
|
|
29736
33107
|
* Atomically bind all keys for one AssetBinding to a pre-created handler.
|
|
29737
33108
|
* Validates all keys BEFORE mutating any map. Any already-registered key
|
|
29738
33109
|
* throws before any mutation (no override in 0.12).
|
|
33110
|
+
*
|
|
33111
|
+
* `Result` and `Options` are inferred from the binding's `AssetBinding<Result, Options>`
|
|
33112
|
+
* contract. A declarative handler's optional `getIdentityKey` is forwarded into
|
|
33113
|
+
* the internal {@link HandlerEntry} so it participates in in-flight deduplication.
|
|
29739
33114
|
* @internal
|
|
29740
33115
|
*/
|
|
29741
33116
|
bindAsset(keys, handler) {
|
|
@@ -29768,17 +33143,30 @@ class Loader {
|
|
|
29768
33143
|
}
|
|
29769
33144
|
}
|
|
29770
33145
|
// All validation passed — install atomically.
|
|
29771
|
-
//
|
|
29772
|
-
//
|
|
29773
|
-
//
|
|
29774
|
-
//
|
|
29775
|
-
//
|
|
33146
|
+
//
|
|
33147
|
+
// Localized type-erasure boundary: the internal Loader uses a flat config
|
|
33148
|
+
// `{ source, ...fields }`. The public AssetHandler<Result, Options> interface
|
|
33149
|
+
// uses `AssetLoadRequest<Options> = { source, options? }`. This single `toRequest`
|
|
33150
|
+
// helper is the only place where the erased flat config is cast to the typed
|
|
33151
|
+
// request — justified by the `AssetBinding<Result, Options>` contract that
|
|
33152
|
+
// associates this handler's Options with the registered constructor.
|
|
33153
|
+
//
|
|
33154
|
+
// `options` is intentionally omitted (not set to `undefined`) when the flat
|
|
33155
|
+
// config carries no extra fields, keeping the object compatible with a future
|
|
33156
|
+
// `exactOptionalPropertyTypes` migration.
|
|
33157
|
+
const toRequest = (config) => {
|
|
33158
|
+
const { source, ...rest } = config;
|
|
33159
|
+
if (Object.keys(rest).length === 0) {
|
|
33160
|
+
return { source };
|
|
33161
|
+
}
|
|
33162
|
+
return { source, options: rest };
|
|
33163
|
+
};
|
|
33164
|
+
const boundIdentityKey = handler.getIdentityKey?.bind(handler);
|
|
29776
33165
|
this._handlerFunctions.set(keys.type, {
|
|
29777
|
-
load: (config, ctx) =>
|
|
29778
|
-
|
|
29779
|
-
|
|
29780
|
-
|
|
29781
|
-
},
|
|
33166
|
+
load: (config, ctx) => handler.load(toRequest(config), ctx),
|
|
33167
|
+
getIdentityKey: boundIdentityKey
|
|
33168
|
+
? (config) => boundIdentityKey(toRequest(config))
|
|
33169
|
+
: undefined,
|
|
29782
33170
|
});
|
|
29783
33171
|
for (const name of resolvedNames) {
|
|
29784
33172
|
this._assetTypeMap.set(name, keys.type);
|
|
@@ -29786,7 +33174,9 @@ class Loader {
|
|
|
29786
33174
|
for (const ext of normalizedExts) {
|
|
29787
33175
|
this._extensionMap.set(ext, keys.type);
|
|
29788
33176
|
}
|
|
29789
|
-
// Own this handler for lifecycle management
|
|
33177
|
+
// Own this handler for lifecycle management.
|
|
33178
|
+
// Cast to the erased AssetHandler for storage; destroy() is the only method
|
|
33179
|
+
// called on entries in this array.
|
|
29790
33180
|
this._boundHandlers.push(handler);
|
|
29791
33181
|
}
|
|
29792
33182
|
/**
|
|
@@ -31642,8 +35032,8 @@ class Application {
|
|
|
31642
35032
|
}
|
|
31643
35033
|
|
|
31644
35034
|
const buildInfo = Object.freeze({
|
|
31645
|
-
version: "0.
|
|
31646
|
-
revision: "
|
|
35035
|
+
version: "0.13.0",
|
|
35036
|
+
revision: "735089b",
|
|
31647
35037
|
development: false,
|
|
31648
35038
|
});
|
|
31649
35039
|
|
|
@@ -37552,5 +40942,5 @@ class TextFactory extends AbstractAssetFactory {
|
|
|
37552
40942
|
}
|
|
37553
40943
|
}
|
|
37554
40944
|
|
|
37555
|
-
export { AbstractAssetFactory, AbstractMedia, AbstractText, AbstractWebGl2BatchedRenderer, AbstractWebGl2Renderer, AbstractWebGpuRenderer, AnimatedSprite, Application, ApplicationStatus, ArcadeStickGamepadMapping, Asset, AssetImpl, Assets, AssetsImpl, AtlasPage, AudioAnalyser, AudioBus, AudioFilter, AudioListener, AudioManager, BeatDetector, BinaryAsset, BinaryFactory, BitmapText, BlendModes, BlurFilter, BmFont, BmFontAdapter, BmFontLoaderFactory, Bounds, BufferTypes, BufferUsage, BundleLoadError, CacheFirstStrategy, CallbackRenderPass, Camera, Capabilities, ChannelOffset, ChannelSize, ChorusFilter, Circle, Clock, CollisionType, Color, ColorFilter, CompressorFilter, Container, CsvAsset, CsvFactory, DataTexture, DelayFilter, Drawable, DuckingFilter, Ease, Ellipse, Envelope, EqualizerFilter, FactoryRegistry, Filter, Flags, FontAsset, FontFactory, GameCubeGamepadMapping, Gamepad, GamepadAxis, GamepadButton, GamepadMapping, GamepadMappingFamily, GamepadPromptLayouts, GenericDualAnalogGamepadMapping, Geometry, GlyphAtlas, GlyphAtlasPool, Gradient, GranularFilter, Graphics, HTMLText, HighpassFilter, ImageAsset, ImageFactory, IndexedDbDatabase, IndexedDbStore, InputBinding, InputManager, InteractionEvent, InteractionManager, Interval, JoyConLeftGamepadMapping, JoyConRightGamepadMapping, Json, JsonFactory, JsonStore, Keyboard, Line, LinearGradient, Loader, LoadingQueue, LowpassFilter, LutFilter, Material, Matrix, Mesh, MeshMaterial, Music, MusicFactory, NetworkOnlyStrategy, ObservableSize, ObservableVector, OscillatorSound, PitchShiftFilter, PlayStationGamepadMapping, Pointer, PointerState, PointerStateFlag, PolarVector, Polygon, Quadtree, RadialGradient, Random, Rectangle, RenderBackendType, RenderNode, RenderNodePass, RenderPass, RenderPipeline, RenderTarget, RenderTexture, RendererRegistry, RenderingContext, RenderingPrimitives, ReverbFilter, SDF_RADIUS, Sampler, ScaleModes, Scene, SceneManager, SceneNode, Segment, Shader, ShaderAttribute, ShaderPrimitives, ShaderSource, ShaderUniform, Signal, Size, Sound, SoundFactory, SoundPoolStrategy, Sprite, SpriteFlags, SpriteMaterial, Spritesheet, SteamControllerGamepadMapping, SteamDeckGamepadMapping, SubtitleAsset, SubtitleFactory, SvgAsset, SvgFactory, SwitchProGamepadMapping, Text, TextAsset, TextFactory, TextStyle, Texture, TextureFactory, Time, Timer, Tween, TweenManager, TweenState, Vector, Video, VideoFactory, View, ViewFlags, VocoderFilter, VoronoiRegion, WasmAsset, WasmFactory, WebGl2Backend, WebGl2MeshRenderer, WebGl2RenderBuffer, WebGl2ShaderBlock, WebGl2ShaderFilter, WebGl2SpriteRenderer, WebGl2TextRenderer, WebGl2VertexArrayObject, WebGpuBackend, WebGpuComputePipeline, WebGpuMeshRenderer, WebGpuShaderFilter, WebGpuSpriteRenderer, WebGpuStorageBuffer, WebGpuTextRenderer, WorkletFilter, WrapModes, XboxGamepadMapping, XmlAsset, XmlFactory, bezierCurveTo, buildCircle, buildEllipse, buildInfo, buildLine, buildPath, buildPolygon, buildRectangle, buildStar, buildTextPageQuads, builtInGamepadDefinitions, canvasSourceToDataUrl, clamp, createRenderStats, createWebGl2ShaderProgram, crossFade, decodeAudioData, defineAssetManifest, degreesPerRadian, degreesToRadians, determineMimeType, disposeAudioManager, emptyArrayBuffer, getAudioContext, getAudioManager, getCanvasSourceSize, getCollisionCircleCircle, getCollisionCircleRectangle, getCollisionEllipseCircle, getCollisionEllipseEllipse, getCollisionEllipseRectangle, getCollisionPolygonCircle, getCollisionRectangleRectangle, getCollisionSat, getDefaultGlyphAtlasPool, getDistance, getOfflineAudioContext, getPreciseTime, getTextureSourceSize, getVoronoiRegion$1 as getVoronoiRegion, getWebGpuBlendState, hours, inRange, intersectionCircleCircle, intersectionCircleEllipse, intersectionCirclePoly, intersectionEllipseEllipse, intersectionEllipsePoly, intersectionLineCircle, intersectionLineEllipse, intersectionLineLine, intersectionLinePoly, intersectionLineRect, intersectionPointCircle, intersectionPointEllipse, intersectionPointLine, intersectionPointPoint, intersectionPointPoly, intersectionPointRect, intersectionPolyPoly, intersectionRectCircle, intersectionRectEllipse, intersectionRectPoly, intersectionRectRect, intersectionSat, isAudioContextReady, isPowerOfTwo, layoutText, lerp, matchesIds, maxPointers, measureText, milliseconds, minutes, noop$1 as noop, onAudioContextReady, parseBmFontText, parseGamepadDescriptor, perfClearMarks, perfClearMeasures, perfMark, perfMeasure, pointerSlotSize, quadraticCurveTo, radiansPerDegree, radiansToDegrees, rand, registerAudioWorkletProcessor, removeArrayItems, resetDefaultGlyphAtlasPool, resetRenderStats, resolveDefinition, resolveGamepadDefinition, seconds, sign, stopEvent, substepSweep, supportsCodec, supportsEventOptions, supportsIndexedDb, supportsPointerEvents, supportsTouchEvents, supportsWebAudio, sweepCircleAgainst, sweepCircleVsCircle, sweepCircleVsRectangle, sweepRectangle, sweepRectangleAgainst, tau, trimRotation, upgradeFragmentShaderToGl300, vibrate, webGl2PrimitiveArrayConstructors, webGl2PrimitiveByteSizeMapping, webGl2PrimitiveTypeNames };
|
|
40945
|
+
export { AbstractAssetFactory, AbstractMedia, AbstractText, AbstractWebGl2BatchedRenderer, AbstractWebGl2Renderer, AbstractWebGpuRenderer, AnimatedSprite, Application, ApplicationStatus, ArcadeStickGamepadMapping, Asset, AssetImpl, Assets, AssetsImpl, AtlasPage, AudioAnalyser, AudioBus, AudioFilter, AudioListener, AudioManager, BeatDetector, BinaryAsset, BinaryFactory, BitmapText, BlendModes, BlurFilter, BmFont, BmFontAdapter, BmFontLoaderFactory, Bounds, BufferTypes, BufferUsage, BundleLoadError, CacheFirstStrategy, CallbackRenderPass, Camera, Capabilities, ChannelOffset, ChannelSize, ChorusFilter, Circle, Clock, CollisionType, Color, ColorFilter, CompressorFilter, Container, CsvAsset, CsvFactory, DataTexture, DelayFilter, Drawable, DuckingFilter, Ease, Ellipse, Envelope, EqualizerFilter, FactoryRegistry, Filter, Flags, FontAsset, FontFactory, GameCubeGamepadMapping, Gamepad, GamepadAxis, GamepadButton, GamepadMapping, GamepadMappingFamily, GamepadPromptLayouts, GenericDualAnalogGamepadMapping, Geometry, GlyphAtlas, GlyphAtlasPool, Gradient, GranularFilter, Graphics, HTMLText, HighpassFilter, ImageAsset, ImageFactory, IndexedDbDatabase, IndexedDbStore, InputBinding, InputManager, InteractionEvent, InteractionManager, Interval, JoyConLeftGamepadMapping, JoyConRightGamepadMapping, Json, JsonFactory, JsonStore, Keyboard, Line, LinearGradient, Loader, LoadingQueue, LowpassFilter, LutFilter, Material, Matrix, Mesh, MeshMaterial, Music, MusicFactory, NetworkOnlyStrategy, NineSliceSprite, ObservableSize, ObservableVector, OscillatorSound, PitchShiftFilter, PlayStationGamepadMapping, Pointer, PointerState, PointerStateFlag, PolarVector, Polygon, Quadtree, RadialGradient, Random, Rectangle, RenderBackendType, RenderNode, RenderNodePass, RenderPass, RenderPipeline, RenderTarget, RenderTexture, RendererRegistry, RenderingContext, RenderingPrimitives, RepeatingSprite, ReverbFilter, SDF_RADIUS, Sampler, ScaleModes, Scene, SceneManager, SceneNode, Segment, Shader, ShaderAttribute, ShaderPrimitives, ShaderSource, ShaderUniform, Signal, Size, Sound, SoundFactory, SoundPoolStrategy, Sprite, SpriteFlags, SpriteMaterial, Spritesheet, SteamControllerGamepadMapping, SteamDeckGamepadMapping, SubtitleAsset, SubtitleFactory, SvgAsset, SvgFactory, SwitchProGamepadMapping, Text, TextAsset, TextFactory, TextStyle, Texture, TextureFactory, TextureRegion, Time, Timer, Tween, TweenManager, TweenState, Vector, Video, VideoFactory, View, ViewFlags, VocoderFilter, VoronoiRegion, WasmAsset, WasmFactory, WebGl2Backend, WebGl2MeshRenderer, WebGl2NineSliceSpriteRenderer, WebGl2RenderBuffer, WebGl2RepeatingSpriteRenderer, WebGl2ShaderBlock, WebGl2ShaderFilter, WebGl2SpriteRenderer, WebGl2TextRenderer, WebGl2VertexArrayObject, WebGpuBackend, WebGpuComputePipeline, WebGpuMeshRenderer, WebGpuNineSliceSpriteRenderer, WebGpuRepeatingSpriteRenderer, WebGpuShaderFilter, WebGpuSpriteRenderer, WebGpuStorageBuffer, WebGpuTextRenderer, WorkletFilter, WrapModes, XboxGamepadMapping, XmlAsset, XmlFactory, bezierCurveTo, buildCircle, buildEllipse, buildInfo, buildLine, buildPath, buildPolygon, buildRectangle, buildStar, buildTextPageQuads, builtInGamepadDefinitions, canvasSourceToDataUrl, clamp, createRenderStats, createWebGl2ShaderProgram, crossFade, decodeAudioData, defineAssetManifest, degreesPerRadian, degreesToRadians, determineMimeType, disposeAudioManager, emptyArrayBuffer, getAudioContext, getAudioManager, getCanvasSourceSize, getCollisionCircleCircle, getCollisionCircleRectangle, getCollisionEllipseCircle, getCollisionEllipseEllipse, getCollisionEllipseRectangle, getCollisionPolygonCircle, getCollisionRectangleRectangle, getCollisionSat, getDefaultGlyphAtlasPool, getDistance, getOfflineAudioContext, getPreciseTime, getTextureSourceSize, getVoronoiRegion$1 as getVoronoiRegion, getWebGpuBlendState, hours, inRange, intersectionCircleCircle, intersectionCircleEllipse, intersectionCirclePoly, intersectionEllipseEllipse, intersectionEllipsePoly, intersectionLineCircle, intersectionLineEllipse, intersectionLineLine, intersectionLinePoly, intersectionLineRect, intersectionPointCircle, intersectionPointEllipse, intersectionPointLine, intersectionPointPoint, intersectionPointPoly, intersectionPointRect, intersectionPolyPoly, intersectionRectCircle, intersectionRectEllipse, intersectionRectPoly, intersectionRectRect, intersectionSat, isAudioContextReady, isPowerOfTwo, layoutText, lerp, matchesIds, maxPointers, measureText, milliseconds, minutes, noop$1 as noop, onAudioContextReady, parseBmFontText, parseGamepadDescriptor, perfClearMarks, perfClearMeasures, perfMark, perfMeasure, planRepeat, pointerSlotSize, quadraticCurveTo, radiansPerDegree, radiansToDegrees, rand, registerAudioWorkletProcessor, removeArrayItems, resetDefaultGlyphAtlasPool, resetRenderStats, resolveDefinition, resolveGamepadDefinition, seconds, sign, stopEvent, substepSweep, supportsCodec, supportsEventOptions, supportsIndexedDb, supportsPointerEvents, supportsTouchEvents, supportsWebAudio, sweepCircleAgainst, sweepCircleVsCircle, sweepCircleVsRectangle, sweepRectangle, sweepRectangleAgainst, tau, trimRotation, upgradeFragmentShaderToGl300, vibrate, webGl2PrimitiveArrayConstructors, webGl2PrimitiveByteSizeMapping, webGl2PrimitiveTypeNames };
|
|
37556
40946
|
//# sourceMappingURL=exo.esm.js.map
|