babylon.quarks 0.20.0 → 0.21.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 +22 -0
- package/README.md +2 -0
- package/dist/babylon.quarks.cjs +1 -1
- package/dist/babylon.quarks.cjs.map +1 -1
- package/dist/babylon.quarks.esm.js +1 -1
- package/dist/babylon.quarks.esm.js.map +1 -1
- package/dist/babylon.quarks.umd.min.js +1 -1
- package/dist/babylon.quarks.umd.min.js.map +1 -1
- package/dist/types/index.d.ts +114 -16
- package/package.json +1 -1
- package/src/QuarksLoader.ts +356 -52
- package/src/index.ts +7 -1
package/dist/types/index.d.ts
CHANGED
|
@@ -3535,6 +3535,18 @@ declare class ParticleSystem implements IParticleSystem {
|
|
|
3535
3535
|
clone(): ParticleSystem;
|
|
3536
3536
|
}
|
|
3537
3537
|
|
|
3538
|
+
/** Kind of linked asset the loader asks a {@link QuarksResourceResolver} to handle. */
|
|
3539
|
+
type QuarksResourceKind = 'effect' | 'geometry' | 'image';
|
|
3540
|
+
/**
|
|
3541
|
+
* Hooks for resolving relative source links (textures, geometry files) and loading their JSON.
|
|
3542
|
+
* Omit either method to keep the default (`baseUrl` join / `fetch`).
|
|
3543
|
+
*/
|
|
3544
|
+
interface QuarksResourceResolver {
|
|
3545
|
+
/** Resolve a relative url to an absolute / blob / data url. Default: {@link QuarksLoader.joinUrl}. */
|
|
3546
|
+
resolve?(url: string, kind: QuarksResourceKind, baseUrl: string): string | Promise<string>;
|
|
3547
|
+
/** Load a JSON asset (geometry files). Default: `fetch(resolvedUrl).json()`. */
|
|
3548
|
+
fetchJson?(resolvedUrl: string): Promise<unknown>;
|
|
3549
|
+
}
|
|
3538
3550
|
interface QuarksLoaderOptions {
|
|
3539
3551
|
baseUrl?: string;
|
|
3540
3552
|
/**
|
|
@@ -3546,19 +3558,96 @@ interface QuarksLoaderOptions {
|
|
|
3546
3558
|
* change one effect's texture settings without touching the others.
|
|
3547
3559
|
*/
|
|
3548
3560
|
shareResources?: boolean;
|
|
3561
|
+
/** Custom url / fetch backend (File picker, CDN, IndexedDB). */
|
|
3562
|
+
resolver?: QuarksResourceResolver;
|
|
3563
|
+
}
|
|
3564
|
+
/** One in-memory effect for {@link QuarksLoader.loadFromJSONAll}. */
|
|
3565
|
+
interface QuarksJSONLoadEntry {
|
|
3566
|
+
json: unknown;
|
|
3567
|
+
/** Overrides loader `baseUrl` for relative texture / geometry links in this effect. */
|
|
3568
|
+
baseUrl?: string;
|
|
3569
|
+
}
|
|
3570
|
+
/**
|
|
3571
|
+
* Options for {@link QuarksLoader.loadAll} / {@link QuarksLoader.loadFromJSONAll}.
|
|
3572
|
+
* Batch loads share the per-scene fetch cache and `shareResources`, so N effects that point at
|
|
3573
|
+
* the same `assets/geometries/*.json` or texture only pay for that asset once.
|
|
3574
|
+
*/
|
|
3575
|
+
interface QuarksBatchLoadOptions {
|
|
3576
|
+
/**
|
|
3577
|
+
* Max concurrent effect-document fetches (phase 1). Linked geometry/image resolve still runs
|
|
3578
|
+
* across the whole pack in parallel so shared urls coalesce. Default: all at once.
|
|
3579
|
+
*/
|
|
3580
|
+
concurrency?: number;
|
|
3581
|
+
/**
|
|
3582
|
+
* Called once per effect when it has fully settled (parsed successfully, or failed under
|
|
3583
|
+
* `continueOnError`). `done` is how many of `total` have settled so far.
|
|
3584
|
+
*/
|
|
3585
|
+
onProgress?: (done: number, total: number) => void;
|
|
3586
|
+
/**
|
|
3587
|
+
* When true, a failed effect becomes `null` in the result array and the rest keep loading.
|
|
3588
|
+
* When false (default), the first failure rejects the whole batch.
|
|
3589
|
+
*/
|
|
3590
|
+
continueOnError?: boolean;
|
|
3549
3591
|
}
|
|
3550
3592
|
declare class QuarksLoader {
|
|
3551
3593
|
private scene;
|
|
3552
3594
|
private options;
|
|
3553
3595
|
constructor(scene: Scene, options?: QuarksLoaderOptions);
|
|
3596
|
+
/** Fetches an effect JSON from `url`, resolves linked sources, and parses it. */
|
|
3554
3597
|
load(url: string): Promise<TransformNode>;
|
|
3598
|
+
/**
|
|
3599
|
+
* Resolves linked geometry / image urls on `json` (via {@link resolveResources}) then parses.
|
|
3600
|
+
* Use when the effect JSON is already in hand (File, CDN payload, editor import).
|
|
3601
|
+
*/
|
|
3602
|
+
loadFromJSON(json: any, baseUrl?: string): Promise<TransformNode>;
|
|
3603
|
+
/**
|
|
3604
|
+
* Loads many effects in parallel for battle / level preload. Phase 1 fetches every effect
|
|
3605
|
+
* JSON (optionally concurrency-limited); phase 2 resolves all linked geometries/images across
|
|
3606
|
+
* the pack together so shared asset urls hit the fetch cache once; phase 3 parses.
|
|
3607
|
+
*
|
|
3608
|
+
* @example
|
|
3609
|
+
* ```ts
|
|
3610
|
+
* const roots = await loader.loadAll([
|
|
3611
|
+
* 'fx/hit.json', 'fx/explosion.json', 'fx/heal.json',
|
|
3612
|
+
* ], { concurrency: 6, continueOnError: true });
|
|
3613
|
+
* ```
|
|
3614
|
+
*/
|
|
3615
|
+
loadAll(urls: string[], options?: QuarksBatchLoadOptions & {
|
|
3616
|
+
continueOnError?: false;
|
|
3617
|
+
}): Promise<TransformNode[]>;
|
|
3618
|
+
loadAll(urls: string[], options: QuarksBatchLoadOptions & {
|
|
3619
|
+
continueOnError: true;
|
|
3620
|
+
}): Promise<(TransformNode | null)[]>;
|
|
3621
|
+
/**
|
|
3622
|
+
* Same two-phase batch as {@link loadAll}, but effect JSON is already in memory (asset pack,
|
|
3623
|
+
* IndexedDB, editor). Entries may be bare JSON or `{ json, baseUrl }`.
|
|
3624
|
+
*/
|
|
3625
|
+
loadFromJSONAll(entries: Array<QuarksJSONLoadEntry | unknown>, options?: QuarksBatchLoadOptions & {
|
|
3626
|
+
continueOnError?: false;
|
|
3627
|
+
}): Promise<TransformNode[]>;
|
|
3628
|
+
loadFromJSONAll(entries: Array<QuarksJSONLoadEntry | unknown>, options: QuarksBatchLoadOptions & {
|
|
3629
|
+
continueOnError: true;
|
|
3630
|
+
}): Promise<(TransformNode | null)[]>;
|
|
3631
|
+
/**
|
|
3632
|
+
* Inlines external geometry files and rewrites relative image urls through the loader's
|
|
3633
|
+
* {@link QuarksResourceResolver}. Idempotent for already-inlined geometries. Failures warn
|
|
3634
|
+
* and leave the entry as is.
|
|
3635
|
+
*/
|
|
3636
|
+
resolveResources(json: any, baseUrl?: string): Promise<void>;
|
|
3637
|
+
/**
|
|
3638
|
+
* Same as instance {@link resolveResources}, but usable without constructing a loader (e.g.
|
|
3639
|
+
* editor import that only prepares JSON). Pass `scene` to share the per-scene fetch cache.
|
|
3640
|
+
*/
|
|
3641
|
+
static resolveLinkedResources(json: any, options?: {
|
|
3642
|
+
baseUrl?: string;
|
|
3643
|
+
resolver?: QuarksResourceResolver;
|
|
3644
|
+
scene?: Scene;
|
|
3645
|
+
}): Promise<void>;
|
|
3555
3646
|
/**
|
|
3556
3647
|
* Inlines geometries that an export wrote to their own file (`{uuid, type: 'QuarksGeometry',
|
|
3557
|
-
* url}`
|
|
3558
|
-
*
|
|
3559
|
-
*
|
|
3560
|
-
* this with `fetch`; call it yourself before `parse()` when the files come from elsewhere.
|
|
3561
|
-
* A geometry that fails to load is left as is and warned about.
|
|
3648
|
+
* url}`). Prefer {@link load}, {@link loadFromJSON}, or {@link resolveResources} — those use
|
|
3649
|
+
* the loader's resolver and fetch cache. Kept for callers that already have a custom
|
|
3650
|
+
* `loadGeometry` and no `QuarksLoader` instance.
|
|
3562
3651
|
*/
|
|
3563
3652
|
static resolveExternalGeometries(json: any, loadGeometry: (url: string) => Promise<any>): Promise<void>;
|
|
3564
3653
|
parse(json: any, baseUrl?: string): TransformNode;
|
|
@@ -3566,15 +3655,31 @@ declare class QuarksLoader {
|
|
|
3566
3655
|
/** The scene's copy of an identical geometry when resources are shared, else `geometry` itself. */
|
|
3567
3656
|
private share;
|
|
3568
3657
|
/**
|
|
3569
|
-
* Forgets the textures and
|
|
3570
|
-
* {@link QuarksLoaderOptions.shareResources}). Effects already loaded keep
|
|
3571
|
-
* load creates new ones. Textures are not disposed — they may still be in use.
|
|
3658
|
+
* Forgets the textures, geometries, and in-flight JSON fetches shared between loads into
|
|
3659
|
+
* `scene` (see {@link QuarksLoaderOptions.shareResources}). Effects already loaded keep
|
|
3660
|
+
* theirs; the next load creates new ones. Textures are not disposed — they may still be in use.
|
|
3572
3661
|
*/
|
|
3573
3662
|
static clearSharedResources(scene: Scene): void;
|
|
3663
|
+
/** Joins `baseUrl` with a relative `url`; leaves data/blob/absolute/root-absolute urls alone. */
|
|
3664
|
+
static joinUrl(baseUrl: string, url: string): string;
|
|
3574
3665
|
private parseSphereGeometry;
|
|
3575
3666
|
private parseBufferGeometry;
|
|
3576
3667
|
private extractInterleavedAttribute;
|
|
3577
3668
|
private int32ArrayToFloat32;
|
|
3669
|
+
private resolveResourceUrl;
|
|
3670
|
+
/** Fetches one effect JSON; documents are not fetch-cached because resolve mutates them. */
|
|
3671
|
+
private fetchEffectDocument;
|
|
3672
|
+
/**
|
|
3673
|
+
* Resolve every document's linked resources in parallel (shared urls coalesce via the
|
|
3674
|
+
* scene fetch cache), then parse. Each index settles exactly once for `onProgress`.
|
|
3675
|
+
*/
|
|
3676
|
+
private loadDocuments;
|
|
3677
|
+
/** Runs `fn` over `items` with at most `concurrency` in flight (or all if unset/≤0). */
|
|
3678
|
+
private static mapPool;
|
|
3679
|
+
private static resolveWith;
|
|
3680
|
+
/** One in-flight fetch per scene + resolved url when `scene` is set; otherwise uncached. */
|
|
3681
|
+
private static fetchJsonWith;
|
|
3682
|
+
private static inlineGeometryEntries;
|
|
3578
3683
|
private parseImages;
|
|
3579
3684
|
/**
|
|
3580
3685
|
* Flags an image entry that is plainly not an image, which otherwise only
|
|
@@ -3586,13 +3691,6 @@ declare class QuarksLoader {
|
|
|
3586
3691
|
* file backs.
|
|
3587
3692
|
*/
|
|
3588
3693
|
private static warnIfNotAnImageUrl;
|
|
3589
|
-
/**
|
|
3590
|
-
* `img.url` is already absolute for data/blob URIs and full http(s) URLs — common for
|
|
3591
|
-
* "portable" exports that embed textures as base64 (see ensurePortableTextureUrl in
|
|
3592
|
-
* babylon.quarks-editor). Prepending baseUrl to those turns e.g.
|
|
3593
|
-
* "data:image/png;base64,AAAA" into an invalid path like "/some/dir/data:image/png;base64,AAAA".
|
|
3594
|
-
*/
|
|
3595
|
-
private static resolveUrl;
|
|
3596
3694
|
private parseTextures;
|
|
3597
3695
|
private mapWrapMode;
|
|
3598
3696
|
private parseMaterials;
|
|
@@ -3828,4 +3926,4 @@ declare class TrailBatch extends VFXBatch {
|
|
|
3828
3926
|
}
|
|
3829
3927
|
|
|
3830
3928
|
export { ApplyCollision, ApplyForce, ApplySequences, AxisAngleGenerator, BatchedParticleRenderer, BatchedRenderer, BehaviorFromJSON, BehaviorTypes, Bezier, BoxEmitFrom, BoxEmitter, ChangeEmitDirection, CircleEmitter, ColorBySpeed, ColorGeneratorFromJSON, ColorOverLife, ColorRange, ConeEmitter, ConstantColor, ConstantValue, DEG2RAD, DonutEmitter, EdgeEmitter, EmitSubParticleSystem, EmitterFromJSON, EmitterMode, EmitterShapes, Euler, EulerGenerator, ForceOverLife, FrameOverLife, GeneratorFromJSON, Gradient, GravityForce, GridEmitter, HemisphereEmitter, InheritVelocity, IntervalValue, LimitSpeedOverLife, MathUtils, Matrix3, Matrix4, MeshSurfaceEmitter, MeshSurfaceEmitterPlugin, NodeParticle, Noise, OrbitOverLife, ParticleEmitter, ParticleMeshPhysicsMaterial, ParticleMeshStandardMaterial, ParticleStore, ParticleSystem, PiecewiseBezier, PiecewiseFunction, Plugins, PointEmitter, QuarksLoader, QuarksPrefab, QuarksUtil, Quaternion, RAD2DEG, RandomColor, RandomColorBetweenGradient, RandomQuatGenerator, RecordState, RectangleEmitter, RenderMode, Rotation3DOverLife, RotationBySpeed, RotationGeneratorFromJSON, RotationOverLife, SequencerFromJSON, ShapeTransform, SizeBySpeed, SizeOverLife, SpeedOverLife, SphereEmitter, SpriteBatch, SpriteParticle, SubParticleEmitMode, TextureSequencer, TrailBatch, TrailParticle, TurbulenceField, VFXBatch, ValueGeneratorFromJSON, Vector2, Vector3, Vector3Function, Vector3GeneratorFromJSON, Vector3View, Vector4, Vector4View, VelocityOverLife, WebGLCoordinateSystem, WebGPUCoordinateSystem, WidthOverLength, cacheEnvAtlas, ceilPowerOfTwo, clamp, createEnvAtlasFromFaceUrls, cubeFaceUrls, damp, degToRad, denormalize, ensureEnvAtlasFromCube, euclideanModulo, floorPowerOfTwo, generateUUID, getCachedEnvAtlas, getPhysicsResolver, getRegisteredShaderChunks, getShaderChunk, getValueFromEmitterMode, inverseLerp, isPowerOfTwo, lerp, loadPlugin, _default$7 as localParticlePhysicsVertShader, _default$6 as localParticleVertShader, mapLinear, normalize, _default$5 as particleFragShader, _default$4 as particlePhysicsFragShader, _default$3 as particleVertShader, pingpong, planBehaviorFusion, radToDeg, randFloat, randFloatSpread, randInt, registerShaderChunks, seededRandom, setPhysicsResolver, setQuaternionFromProperEuler, smootherstep, smoothstep, _default$2 as stretchedBillboardVertShader, _default$1 as trailFragShader, _default as trailVertShader, unloadPlugin };
|
|
3831
|
-
export type { AdaptivePerformanceOptions, AdaptivePerformanceState, AnimationData, BabylonMetaData, BabylonParticleMaterialParameters, Behavior, BehaviorPlugin, BillBoardSettings, BoxEmitterParameters, BurstParameters, BurstParametersJSON, CircleEmitterParameters, ColorGenerator, ConeEmitterParameters, Constructable, DonutEmitterParameters, EdgeEmitterParameters, EmissionState, EmitterShape, EmitterShapePlugin, EulerOrder, FieldType, FunctionColorGenerator, FunctionJSON, FunctionValueGenerator, FusedPass, FusionStep, GeneratorMemory, GridEmitterParameters, HemisphereEmitterParameters, IAnimationData, IEmitter, IParticle, IParticleSystem, IPrefab, InheritVelocityMode, JsonMetaData, MeshSettings, ParameterPair, ParameterType, Particle, ParticleSystemEvent, ParticleSystemEventType, ParticleSystemJSONParameters, ParticleSystemParameters, Plugin, QuarksLoaderOptions, QuarksTimelineClip, RectangleEmitterParameters, RendererEmitterSettings, Resource, RotationGenerator, Sequencer, SerializationOptions, ShapeJSON, ShapeTransformJSON, SphereEmitterParameters, StoredBatchSettings, StretchedBillBoardSettings, TrailSettings, VFXBatchSettings, ValueGenerator, Vector3Generator, VelocityOverLifeSpace };
|
|
3929
|
+
export type { AdaptivePerformanceOptions, AdaptivePerformanceState, AnimationData, BabylonMetaData, BabylonParticleMaterialParameters, Behavior, BehaviorPlugin, BillBoardSettings, BoxEmitterParameters, BurstParameters, BurstParametersJSON, CircleEmitterParameters, ColorGenerator, ConeEmitterParameters, Constructable, DonutEmitterParameters, EdgeEmitterParameters, EmissionState, EmitterShape, EmitterShapePlugin, EulerOrder, FieldType, FunctionColorGenerator, FunctionJSON, FunctionValueGenerator, FusedPass, FusionStep, GeneratorMemory, GridEmitterParameters, HemisphereEmitterParameters, IAnimationData, IEmitter, IParticle, IParticleSystem, IPrefab, InheritVelocityMode, JsonMetaData, MeshSettings, ParameterPair, ParameterType, Particle, ParticleSystemEvent, ParticleSystemEventType, ParticleSystemJSONParameters, ParticleSystemParameters, Plugin, QuarksBatchLoadOptions, QuarksJSONLoadEntry, QuarksLoaderOptions, QuarksResourceKind, QuarksResourceResolver, QuarksTimelineClip, RectangleEmitterParameters, RendererEmitterSettings, Resource, RotationGenerator, Sequencer, SerializationOptions, ShapeJSON, ShapeTransformJSON, SphereEmitterParameters, StoredBatchSettings, StretchedBillBoardSettings, TrailSettings, VFXBatchSettings, ValueGenerator, Vector3Generator, VelocityOverLifeSpace };
|
package/package.json
CHANGED
package/src/QuarksLoader.ts
CHANGED
|
@@ -12,6 +12,20 @@ import {ParticleSystem} from './ParticleSystem';
|
|
|
12
12
|
import {QuarksPrefab} from './QuarksPrefab';
|
|
13
13
|
import {ensureTriangleIndices} from './geometryUtil';
|
|
14
14
|
|
|
15
|
+
/** Kind of linked asset the loader asks a {@link QuarksResourceResolver} to handle. */
|
|
16
|
+
export type QuarksResourceKind = 'effect' | 'geometry' | 'image';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Hooks for resolving relative source links (textures, geometry files) and loading their JSON.
|
|
20
|
+
* Omit either method to keep the default (`baseUrl` join / `fetch`).
|
|
21
|
+
*/
|
|
22
|
+
export interface QuarksResourceResolver {
|
|
23
|
+
/** Resolve a relative url to an absolute / blob / data url. Default: {@link QuarksLoader.joinUrl}. */
|
|
24
|
+
resolve?(url: string, kind: QuarksResourceKind, baseUrl: string): string | Promise<string>;
|
|
25
|
+
/** Load a JSON asset (geometry files). Default: `fetch(resolvedUrl).json()`. */
|
|
26
|
+
fetchJson?(resolvedUrl: string): Promise<unknown>;
|
|
27
|
+
}
|
|
28
|
+
|
|
15
29
|
export interface QuarksLoaderOptions {
|
|
16
30
|
baseUrl?: string;
|
|
17
31
|
/**
|
|
@@ -23,6 +37,38 @@ export interface QuarksLoaderOptions {
|
|
|
23
37
|
* change one effect's texture settings without touching the others.
|
|
24
38
|
*/
|
|
25
39
|
shareResources?: boolean;
|
|
40
|
+
/** Custom url / fetch backend (File picker, CDN, IndexedDB). */
|
|
41
|
+
resolver?: QuarksResourceResolver;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** One in-memory effect for {@link QuarksLoader.loadFromJSONAll}. */
|
|
45
|
+
export interface QuarksJSONLoadEntry {
|
|
46
|
+
json: unknown;
|
|
47
|
+
/** Overrides loader `baseUrl` for relative texture / geometry links in this effect. */
|
|
48
|
+
baseUrl?: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Options for {@link QuarksLoader.loadAll} / {@link QuarksLoader.loadFromJSONAll}.
|
|
53
|
+
* Batch loads share the per-scene fetch cache and `shareResources`, so N effects that point at
|
|
54
|
+
* the same `assets/geometries/*.json` or texture only pay for that asset once.
|
|
55
|
+
*/
|
|
56
|
+
export interface QuarksBatchLoadOptions {
|
|
57
|
+
/**
|
|
58
|
+
* Max concurrent effect-document fetches (phase 1). Linked geometry/image resolve still runs
|
|
59
|
+
* across the whole pack in parallel so shared urls coalesce. Default: all at once.
|
|
60
|
+
*/
|
|
61
|
+
concurrency?: number;
|
|
62
|
+
/**
|
|
63
|
+
* Called once per effect when it has fully settled (parsed successfully, or failed under
|
|
64
|
+
* `continueOnError`). `done` is how many of `total` have settled so far.
|
|
65
|
+
*/
|
|
66
|
+
onProgress?: (done: number, total: number) => void;
|
|
67
|
+
/**
|
|
68
|
+
* When true, a failed effect becomes `null` in the result array and the rest keep loading.
|
|
69
|
+
* When false (default), the first failure rejects the whole batch.
|
|
70
|
+
*/
|
|
71
|
+
continueOnError?: boolean;
|
|
26
72
|
}
|
|
27
73
|
|
|
28
74
|
interface ParsedGeometry {
|
|
@@ -35,6 +81,8 @@ interface ParsedGeometry {
|
|
|
35
81
|
/** Textures and geometries reused across loads, per scene. See QuarksLoaderOptions.shareResources. */
|
|
36
82
|
const sharedTextures = new WeakMap<Scene, Map<string, Texture>>();
|
|
37
83
|
const sharedGeometries = new WeakMap<Scene, Map<string, ParsedGeometry>>();
|
|
84
|
+
/** In-flight / completed JSON fetches keyed by `json|resolvedUrl`, per scene. */
|
|
85
|
+
const sharedFetches = new WeakMap<Scene, Map<string, Promise<unknown>>>();
|
|
38
86
|
|
|
39
87
|
function sceneMap<T>(maps: WeakMap<Scene, Map<string, T>>, scene: Scene): Map<string, T> {
|
|
40
88
|
let map = maps.get(scene);
|
|
@@ -86,49 +134,131 @@ export class QuarksLoader {
|
|
|
86
134
|
this.options = options;
|
|
87
135
|
}
|
|
88
136
|
|
|
137
|
+
/** Fetches an effect JSON from `url`, resolves linked sources, and parses it. */
|
|
89
138
|
async load(url: string): Promise<TransformNode> {
|
|
90
|
-
const baseUrl = this.
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
139
|
+
const {json, baseUrl} = await this.fetchEffectDocument(url);
|
|
140
|
+
return this.loadFromJSON(json, baseUrl);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Resolves linked geometry / image urls on `json` (via {@link resolveResources}) then parses.
|
|
145
|
+
* Use when the effect JSON is already in hand (File, CDN payload, editor import).
|
|
146
|
+
*/
|
|
147
|
+
async loadFromJSON(json: any, baseUrl?: string): Promise<TransformNode> {
|
|
148
|
+
const resolvedBase = baseUrl ?? this.options.baseUrl ?? '';
|
|
149
|
+
await this.resolveResources(json, resolvedBase);
|
|
150
|
+
return this.parse(json, resolvedBase);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Loads many effects in parallel for battle / level preload. Phase 1 fetches every effect
|
|
155
|
+
* JSON (optionally concurrency-limited); phase 2 resolves all linked geometries/images across
|
|
156
|
+
* the pack together so shared asset urls hit the fetch cache once; phase 3 parses.
|
|
157
|
+
*
|
|
158
|
+
* @example
|
|
159
|
+
* ```ts
|
|
160
|
+
* const roots = await loader.loadAll([
|
|
161
|
+
* 'fx/hit.json', 'fx/explosion.json', 'fx/heal.json',
|
|
162
|
+
* ], { concurrency: 6, continueOnError: true });
|
|
163
|
+
* ```
|
|
164
|
+
*/
|
|
165
|
+
loadAll(urls: string[], options?: QuarksBatchLoadOptions & {continueOnError?: false}): Promise<TransformNode[]>;
|
|
166
|
+
loadAll(
|
|
167
|
+
urls: string[],
|
|
168
|
+
options: QuarksBatchLoadOptions & {continueOnError: true}
|
|
169
|
+
): Promise<(TransformNode | null)[]>;
|
|
170
|
+
async loadAll(urls: string[], options: QuarksBatchLoadOptions = {}): Promise<(TransformNode | null)[]> {
|
|
171
|
+
const docs = await QuarksLoader.mapPool(urls, options.concurrency, async (url, index) => {
|
|
172
|
+
try {
|
|
173
|
+
return await this.fetchEffectDocument(url);
|
|
174
|
+
} catch (err) {
|
|
175
|
+
if (!options.continueOnError) {
|
|
176
|
+
throw err;
|
|
177
|
+
}
|
|
178
|
+
console.warn(`[QuarksLoader] Failed to fetch effect "${url}" (${index + 1}/${urls.length}):`, err);
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
99
181
|
});
|
|
100
|
-
return this.
|
|
182
|
+
return this.loadDocuments(docs, options);
|
|
101
183
|
}
|
|
102
184
|
|
|
103
185
|
/**
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
* `loadGeometry`, and the `positions / indices / uvs / normals` it returns replace the url in
|
|
107
|
-
* place, so `parse()` (which is synchronous) sees an ordinary inline geometry. `load()` does
|
|
108
|
-
* this with `fetch`; call it yourself before `parse()` when the files come from elsewhere.
|
|
109
|
-
* A geometry that fails to load is left as is and warned about.
|
|
186
|
+
* Same two-phase batch as {@link loadAll}, but effect JSON is already in memory (asset pack,
|
|
187
|
+
* IndexedDB, editor). Entries may be bare JSON or `{ json, baseUrl }`.
|
|
110
188
|
*/
|
|
111
|
-
|
|
112
|
-
|
|
189
|
+
loadFromJSONAll(
|
|
190
|
+
entries: Array<QuarksJSONLoadEntry | unknown>,
|
|
191
|
+
options?: QuarksBatchLoadOptions & {continueOnError?: false}
|
|
192
|
+
): Promise<TransformNode[]>;
|
|
193
|
+
loadFromJSONAll(
|
|
194
|
+
entries: Array<QuarksJSONLoadEntry | unknown>,
|
|
195
|
+
options: QuarksBatchLoadOptions & {continueOnError: true}
|
|
196
|
+
): Promise<(TransformNode | null)[]>;
|
|
197
|
+
async loadFromJSONAll(
|
|
198
|
+
entries: Array<QuarksJSONLoadEntry | unknown>,
|
|
199
|
+
options: QuarksBatchLoadOptions = {}
|
|
200
|
+
): Promise<(TransformNode | null)[]> {
|
|
201
|
+
const docs: Array<{json: any; baseUrl: string} | null> = entries.map((entry) => {
|
|
202
|
+
if (entry !== null && typeof entry === 'object' && 'json' in (entry as object)) {
|
|
203
|
+
const typed = entry as QuarksJSONLoadEntry;
|
|
204
|
+
return {json: typed.json as any, baseUrl: typed.baseUrl ?? this.options.baseUrl ?? ''};
|
|
205
|
+
}
|
|
206
|
+
return {json: entry as any, baseUrl: this.options.baseUrl ?? ''};
|
|
207
|
+
});
|
|
208
|
+
return this.loadDocuments(docs, options);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Inlines external geometry files and rewrites relative image urls through the loader's
|
|
213
|
+
* {@link QuarksResourceResolver}. Idempotent for already-inlined geometries. Failures warn
|
|
214
|
+
* and leave the entry as is.
|
|
215
|
+
*/
|
|
216
|
+
async resolveResources(json: any, baseUrl: string = ''): Promise<void> {
|
|
217
|
+
await QuarksLoader.resolveLinkedResources(json, {
|
|
218
|
+
baseUrl,
|
|
219
|
+
resolver: this.options.resolver,
|
|
220
|
+
scene: this.scene,
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Same as instance {@link resolveResources}, but usable without constructing a loader (e.g.
|
|
226
|
+
* editor import that only prepares JSON). Pass `scene` to share the per-scene fetch cache.
|
|
227
|
+
*/
|
|
228
|
+
static async resolveLinkedResources(
|
|
229
|
+
json: any,
|
|
230
|
+
options: {baseUrl?: string; resolver?: QuarksResourceResolver; scene?: Scene} = {}
|
|
231
|
+
): Promise<void> {
|
|
232
|
+
const baseUrl = options.baseUrl ?? '';
|
|
233
|
+
const resolver = options.resolver;
|
|
234
|
+
const scene = options.scene;
|
|
235
|
+
|
|
236
|
+
await QuarksLoader.inlineGeometryEntries(json, async (geometryUrl) => {
|
|
237
|
+
const resolved = await QuarksLoader.resolveWith(resolver, geometryUrl, 'geometry', baseUrl);
|
|
238
|
+
return QuarksLoader.fetchJsonWith(resolver, resolved, scene);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
const images: any[] = Array.isArray(json?.images) ? json.images : [];
|
|
113
242
|
await Promise.all(
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
if (data?.[key] !== undefined) {
|
|
121
|
-
geom[key] = data[key];
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
delete geom.url;
|
|
125
|
-
} catch (err) {
|
|
126
|
-
console.warn(`[QuarksLoader] Could not load geometry "${geom.url}":`, err);
|
|
127
|
-
}
|
|
128
|
-
})
|
|
243
|
+
images.map(async (img) => {
|
|
244
|
+
if (typeof img?.url !== 'string' || !img.url) {
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
img.url = await QuarksLoader.resolveWith(resolver, img.url, 'image', baseUrl);
|
|
248
|
+
})
|
|
129
249
|
);
|
|
130
250
|
}
|
|
131
251
|
|
|
252
|
+
/**
|
|
253
|
+
* Inlines geometries that an export wrote to their own file (`{uuid, type: 'QuarksGeometry',
|
|
254
|
+
* url}`). Prefer {@link load}, {@link loadFromJSON}, or {@link resolveResources} — those use
|
|
255
|
+
* the loader's resolver and fetch cache. Kept for callers that already have a custom
|
|
256
|
+
* `loadGeometry` and no `QuarksLoader` instance.
|
|
257
|
+
*/
|
|
258
|
+
static async resolveExternalGeometries(json: any, loadGeometry: (url: string) => Promise<any>): Promise<void> {
|
|
259
|
+
await QuarksLoader.inlineGeometryEntries(json, loadGeometry);
|
|
260
|
+
}
|
|
261
|
+
|
|
132
262
|
parse(json: any, baseUrl: string = ''): TransformNode {
|
|
133
263
|
const meta: LoadedMeta = {
|
|
134
264
|
textures: {},
|
|
@@ -175,8 +305,8 @@ export class QuarksLoader {
|
|
|
175
305
|
} else if (geom.type === 'QuarksGeometry') {
|
|
176
306
|
if (typeof geom.url === 'string' && !geom.positions) {
|
|
177
307
|
console.warn(
|
|
178
|
-
`[QuarksLoader] Geometry "${geom.url}" is an external file; use load()
|
|
179
|
-
'
|
|
308
|
+
`[QuarksLoader] Geometry "${geom.url}" is an external file; use load(), ` +
|
|
309
|
+
'loadFromJSON(), or resolveResources() before parse().'
|
|
180
310
|
);
|
|
181
311
|
}
|
|
182
312
|
meta.geometries[geom.uuid] = this.share({
|
|
@@ -205,13 +335,22 @@ export class QuarksLoader {
|
|
|
205
335
|
}
|
|
206
336
|
|
|
207
337
|
/**
|
|
208
|
-
* Forgets the textures and
|
|
209
|
-
* {@link QuarksLoaderOptions.shareResources}). Effects already loaded keep
|
|
210
|
-
* load creates new ones. Textures are not disposed — they may still be in use.
|
|
338
|
+
* Forgets the textures, geometries, and in-flight JSON fetches shared between loads into
|
|
339
|
+
* `scene` (see {@link QuarksLoaderOptions.shareResources}). Effects already loaded keep
|
|
340
|
+
* theirs; the next load creates new ones. Textures are not disposed — they may still be in use.
|
|
211
341
|
*/
|
|
212
342
|
static clearSharedResources(scene: Scene): void {
|
|
213
343
|
sharedTextures.delete(scene);
|
|
214
344
|
sharedGeometries.delete(scene);
|
|
345
|
+
sharedFetches.delete(scene);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/** Joins `baseUrl` with a relative `url`; leaves data/blob/absolute/root-absolute urls alone. */
|
|
349
|
+
static joinUrl(baseUrl: string, url: string): string {
|
|
350
|
+
if (/^(data|blob):/i.test(url) || /^[a-z][a-z0-9+.-]*:\/\//i.test(url) || url.startsWith('/')) {
|
|
351
|
+
return url;
|
|
352
|
+
}
|
|
353
|
+
return baseUrl + url;
|
|
215
354
|
}
|
|
216
355
|
|
|
217
356
|
private parseSphereGeometry(geom: any): ParsedGeometry {
|
|
@@ -366,13 +505,191 @@ export class QuarksLoader {
|
|
|
366
505
|
return new Float32Array(buffer) as any;
|
|
367
506
|
}
|
|
368
507
|
|
|
508
|
+
private async resolveResourceUrl(url: string, kind: QuarksResourceKind, baseUrl: string): Promise<string> {
|
|
509
|
+
return QuarksLoader.resolveWith(this.options.resolver, url, kind, baseUrl);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/** Fetches one effect JSON; documents are not fetch-cached because resolve mutates them. */
|
|
513
|
+
private async fetchEffectDocument(url: string): Promise<{json: any; baseUrl: string}> {
|
|
514
|
+
const baseUrl = this.options.baseUrl || url.substring(0, url.lastIndexOf('/') + 1);
|
|
515
|
+
const resolvedEffect = await this.resolveResourceUrl(url, 'effect', baseUrl);
|
|
516
|
+
const json = await QuarksLoader.fetchJsonWith(this.options.resolver, resolvedEffect, undefined);
|
|
517
|
+
return {json, baseUrl};
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* Resolve every document's linked resources in parallel (shared urls coalesce via the
|
|
522
|
+
* scene fetch cache), then parse. Each index settles exactly once for `onProgress`.
|
|
523
|
+
*/
|
|
524
|
+
private async loadDocuments(
|
|
525
|
+
docs: Array<{json: any; baseUrl: string} | null>,
|
|
526
|
+
options: QuarksBatchLoadOptions
|
|
527
|
+
): Promise<(TransformNode | null)[]> {
|
|
528
|
+
const total = docs.length;
|
|
529
|
+
if (total === 0) {
|
|
530
|
+
return [];
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
const results: (TransformNode | null)[] = new Array(total);
|
|
534
|
+
const settled = new Array<boolean>(total).fill(false);
|
|
535
|
+
let done = 0;
|
|
536
|
+
const settle = (index: number, value: TransformNode | null) => {
|
|
537
|
+
if (settled[index]) {
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
settled[index] = true;
|
|
541
|
+
results[index] = value;
|
|
542
|
+
done++;
|
|
543
|
+
options.onProgress?.(done, total);
|
|
544
|
+
};
|
|
545
|
+
|
|
546
|
+
await Promise.all(
|
|
547
|
+
docs.map(async (doc, i) => {
|
|
548
|
+
if (!doc) {
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
try {
|
|
552
|
+
await this.resolveResources(doc.json, doc.baseUrl);
|
|
553
|
+
} catch (err) {
|
|
554
|
+
if (!options.continueOnError) {
|
|
555
|
+
throw err;
|
|
556
|
+
}
|
|
557
|
+
console.warn(`[QuarksLoader] Failed to resolve resources for effect ${i + 1}/${total}:`, err);
|
|
558
|
+
docs[i] = null;
|
|
559
|
+
settle(i, null);
|
|
560
|
+
}
|
|
561
|
+
})
|
|
562
|
+
);
|
|
563
|
+
|
|
564
|
+
for (let i = 0; i < total; i++) {
|
|
565
|
+
if (settled[i]) {
|
|
566
|
+
continue;
|
|
567
|
+
}
|
|
568
|
+
const doc = docs[i];
|
|
569
|
+
if (!doc) {
|
|
570
|
+
settle(i, null);
|
|
571
|
+
continue;
|
|
572
|
+
}
|
|
573
|
+
try {
|
|
574
|
+
settle(i, this.parse(doc.json, doc.baseUrl));
|
|
575
|
+
} catch (err) {
|
|
576
|
+
if (!options.continueOnError) {
|
|
577
|
+
throw err;
|
|
578
|
+
}
|
|
579
|
+
console.warn(`[QuarksLoader] Failed to parse effect ${i + 1}/${total}:`, err);
|
|
580
|
+
settle(i, null);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
return results;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/** Runs `fn` over `items` with at most `concurrency` in flight (or all if unset/≤0). */
|
|
587
|
+
private static async mapPool<T, R>(
|
|
588
|
+
items: T[],
|
|
589
|
+
concurrency: number | undefined,
|
|
590
|
+
fn: (item: T, index: number) => Promise<R>
|
|
591
|
+
): Promise<R[]> {
|
|
592
|
+
if (items.length === 0) {
|
|
593
|
+
return [];
|
|
594
|
+
}
|
|
595
|
+
const limit = concurrency !== undefined && concurrency > 0 ? concurrency : items.length;
|
|
596
|
+
const results = new Array<R>(items.length);
|
|
597
|
+
let next = 0;
|
|
598
|
+
|
|
599
|
+
const worker = async () => {
|
|
600
|
+
while (next < items.length) {
|
|
601
|
+
const index = next++;
|
|
602
|
+
results[index] = await fn(items[index], index);
|
|
603
|
+
}
|
|
604
|
+
};
|
|
605
|
+
|
|
606
|
+
const workers = Math.min(limit, items.length);
|
|
607
|
+
await Promise.all(Array.from({length: workers}, () => worker()));
|
|
608
|
+
return results;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
private static async resolveWith(
|
|
612
|
+
resolver: QuarksResourceResolver | undefined,
|
|
613
|
+
url: string,
|
|
614
|
+
kind: QuarksResourceKind,
|
|
615
|
+
baseUrl: string
|
|
616
|
+
): Promise<string> {
|
|
617
|
+
if (resolver?.resolve) {
|
|
618
|
+
return resolver.resolve(url, kind, baseUrl);
|
|
619
|
+
}
|
|
620
|
+
return QuarksLoader.joinUrl(baseUrl, url);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
/** One in-flight fetch per scene + resolved url when `scene` is set; otherwise uncached. */
|
|
624
|
+
private static fetchJsonWith(
|
|
625
|
+
resolver: QuarksResourceResolver | undefined,
|
|
626
|
+
resolvedUrl: string,
|
|
627
|
+
scene: Scene | undefined
|
|
628
|
+
): Promise<unknown> {
|
|
629
|
+
const load = async () => {
|
|
630
|
+
if (resolver?.fetchJson) {
|
|
631
|
+
return resolver.fetchJson(resolvedUrl);
|
|
632
|
+
}
|
|
633
|
+
const response = await fetch(resolvedUrl);
|
|
634
|
+
if (!response.ok) {
|
|
635
|
+
throw new Error(`HTTP ${response.status}`);
|
|
636
|
+
}
|
|
637
|
+
return response.json();
|
|
638
|
+
};
|
|
639
|
+
|
|
640
|
+
if (!scene) {
|
|
641
|
+
return load();
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
const cache = sceneMap(sharedFetches, scene);
|
|
645
|
+
const key = `json|${resolvedUrl}`;
|
|
646
|
+
let pending = cache.get(key);
|
|
647
|
+
if (pending === undefined) {
|
|
648
|
+
pending = load().catch((err) => {
|
|
649
|
+
if (cache.get(key) === pending) {
|
|
650
|
+
cache.delete(key);
|
|
651
|
+
}
|
|
652
|
+
throw err;
|
|
653
|
+
});
|
|
654
|
+
cache.set(key, pending);
|
|
655
|
+
}
|
|
656
|
+
return pending;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
private static async inlineGeometryEntries(json: any, loadGeometry: (url: string) => Promise<any>): Promise<void> {
|
|
660
|
+
const geometries: any[] = Array.isArray(json?.geometries) ? json.geometries : [];
|
|
661
|
+
await Promise.all(
|
|
662
|
+
geometries
|
|
663
|
+
.filter((geom) => typeof geom?.url === 'string' && !geom.positions)
|
|
664
|
+
.map(async (geom) => {
|
|
665
|
+
try {
|
|
666
|
+
const data = await loadGeometry(geom.url);
|
|
667
|
+
// Copy arrays so a shared fetch-cache document is not aliased across
|
|
668
|
+
// effects — mutating one effect's inlined geometry must not corrupt others.
|
|
669
|
+
for (const key of ['positions', 'indices', 'uvs', 'normals']) {
|
|
670
|
+
const value = data?.[key];
|
|
671
|
+
if (value === undefined) {
|
|
672
|
+
continue;
|
|
673
|
+
}
|
|
674
|
+
geom[key] = Array.isArray(value) ? value.slice() : value;
|
|
675
|
+
}
|
|
676
|
+
delete geom.url;
|
|
677
|
+
} catch (err) {
|
|
678
|
+
console.warn(`[QuarksLoader] Could not load geometry "${geom.url}":`, err);
|
|
679
|
+
}
|
|
680
|
+
})
|
|
681
|
+
);
|
|
682
|
+
}
|
|
683
|
+
|
|
369
684
|
private parseImages(images: any[], baseUrl: string, _meta: LoadedMeta): void {
|
|
370
685
|
(this as any)._images = {};
|
|
371
686
|
for (const img of images) {
|
|
372
687
|
if (img.url) {
|
|
373
688
|
QuarksLoader.warnIfNotAnImageUrl(img.url);
|
|
374
689
|
}
|
|
375
|
-
|
|
690
|
+
// Urls are usually already absolute after resolveResources; joinUrl is a no-op for
|
|
691
|
+
// data/blob/http(s) and still supports callers that use parse() with a baseUrl only.
|
|
692
|
+
(this as any)._images[img.uuid] = img.url ? QuarksLoader.joinUrl(baseUrl, img.url) : null;
|
|
376
693
|
}
|
|
377
694
|
}
|
|
378
695
|
|
|
@@ -395,19 +712,6 @@ export class QuarksLoader {
|
|
|
395
712
|
);
|
|
396
713
|
}
|
|
397
714
|
|
|
398
|
-
/**
|
|
399
|
-
* `img.url` is already absolute for data/blob URIs and full http(s) URLs — common for
|
|
400
|
-
* "portable" exports that embed textures as base64 (see ensurePortableTextureUrl in
|
|
401
|
-
* babylon.quarks-editor). Prepending baseUrl to those turns e.g.
|
|
402
|
-
* "data:image/png;base64,AAAA" into an invalid path like "/some/dir/data:image/png;base64,AAAA".
|
|
403
|
-
*/
|
|
404
|
-
private static resolveUrl(baseUrl: string, url: string): string {
|
|
405
|
-
if (/^(data|blob):/i.test(url) || /^[a-z][a-z0-9+.-]*:\/\//i.test(url) || url.startsWith('/')) {
|
|
406
|
-
return url;
|
|
407
|
-
}
|
|
408
|
-
return baseUrl + url;
|
|
409
|
-
}
|
|
410
|
-
|
|
411
715
|
private parseTextures(textures: any[], meta: LoadedMeta): void {
|
|
412
716
|
const images = (this as any)._images || {};
|
|
413
717
|
for (const texDef of textures) {
|