@caperjs/plugin-3d 0.0.0-stage → 0.8.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/src/owned.ts ADDED
@@ -0,0 +1,42 @@
1
+ import type { Ticker } from 'pixi.js';
2
+
3
+ type Resource = { destroy(): void };
4
+ type Player = { destroyed: boolean; update(ticker: Ticker): void };
5
+
6
+ export const ownedByView = new WeakMap<object, Owned3D>();
7
+
8
+ export class Owned3D {
9
+ private readonly geometries = new Map<string, Resource>();
10
+ private readonly materials = new Set<Resource>();
11
+ private readonly players = new Set<Player>();
12
+
13
+ /** One geometry per kind per view: bounded, and meshes that share a geometry can batch. */
14
+ geometry<T extends Resource>(kind: string, build: () => T): T {
15
+ if (!this.geometries.has(kind)) this.geometries.set(kind, build());
16
+ return this.geometries.get(kind) as T;
17
+ }
18
+
19
+ material<T extends Resource>(material: T): T {
20
+ this.materials.add(material);
21
+ return material;
22
+ }
23
+
24
+ player<T extends Player>(player: T): T {
25
+ this.players.add(player);
26
+ return player;
27
+ }
28
+
29
+ update(ticker: Ticker): void {
30
+ for (const player of this.players) {
31
+ if (player.destroyed) this.players.delete(player);
32
+ else player.update(ticker);
33
+ }
34
+ }
35
+
36
+ destroy(): void {
37
+ for (const resource of [...this.geometries.values(), ...this.materials]) resource.destroy();
38
+ this.geometries.clear();
39
+ this.materials.clear();
40
+ this.players.clear();
41
+ }
42
+ }
package/src/props.ts ADDED
@@ -0,0 +1,96 @@
1
+ import type {
2
+ AmbientLightOptions,
3
+ Camera3DOptions,
4
+ Container3D,
5
+ Container3DOptions,
6
+ DirectionalLightOptions,
7
+ Geometry3D,
8
+ Material3DOptions,
9
+ Mesh3DOptions as PixiMesh3DOptions,
10
+ Model3DOptions as PixiModel3DOptions,
11
+ PointLightOptions,
12
+ Scene3DSource,
13
+ SpotLightOptions,
14
+ } from '@pixi/3d';
15
+ import type { BaseMaterial3D } from '@pixi/3d';
16
+ import type { AppTypeOverrides, AssetTypeOverrides } from '@caperjs/core';
17
+
18
+ export type Vec3 = [number, number, number] | { x: number; y: number; z: number };
19
+
20
+ export type Node3DProps = Omit<Container3DOptions, 'position' | 'rotation' | 'scale'> & {
21
+ position?: Vec3;
22
+ rotation?: Vec3;
23
+ scale?: Vec3 | number;
24
+ lookAt?: Vec3;
25
+ };
26
+
27
+ export type Material3DProps = Material3DOptions & { /** Shorthand for `baseColor`. */ color?: number };
28
+
29
+ type EntityClasses = AppTypeOverrides extends { EntityClasses: infer E } ? E : {};
30
+ type Entity3DCtor = abstract new (...args: any[]) => Container3D;
31
+ /** Ids of discovered entities whose class extends `Container3D` (usually through `Entity3D`). */
32
+ export type Entity3DId = {
33
+ [K in keyof EntityClasses]: EntityClasses[K] extends Entity3DCtor ? K : never;
34
+ }[keyof EntityClasses] & string;
35
+ type OwnProps<C extends Entity3DCtor> = Exclude<ConstructorParameters<C>[0], void | undefined>;
36
+ /** The entity's own constructor props; `{}` for a class that takes none (`Entity3D` with no `Props`). */
37
+ export type Entity3DProps<K extends Entity3DId> = EntityClasses[K] extends Entity3DCtor
38
+ ? [OwnProps<EntityClasses[K]>] extends [never]
39
+ ? {}
40
+ : OwnProps<EntityClasses[K]>
41
+ : never;
42
+ export type Entity3DInstance<K extends Entity3DId> = EntityClasses[K] extends Entity3DCtor
43
+ ? InstanceType<EntityClasses[K]>
44
+ : never;
45
+ /** Placement props `view.add.entity` applies to the new entity; everything else is the entity's own. */
46
+ export type Entity3DNodeProps = Pick<
47
+ Node3DProps,
48
+ 'position' | 'rotation' | 'scale' | 'x' | 'y' | 'z' | 'visible' | 'lookAt' | 'parent'
49
+ >;
50
+
51
+ export type Mesh3DProps = Omit<PixiMesh3DOptions, keyof Node3DProps | 'geometry' | 'material'> &
52
+ Node3DProps & {
53
+ geometry?: 'box' | 'plane' | 'sphere' | 'cylinder' | Geometry3D;
54
+ material?: BaseMaterial3D;
55
+ color?: number;
56
+ roughness?: number;
57
+ metallic?: number;
58
+ };
59
+
60
+ /**
61
+ * A model's manifest name. Once the app's generated `caper-assets.d.ts` lists models (any `.glb` in the assets
62
+ * folder), this is that union, so unknown names are type errors; until then it is `string`. A model loaded by
63
+ * hand is passed as its `Scene3DSource`.
64
+ */
65
+ // Known names autocomplete; any other string is still allowed, the same as texture and spritesheet names.
66
+ export type ModelAsset = AssetTypeOverrides extends { Model: infer Name extends string }
67
+ ? Name | (string & {})
68
+ : string;
69
+
70
+ export type Model3DProps = Omit<PixiModel3DOptions, keyof Node3DProps | 'source'> &
71
+ Node3DProps & { asset: ModelAsset | Scene3DSource };
72
+
73
+ export type Light3DProps = Omit<
74
+ DirectionalLightOptions & PointLightOptions & SpotLightOptions & AmbientLightOptions,
75
+ keyof Node3DProps
76
+ > &
77
+ Node3DProps & { type?: 'directional' | 'point' | 'spot' | 'ambient' };
78
+
79
+ export type Camera3DProps = Omit<Camera3DOptions, keyof Node3DProps> & Node3DProps;
80
+
81
+ export function toVec3(value: Vec3): { x: number; y: number; z: number } {
82
+ return Array.isArray(value) ? { x: value[0], y: value[1], z: value[2] } : value;
83
+ }
84
+
85
+ export function resolveNodeOptions<T extends Node3DProps>(
86
+ props: T,
87
+ ): Omit<T, 'lookAt' | 'position' | 'rotation' | 'scale'> & Pick<Container3DOptions, 'position' | 'rotation' | 'scale'> {
88
+ const options: Record<string, unknown> = { ...props };
89
+ delete options.lookAt;
90
+ for (const key of ['position', 'rotation', 'scale'] as const) {
91
+ const value = options[key];
92
+ if (Array.isArray(value)) options[key] = toVec3(value as Vec3);
93
+ }
94
+ return options as Omit<T, 'lookAt' | 'position' | 'rotation' | 'scale'> &
95
+ Pick<Container3DOptions, 'position' | 'rotation' | 'scale'>;
96
+ }
package/src/quality.ts ADDED
@@ -0,0 +1,12 @@
1
+ export type Quality3D = 'auto' | 'low' | 'high';
2
+
3
+ export function resolveQuality(
4
+ quality: Quality3D,
5
+ device: { isMobile: boolean; resolution: number },
6
+ ): { shadowQuality: 'low' | 'medium' | 'high'; resolution: number | 'inherit' } {
7
+ if (quality === 'high') return { shadowQuality: 'high', resolution: 'inherit' };
8
+ if (quality === 'low') return { shadowQuality: 'low', resolution: Math.min(device.resolution, 1) };
9
+ return device.isMobile
10
+ ? { shadowQuality: 'low', resolution: Math.min(device.resolution, 2) }
11
+ : { shadowQuality: 'medium', resolution: 'inherit' };
12
+ }
@@ -0,0 +1,22 @@
1
+ type Renderer3D = {
2
+ name?: string;
3
+ gl?: { texImage3D?: unknown };
4
+ context?: { gl?: { texImage3D?: unknown } };
5
+ lighting3d?: unknown;
6
+ };
7
+
8
+ export function assertRenderer3D(value: unknown): void {
9
+ const renderer = value as Renderer3D | null | undefined;
10
+ if (!renderer)
11
+ throw new Error('[@caperjs/plugin-3d] No renderer is available. Create the renderer before a 3D view.');
12
+ if (renderer.name === 'canvas')
13
+ throw new Error('[@caperjs/plugin-3d] Canvas renderer cannot draw 3D. Use WebGL2 or WebGPU.');
14
+ if (renderer.name === 'webgl' && typeof (renderer.gl ?? renderer.context?.gl)?.texImage3D !== 'function') {
15
+ throw new Error('[@caperjs/plugin-3d] WebGL2 is required; this renderer is using WebGL1.');
16
+ }
17
+ if (!renderer.lighting3d) {
18
+ throw new Error(
19
+ "[@caperjs/plugin-3d] @pixi/3d was loaded after the renderer was created. Add '3d' to plugins in caper.config.ts and do not mark it autoLoad: false.",
20
+ );
21
+ }
22
+ }
package/src/version.ts ADDED
@@ -0,0 +1 @@
1
+ export const version = '0.8.0';