bloody-engine 1.0.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/LICENSE +21 -0
- package/README.md +92 -0
- package/dist/node/batch-renderer-JqZ4TYcL.js +308 -0
- package/dist/node/browser-resource-loader-D51BD3k_.js +146 -0
- package/dist/node/camera-A8EGrk7U.js +271 -0
- package/dist/node/index-node.js +2117 -0
- package/dist/node/node-resource-loader-MzkD-IGo.js +166 -0
- package/dist/node/resource-loader-factory-DQ-PAVcN.js +93 -0
- package/dist/node/resource-pipeline-Dac9qRso.js +211 -0
- package/dist/web/index.js +1940 -0
- package/dist/web/index.umd.js +56 -0
- package/package.json +58 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Bloody Engine Contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# Bloody Engine
|
|
2
|
+
|
|
3
|
+
A WebGL-based 2.5D graphics engine for isometric rendering, written in TypeScript. Designed for both browser and Node.js environments with full isomorphic support.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **2.5D Rendering** - Optimized for isometric and dimetric projections
|
|
8
|
+
- **Cross-Platform** - Works in browsers and Node.js (headless rendering)
|
|
9
|
+
- **Batch Rendering** - Efficient sprite batching with GPU-accelerated transformations
|
|
10
|
+
- **Resource Management** - Unified asset loading pipeline for textures and resources
|
|
11
|
+
- **TypeScript** - Fully typed for excellent developer experience
|
|
12
|
+
- **Depth Sorting** - Proper 2.5D occlusion handling
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install bloody-engine
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Quick Start
|
|
21
|
+
|
|
22
|
+
### Browser
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
import { BloodyEngine } from 'bloody-engine';
|
|
26
|
+
|
|
27
|
+
// Initialize engine
|
|
28
|
+
const engine = new BloodyEngine({
|
|
29
|
+
width: 800,
|
|
30
|
+
height: 600
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// Start rendering loop
|
|
34
|
+
engine.start();
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Node.js
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
import { BloodyEngine } from 'bloody-engine';
|
|
41
|
+
|
|
42
|
+
// Initialize engine for headless rendering
|
|
43
|
+
const engine = new BloodyEngine({
|
|
44
|
+
width: 800,
|
|
45
|
+
height: 600,
|
|
46
|
+
headless: true
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// Render and capture output
|
|
50
|
+
engine.renderFrame();
|
|
51
|
+
const pixels = engine.getPixels();
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Documentation
|
|
55
|
+
|
|
56
|
+
For detailed documentation and architecture, see [docs/README.MD](docs/README.MD).
|
|
57
|
+
|
|
58
|
+
## Examples
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
// Create a sprite batch renderer
|
|
62
|
+
import { SpriteBatchRenderer, Texture } from 'bloody-engine';
|
|
63
|
+
|
|
64
|
+
const batchRenderer = new SpriteBatchRenderer(gl, shader);
|
|
65
|
+
|
|
66
|
+
// Add sprites
|
|
67
|
+
batchRenderer.addQuad({
|
|
68
|
+
x: 100,
|
|
69
|
+
y: 100,
|
|
70
|
+
z: 0,
|
|
71
|
+
width: 64,
|
|
72
|
+
height: 64,
|
|
73
|
+
rotation: 0,
|
|
74
|
+
color: { r: 1, g: 1, b: 1, a: 1 },
|
|
75
|
+
uvRect: { uMin: 0, vMin: 0, uMax: 1, vMax: 1 }
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// Render
|
|
79
|
+
batchRenderer.render(camera);
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## License
|
|
83
|
+
|
|
84
|
+
MIT License - see [LICENSE](LICENSE) for details.
|
|
85
|
+
|
|
86
|
+
## Repository
|
|
87
|
+
|
|
88
|
+
[https://github.com/BLooDek/bloody-engine](https://github.com/BLooDek/bloody-engine)
|
|
89
|
+
|
|
90
|
+
## Issues
|
|
91
|
+
|
|
92
|
+
Report bugs and request features at: [https://github.com/BLooDek/bloody-engine/issues](https://github.com/BLooDek/bloody-engine/issues)
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
class SpriteBatchRenderer {
|
|
2
|
+
/**
|
|
3
|
+
* Create a new sprite batch renderer (V2)
|
|
4
|
+
* @param gl WebGL rendering context
|
|
5
|
+
* @param shader Shader program to use (should be SHADERS_V2)
|
|
6
|
+
* @param maxQuads Maximum number of quads to batch (default 1000)
|
|
7
|
+
*/
|
|
8
|
+
constructor(gl, shader, maxQuads = 1e3) {
|
|
9
|
+
this.vertexBuffer = null;
|
|
10
|
+
this.quads = [];
|
|
11
|
+
this.isDirty = false;
|
|
12
|
+
this.verticesPerQuad = 6;
|
|
13
|
+
this.floatsPerVertex = 10;
|
|
14
|
+
this.texture = null;
|
|
15
|
+
this.depthTestEnabled = true;
|
|
16
|
+
this.gl = gl;
|
|
17
|
+
this.shader = shader;
|
|
18
|
+
this.maxQuads = maxQuads;
|
|
19
|
+
const totalFloats = maxQuads * this.verticesPerQuad * this.floatsPerVertex;
|
|
20
|
+
this.vertexData = new Float32Array(totalFloats);
|
|
21
|
+
const buf = gl.createBuffer();
|
|
22
|
+
if (!buf) {
|
|
23
|
+
throw new Error("Failed to create vertex buffer");
|
|
24
|
+
}
|
|
25
|
+
this.vertexBuffer = buf;
|
|
26
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer);
|
|
27
|
+
gl.bufferData(gl.ARRAY_BUFFER, this.vertexData.byteLength, gl.DYNAMIC_DRAW);
|
|
28
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, null);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Set the texture for batch rendering
|
|
32
|
+
* @param texture The texture to use when rendering
|
|
33
|
+
*/
|
|
34
|
+
setTexture(texture) {
|
|
35
|
+
this.texture = texture;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Add a sprite quad to the batch
|
|
39
|
+
* @param quad Sprite quad instance to add
|
|
40
|
+
*/
|
|
41
|
+
addQuad(quad) {
|
|
42
|
+
if (this.quads.length >= this.maxQuads) {
|
|
43
|
+
console.warn(`Sprite batch renderer at max capacity (${this.maxQuads})`);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
this.quads.push(quad);
|
|
47
|
+
this.isDirty = true;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Clear all quads from the batch
|
|
51
|
+
*/
|
|
52
|
+
clear() {
|
|
53
|
+
this.quads = [];
|
|
54
|
+
this.isDirty = true;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Get number of quads currently in batch
|
|
58
|
+
*/
|
|
59
|
+
getQuadCount() {
|
|
60
|
+
return this.quads.length;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Update the batch - rebuilds vertex buffer if quads changed
|
|
64
|
+
*/
|
|
65
|
+
update() {
|
|
66
|
+
if (!this.isDirty || this.quads.length === 0) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
let vertexIndex = 0;
|
|
70
|
+
for (const quad of this.quads) {
|
|
71
|
+
const {
|
|
72
|
+
x,
|
|
73
|
+
y,
|
|
74
|
+
z = 0,
|
|
75
|
+
width,
|
|
76
|
+
height,
|
|
77
|
+
rotation,
|
|
78
|
+
color = { r: 1, g: 1, b: 1, a: 1 },
|
|
79
|
+
uvRect = { uMin: 0, vMin: 0, uMax: 1, vMax: 1 },
|
|
80
|
+
texIndex = 0
|
|
81
|
+
} = quad;
|
|
82
|
+
const vertices = this.generateQuadVertices({
|
|
83
|
+
x,
|
|
84
|
+
y,
|
|
85
|
+
z,
|
|
86
|
+
width,
|
|
87
|
+
height,
|
|
88
|
+
rotation,
|
|
89
|
+
color,
|
|
90
|
+
uvRect,
|
|
91
|
+
texIndex
|
|
92
|
+
});
|
|
93
|
+
for (const vertex of vertices) {
|
|
94
|
+
this.vertexData[vertexIndex++] = vertex.x;
|
|
95
|
+
this.vertexData[vertexIndex++] = vertex.y;
|
|
96
|
+
this.vertexData[vertexIndex++] = vertex.z;
|
|
97
|
+
this.vertexData[vertexIndex++] = vertex.u;
|
|
98
|
+
this.vertexData[vertexIndex++] = vertex.v;
|
|
99
|
+
this.vertexData[vertexIndex++] = vertex.r;
|
|
100
|
+
this.vertexData[vertexIndex++] = vertex.g;
|
|
101
|
+
this.vertexData[vertexIndex++] = vertex.b;
|
|
102
|
+
this.vertexData[vertexIndex++] = vertex.a;
|
|
103
|
+
this.vertexData[vertexIndex++] = vertex.texIndex;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (this.vertexBuffer) {
|
|
107
|
+
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.vertexBuffer);
|
|
108
|
+
this.gl.bufferSubData(
|
|
109
|
+
this.gl.ARRAY_BUFFER,
|
|
110
|
+
0,
|
|
111
|
+
this.vertexData.subarray(0, vertexIndex)
|
|
112
|
+
);
|
|
113
|
+
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, null);
|
|
114
|
+
}
|
|
115
|
+
this.isDirty = false;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Set whether depth testing is enabled
|
|
119
|
+
* When enabled, sprites with lower Z values appear behind sprites with higher Z values
|
|
120
|
+
* @param enabled Whether to enable depth testing (default true)
|
|
121
|
+
*/
|
|
122
|
+
setDepthTestEnabled(enabled) {
|
|
123
|
+
this.depthTestEnabled = enabled;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Render the batch
|
|
127
|
+
* @param camera Optional camera for view transform (defaults to identity matrix)
|
|
128
|
+
*/
|
|
129
|
+
render(camera) {
|
|
130
|
+
if (this.quads.length === 0) {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
this.update();
|
|
134
|
+
this.shader.use();
|
|
135
|
+
if (this.depthTestEnabled) {
|
|
136
|
+
this.gl.enable(this.gl.DEPTH_TEST);
|
|
137
|
+
this.gl.depthFunc(this.gl.LEQUAL);
|
|
138
|
+
} else {
|
|
139
|
+
this.gl.disable(this.gl.DEPTH_TEST);
|
|
140
|
+
}
|
|
141
|
+
if (this.vertexBuffer) {
|
|
142
|
+
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.vertexBuffer);
|
|
143
|
+
const posAttr = this.shader.getAttributeLocation("aPosition");
|
|
144
|
+
const texCoordAttr = this.shader.getAttributeLocation("aTexCoord");
|
|
145
|
+
const colorAttr = this.shader.getAttributeLocation("aColor");
|
|
146
|
+
const texIndexAttr = this.shader.getAttributeLocation("aTexIndex");
|
|
147
|
+
const stride = this.floatsPerVertex * 4;
|
|
148
|
+
if (posAttr !== -1) {
|
|
149
|
+
this.gl.enableVertexAttribArray(posAttr);
|
|
150
|
+
this.gl.vertexAttribPointer(
|
|
151
|
+
posAttr,
|
|
152
|
+
3,
|
|
153
|
+
// 3 floats (x, y, z)
|
|
154
|
+
this.gl.FLOAT,
|
|
155
|
+
false,
|
|
156
|
+
stride,
|
|
157
|
+
0
|
|
158
|
+
// offset
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
if (texCoordAttr !== -1) {
|
|
162
|
+
this.gl.enableVertexAttribArray(texCoordAttr);
|
|
163
|
+
this.gl.vertexAttribPointer(
|
|
164
|
+
texCoordAttr,
|
|
165
|
+
2,
|
|
166
|
+
// 2 floats (u, v)
|
|
167
|
+
this.gl.FLOAT,
|
|
168
|
+
false,
|
|
169
|
+
stride,
|
|
170
|
+
3 * 4
|
|
171
|
+
// offset after position
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
if (colorAttr !== -1) {
|
|
175
|
+
this.gl.enableVertexAttribArray(colorAttr);
|
|
176
|
+
this.gl.vertexAttribPointer(
|
|
177
|
+
colorAttr,
|
|
178
|
+
4,
|
|
179
|
+
// 4 floats (r, g, b, a)
|
|
180
|
+
this.gl.FLOAT,
|
|
181
|
+
false,
|
|
182
|
+
stride,
|
|
183
|
+
5 * 4
|
|
184
|
+
// offset after texCoord
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
if (texIndexAttr !== -1) {
|
|
188
|
+
this.gl.enableVertexAttribArray(texIndexAttr);
|
|
189
|
+
this.gl.vertexAttribPointer(
|
|
190
|
+
texIndexAttr,
|
|
191
|
+
1,
|
|
192
|
+
// 1 float (texIndex)
|
|
193
|
+
this.gl.FLOAT,
|
|
194
|
+
false,
|
|
195
|
+
stride,
|
|
196
|
+
9 * 4
|
|
197
|
+
// offset after color
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
if (this.texture) {
|
|
201
|
+
this.texture.bind(0);
|
|
202
|
+
const textureUniform = this.shader.getUniformLocation("uTexture");
|
|
203
|
+
if (textureUniform !== null) {
|
|
204
|
+
this.gl.uniform1i(textureUniform, 0);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
const matrixUniform = this.shader.getUniformLocation("uMatrix");
|
|
208
|
+
if (matrixUniform !== null) {
|
|
209
|
+
const matrix = camera ? camera.getViewMatrix() : new Float32Array([
|
|
210
|
+
1,
|
|
211
|
+
0,
|
|
212
|
+
0,
|
|
213
|
+
0,
|
|
214
|
+
0,
|
|
215
|
+
1,
|
|
216
|
+
0,
|
|
217
|
+
0,
|
|
218
|
+
0,
|
|
219
|
+
0,
|
|
220
|
+
1,
|
|
221
|
+
0,
|
|
222
|
+
0,
|
|
223
|
+
0,
|
|
224
|
+
0,
|
|
225
|
+
1
|
|
226
|
+
]);
|
|
227
|
+
this.gl.uniformMatrix4fv(matrixUniform, false, matrix);
|
|
228
|
+
}
|
|
229
|
+
const vertexCount = this.quads.length * this.verticesPerQuad;
|
|
230
|
+
this.gl.drawArrays(this.gl.TRIANGLES, 0, vertexCount);
|
|
231
|
+
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, null);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Generate vertices for a quad with rotation applied
|
|
236
|
+
* Returns 6 vertices (2 triangles)
|
|
237
|
+
* @private
|
|
238
|
+
*/
|
|
239
|
+
generateQuadVertices(instance) {
|
|
240
|
+
const { x, y, z, width, height, rotation, color, uvRect, texIndex } = instance;
|
|
241
|
+
const halfW = width / 2;
|
|
242
|
+
const halfH = height / 2;
|
|
243
|
+
const cos = Math.cos(rotation);
|
|
244
|
+
const sin = Math.sin(rotation);
|
|
245
|
+
const rotatePoint = (px, py) => {
|
|
246
|
+
return [px * cos - py * sin, px * sin + py * cos];
|
|
247
|
+
};
|
|
248
|
+
const corners = [
|
|
249
|
+
[-halfW, -halfH],
|
|
250
|
+
// bottom-left
|
|
251
|
+
[halfW, -halfH],
|
|
252
|
+
// bottom-right
|
|
253
|
+
[halfW, halfH],
|
|
254
|
+
// top-right
|
|
255
|
+
[halfW, halfH],
|
|
256
|
+
// top-right (duplicate)
|
|
257
|
+
[-halfW, halfH],
|
|
258
|
+
// top-left
|
|
259
|
+
[-halfW, -halfH]
|
|
260
|
+
// bottom-left (duplicate)
|
|
261
|
+
];
|
|
262
|
+
const texCoords = [
|
|
263
|
+
[uvRect.uMin, uvRect.vMin],
|
|
264
|
+
// bottom-left
|
|
265
|
+
[uvRect.uMax, uvRect.vMin],
|
|
266
|
+
// bottom-right
|
|
267
|
+
[uvRect.uMax, uvRect.vMax],
|
|
268
|
+
// top-right
|
|
269
|
+
[uvRect.uMax, uvRect.vMax],
|
|
270
|
+
// top-right
|
|
271
|
+
[uvRect.uMin, uvRect.vMax],
|
|
272
|
+
// top-left
|
|
273
|
+
[uvRect.uMin, uvRect.vMin]
|
|
274
|
+
// bottom-left
|
|
275
|
+
];
|
|
276
|
+
const vertices = [];
|
|
277
|
+
for (let i = 0; i < corners.length; i++) {
|
|
278
|
+
const [localX, localY] = corners[i];
|
|
279
|
+
const [rotX, rotY] = rotatePoint(localX, localY);
|
|
280
|
+
const [u, v] = texCoords[i];
|
|
281
|
+
vertices.push({
|
|
282
|
+
x: x + rotX,
|
|
283
|
+
y: y + rotY,
|
|
284
|
+
z,
|
|
285
|
+
u,
|
|
286
|
+
v,
|
|
287
|
+
r: color.r,
|
|
288
|
+
g: color.g,
|
|
289
|
+
b: color.b,
|
|
290
|
+
a: color.a,
|
|
291
|
+
texIndex
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
return vertices;
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Dispose resources
|
|
298
|
+
*/
|
|
299
|
+
dispose() {
|
|
300
|
+
if (this.vertexBuffer) {
|
|
301
|
+
this.gl.deleteBuffer(this.vertexBuffer);
|
|
302
|
+
this.vertexBuffer = null;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
export {
|
|
307
|
+
SpriteBatchRenderer
|
|
308
|
+
};
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
class BrowserResourceLoader {
|
|
2
|
+
/**
|
|
3
|
+
* Create a new browser resource loader
|
|
4
|
+
* @param baseUrl Optional base URL for resolving relative paths (defaults to current origin)
|
|
5
|
+
* @param timeout Default timeout for requests in milliseconds (default: 10000)
|
|
6
|
+
*/
|
|
7
|
+
constructor(baseUrl = "", timeout = 1e4) {
|
|
8
|
+
this.baseUrl = baseUrl || this.getCurrentOrigin();
|
|
9
|
+
this.defaultTimeout = timeout;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Get the current origin (protocol + host + port)
|
|
13
|
+
*/
|
|
14
|
+
getCurrentOrigin() {
|
|
15
|
+
return typeof window !== "undefined" ? window.location.origin : "http://localhost";
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Resolve a relative path against the base URL
|
|
19
|
+
* @param path Relative or absolute path
|
|
20
|
+
* @returns Resolved absolute URL
|
|
21
|
+
*/
|
|
22
|
+
resolvePath(path) {
|
|
23
|
+
try {
|
|
24
|
+
if (path.startsWith("http://") || path.startsWith("https://")) {
|
|
25
|
+
return path;
|
|
26
|
+
}
|
|
27
|
+
if (path.startsWith("//")) {
|
|
28
|
+
return window.location.protocol + path;
|
|
29
|
+
}
|
|
30
|
+
if (path.startsWith("/")) {
|
|
31
|
+
return this.baseUrl + path;
|
|
32
|
+
}
|
|
33
|
+
return `${this.baseUrl}/${path}`;
|
|
34
|
+
} catch {
|
|
35
|
+
return path;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Load a single resource from a URL
|
|
40
|
+
* @param path URL or relative path to the resource
|
|
41
|
+
* @param options Optional loading configuration
|
|
42
|
+
* @returns Promise resolving to the resource content
|
|
43
|
+
*/
|
|
44
|
+
async load(path, options) {
|
|
45
|
+
const url = this.resolvePath(path);
|
|
46
|
+
try {
|
|
47
|
+
const fetchOptions = {
|
|
48
|
+
credentials: options?.credentials || "same-origin"
|
|
49
|
+
};
|
|
50
|
+
if (options?.headers) {
|
|
51
|
+
fetchOptions.headers = options.headers;
|
|
52
|
+
}
|
|
53
|
+
const controller = new AbortController();
|
|
54
|
+
const timeoutId = setTimeout(() => controller.abort(), this.defaultTimeout);
|
|
55
|
+
fetchOptions.signal = controller.signal;
|
|
56
|
+
const response = await fetch(url, fetchOptions);
|
|
57
|
+
clearTimeout(timeoutId);
|
|
58
|
+
if (!response.ok) {
|
|
59
|
+
throw new Error(
|
|
60
|
+
`HTTP ${response.status}: ${response.statusText} for URL: ${url}`
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
const text = await response.text();
|
|
64
|
+
return text;
|
|
65
|
+
} catch (error) {
|
|
66
|
+
if (error instanceof Error) {
|
|
67
|
+
if (error.name === "AbortError") {
|
|
68
|
+
throw new Error(
|
|
69
|
+
`Request timeout after ${this.defaultTimeout}ms for URL: ${url}`
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
throw new Error(`Failed to load resource from ${url}: ${error.message}`);
|
|
73
|
+
}
|
|
74
|
+
throw new Error(`Failed to load resource from ${url}: Unknown error`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Load multiple resources in parallel
|
|
79
|
+
* @param paths Array of URLs or paths
|
|
80
|
+
* @param options Optional loading configuration
|
|
81
|
+
* @returns Promise resolving to array of load results
|
|
82
|
+
*/
|
|
83
|
+
async loadMultiple(paths, options) {
|
|
84
|
+
const promises = paths.map(async (path) => {
|
|
85
|
+
try {
|
|
86
|
+
const data = await this.load(path, options);
|
|
87
|
+
return {
|
|
88
|
+
data,
|
|
89
|
+
path,
|
|
90
|
+
success: true
|
|
91
|
+
};
|
|
92
|
+
} catch (error) {
|
|
93
|
+
return {
|
|
94
|
+
data: "",
|
|
95
|
+
path,
|
|
96
|
+
success: false,
|
|
97
|
+
error: error instanceof Error ? error.message : String(error)
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
return Promise.all(promises);
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Check if the path is valid for loading in the browser
|
|
105
|
+
* @param path URL or path to check
|
|
106
|
+
* @returns true if the path can be loaded
|
|
107
|
+
*/
|
|
108
|
+
canLoad(path) {
|
|
109
|
+
const validPatterns = [
|
|
110
|
+
/^https?:\/\//i,
|
|
111
|
+
// Absolute HTTP(S) URLs
|
|
112
|
+
/^\/\//,
|
|
113
|
+
// Protocol-relative URLs
|
|
114
|
+
/^\//,
|
|
115
|
+
// Absolute paths
|
|
116
|
+
/^\.\.?\//
|
|
117
|
+
// Relative paths starting with ./ or ../
|
|
118
|
+
];
|
|
119
|
+
const hasFileExtension = /\.[a-z0-9]+$/i.test(path);
|
|
120
|
+
return validPatterns.some((pattern) => pattern.test(path)) || hasFileExtension;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Set a new base URL for resolving relative paths
|
|
124
|
+
* @param baseUrl New base URL
|
|
125
|
+
*/
|
|
126
|
+
setBaseUrl(baseUrl) {
|
|
127
|
+
this.baseUrl = baseUrl;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Get the current base URL
|
|
131
|
+
* @returns Current base URL
|
|
132
|
+
*/
|
|
133
|
+
getBaseUrl() {
|
|
134
|
+
return this.baseUrl;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Set the default request timeout
|
|
138
|
+
* @param timeout Timeout in milliseconds
|
|
139
|
+
*/
|
|
140
|
+
setTimeout(timeout) {
|
|
141
|
+
this.defaultTimeout = timeout;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
export {
|
|
145
|
+
BrowserResourceLoader
|
|
146
|
+
};
|