@pixifact/three 0.0.0-stage → 0.20.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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 youxia
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 CHANGED
@@ -1,3 +1,48 @@
1
- # Temporary Holding Version
1
+ # @pixifact/three
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Pixifact 可选 Three.js 插件。Three 绘制 3D 世界,Pixifact / PixiJS 绘制 2D UI,共用一个 WebGL2 Canvas 和 Application ticker。
4
+
5
+ ```bash
6
+ bun add @pixifact/three three@0.186.1
7
+ bun add -d @types/three@0.186.0
8
+ ```
9
+
10
+ 在现有 Vite 配置中注册:
11
+
12
+ ```ts
13
+ import { pixifact } from 'pixifact/compiler-node';
14
+ import { three } from '@pixifact/three/vite';
15
+
16
+ export default {
17
+ plugins: [pixifact({ codePackages: { game: 'src/game.ts' } }), three()],
18
+ };
19
+ ```
20
+
21
+ 在 `src/game.ts` 中创建 3D 层:
22
+
23
+ ```ts
24
+ import { createThreeLayer } from '@pixifact/three';
25
+ import { PerspectiveCamera, Scene } from 'three';
26
+ import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
27
+ import type { Application } from 'pixi.js';
28
+
29
+ export function startGame(app: Application) {
30
+ const scene = new Scene();
31
+ const camera = new PerspectiveCamera(45, 1, 0.1, 100);
32
+ camera.position.z = 5;
33
+ const layer = createThreeLayer(app, { scene, camera });
34
+ const controls = new OrbitControls(camera, layer.input);
35
+ return { layer, controls };
36
+ }
37
+ ```
38
+
39
+ 主入口使用 `pixifact:platform` 的 `createApplication({ preference: 'webgl' })`,再调用 `import('./game')`。沿用 `pixifact build --mode web/wechat/douyin`,引擎随游戏代码进入分包。
40
+
41
+ - 原生 Three Scene、Camera、Mesh 与 `.scene` UI 可以组合使用。
42
+ - UI 优先接收输入;`layer.input` 提供世界手势,`layer.raycast(event, objects)` 用于拾取。
43
+ - 普通 glTF/GLB、外部纹理和内嵌图片支持小游戏资源通道及资源分包。资源 URL 可从 `Assets.resolver.resolve(alias).src` 获取。
44
+ - Web 开发 Runtime 注册时传入 `captureScreenshot: layer.captureScreenshot`,截图包含 3D 和 UI。
45
+ - `layer.destroy()` 释放插件 renderer 和输入,恢复 Pixi 绘制;共享 Canvas/context 保留。控件、模型 geometry/material/texture 按 Three 原生规则 dispose。
46
+ - 需要 WebGL2;首版小游戏 loader 适配 Three r186。WASM、Draco/KTX2、3D 可视化编辑不在此版本范围内。
47
+
48
+ [完整接入文档](https://github.com/hxg2050/pixifact/blob/main/docs/zh/three.md) · [三端示例](https://github.com/hxg2050/pixifact/tree/main/sample-projects/three-demo)
@@ -0,0 +1,2 @@
1
+ export { createThreeLayer } from './layer.js';
2
+ export type { ThreeLayer, ThreeLayerOptions, ThreeScreenshot } from './layer.js';
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ export { createThreeLayer } from './layer.js';
@@ -0,0 +1,6 @@
1
+ import { type Application } from 'pixi.js';
2
+ /** 只供 Three 控件使用的局部事件面,不修改宿主 Canvas / document。 */
3
+ export declare function createThreeInput(app: Application): {
4
+ element: HTMLElement;
5
+ destroy(): void;
6
+ };
package/dist/input.js ADDED
@@ -0,0 +1,110 @@
1
+ import { Container } from 'pixi.js';
2
+ /** 只供 Three 控件使用的局部事件面,不修改宿主 Canvas / document。 */
3
+ export function createThreeInput(app) {
4
+ const surface = new Container({ label: 'Three input', eventMode: 'static' });
5
+ // Pointer down 只来自 Canvas;无限命中面让 UI stage 的视口变换不影响世界输入。
6
+ surface.hitArea = { contains: () => true };
7
+ app.stage.addChildAt(surface, 0);
8
+ const listeners = new Map();
9
+ const active = new Map();
10
+ const captured = new Set();
11
+ const element = {
12
+ style: app.canvas.style,
13
+ get clientWidth() { return app.canvas.getBoundingClientRect().width; },
14
+ get clientHeight() { return app.canvas.getBoundingClientRect().height; },
15
+ get ownerDocument() { return element; },
16
+ getRootNode: () => element,
17
+ getBoundingClientRect: () => app.canvas.getBoundingClientRect(),
18
+ setPointerCapture: (id) => { captured.add(id); },
19
+ hasPointerCapture: (id) => captured.has(id),
20
+ releasePointerCapture: (id) => { captured.delete(id); },
21
+ addEventListener(type, listener, options) {
22
+ const current = listeners.get(type) ?? new Map();
23
+ current.set(listener, { once: typeof options === 'object' && options.once === true });
24
+ listeners.set(type, current);
25
+ },
26
+ removeEventListener(type, listener) {
27
+ listeners.get(type)?.delete(listener);
28
+ },
29
+ };
30
+ const emit = (event) => {
31
+ for (const [listener, options] of [...(listeners.get(event.type) ?? [])]) {
32
+ if (options.once)
33
+ listeners.get(event.type).delete(listener);
34
+ if (typeof listener === 'function')
35
+ listener.call(element, event);
36
+ else
37
+ listener.handleEvent(event);
38
+ if (event.cancelBubble)
39
+ break;
40
+ }
41
+ };
42
+ const dispatch = (type, source) => {
43
+ const pointer = source;
44
+ const wheel = source;
45
+ const nativeEvent = source.nativeEvent;
46
+ let prevented = false;
47
+ const event = {
48
+ type, target: element, currentTarget: element,
49
+ clientX: source.clientX, clientY: source.clientY,
50
+ pageX: source.pageX, pageY: source.pageY,
51
+ pointerId: pointer.pointerId, pointerType: pointer.pointerType, isPrimary: pointer.isPrimary,
52
+ button: source.button, buttons: source.buttons,
53
+ altKey: source.altKey, ctrlKey: source.ctrlKey, metaKey: source.metaKey, shiftKey: source.shiftKey,
54
+ deltaX: wheel.deltaX, deltaY: wheel.deltaY, deltaZ: wheel.deltaZ, deltaMode: wheel.deltaMode,
55
+ get defaultPrevented() { return prevented; },
56
+ preventDefault() {
57
+ prevented = true;
58
+ if (nativeEvent && 'preventDefault' in nativeEvent)
59
+ nativeEvent.preventDefault();
60
+ },
61
+ stopPropagation() { },
62
+ cancelBubble: false,
63
+ stopImmediatePropagation() { event.cancelBubble = true; },
64
+ };
65
+ emit(event);
66
+ return event;
67
+ };
68
+ const down = (event) => {
69
+ if (event.target !== surface)
70
+ return;
71
+ active.set(event.pointerId, dispatch('pointerdown', event));
72
+ };
73
+ const move = (event) => {
74
+ if (active.has(event.pointerId))
75
+ active.set(event.pointerId, dispatch('pointermove', event));
76
+ else if (event.target === surface)
77
+ dispatch('pointermove', event);
78
+ };
79
+ const up = (event) => {
80
+ if (!active.delete(event.pointerId))
81
+ return;
82
+ dispatch('pointerup', event);
83
+ captured.delete(event.pointerId);
84
+ };
85
+ const wheel = (event) => {
86
+ if (event.target === surface)
87
+ dispatch('wheel', event);
88
+ };
89
+ const boundary = app.renderer.events.rootBoundary.dispatch;
90
+ boundary.on('pointerdown', down);
91
+ boundary.on('pointermove', move);
92
+ boundary.on('pointerup', up);
93
+ boundary.on('wheel', wheel);
94
+ surface.on('pointerupoutside', up);
95
+ return {
96
+ element: element,
97
+ destroy() {
98
+ for (const event of active.values())
99
+ emit({ ...event, type: 'pointercancel' });
100
+ active.clear();
101
+ captured.clear();
102
+ boundary.off('pointerdown', down);
103
+ boundary.off('pointermove', move);
104
+ boundary.off('pointerup', up);
105
+ boundary.off('wheel', wheel);
106
+ listeners.clear();
107
+ surface.destroy();
108
+ },
109
+ };
110
+ }
@@ -0,0 +1,33 @@
1
+ import { type Application } from 'pixi.js';
2
+ import { Raycaster, WebGLRenderer, type Camera, type Object3D, type Scene, type WebGLRendererParameters } from 'three';
3
+ export interface ThreeLayerOptions {
4
+ scene: Scene;
5
+ camera: Camera;
6
+ rendererOptions?: Omit<WebGLRendererParameters, 'canvas' | 'context'>;
7
+ /** 用于多 pass、后处理等原生 Three 绘制。默认调用 renderer.render(scene, camera)。 */
8
+ renderScene?: (renderer: WebGLRenderer, scene: Scene, camera: Camera) => void;
9
+ /** 自定义投影或布局;透视相机的 aspect 由插件自动更新。 */
10
+ onResize?: (width: number, height: number) => void;
11
+ }
12
+ export interface ThreeScreenshot {
13
+ width: number;
14
+ height: number;
15
+ dataUrl: string;
16
+ }
17
+ export interface ThreeLayer {
18
+ readonly renderer: WebGLRenderer;
19
+ readonly scene: Scene;
20
+ readonly camera: Camera;
21
+ /** 交给 OrbitControls 等控件的输入元素,只接收未被 UI 占用的手势。 */
22
+ readonly input: HTMLElement;
23
+ raycast(event: {
24
+ clientX: number;
25
+ clientY: number;
26
+ }, objects: Object3D[], recursive?: boolean): ReturnType<Raycaster['intersectObjects']>;
27
+ render(): void;
28
+ /** Web Runtime 的完整合成截图;与 registerPixiRuntime 的 captureScreenshot 选项配合使用。 */
29
+ captureScreenshot(): ThreeScreenshot;
30
+ /** 只释放插件拥有的 renderer/输入;模型、材质、纹理由游戏释放。 */
31
+ destroy(): void;
32
+ }
33
+ export declare function createThreeLayer(app: Application, options: ThreeLayerOptions): ThreeLayer;
package/dist/layer.js ADDED
@@ -0,0 +1,83 @@
1
+ import { DOMAdapter, RendererType, UPDATE_PRIORITY } from 'pixi.js';
2
+ import { Raycaster, Vector2, WebGLRenderer } from 'three';
3
+ import { createThreeInput } from './input.js';
4
+ export function createThreeLayer(app, options) {
5
+ const pixi = app.renderer;
6
+ if (pixi.type !== RendererType.WEBGL || pixi.context.webGLVersion !== 2) {
7
+ throw new Error('@pixifact/three 需要 WebGL2 Application;创建时设置 preference: "webgl"。');
8
+ }
9
+ const { scene, camera } = options;
10
+ // Pixi 初始化纹理后可能保留 premultiply unpack 状态;Three 构造时就会上传 3D 占位纹理。
11
+ pixi.resetState();
12
+ const renderer = new WebGLRenderer({ ...options.rendererOptions, canvas: app.canvas, context: pixi.gl });
13
+ const input = createThreeInput(app);
14
+ const raycaster = new Raycaster();
15
+ const pointer = new Vector2();
16
+ const originalRender = app.render;
17
+ const originalDestroy = app.destroy;
18
+ let width = -1;
19
+ let height = -1;
20
+ let resolution = -1;
21
+ let destroyed = false;
22
+ const layer = {
23
+ renderer, scene, camera, input: input.element,
24
+ raycast(event, objects, recursive = true) {
25
+ const rect = app.canvas.getBoundingClientRect();
26
+ pointer.set(((event.clientX - rect.left) / rect.width) * 2 - 1, 1 - ((event.clientY - rect.top) / rect.height) * 2);
27
+ scene.updateMatrixWorld();
28
+ camera.updateMatrixWorld();
29
+ raycaster.setFromCamera(pointer, camera);
30
+ return raycaster.intersectObjects(objects, recursive);
31
+ },
32
+ render() {
33
+ if (width !== app.screen.width || height !== app.screen.height || resolution !== pixi.resolution) {
34
+ width = app.screen.width;
35
+ height = app.screen.height;
36
+ resolution = pixi.resolution;
37
+ renderer.setDrawingBufferSize(width, height, resolution);
38
+ if (camera.isPerspectiveCamera) {
39
+ camera.aspect = width / height;
40
+ camera.updateProjectionMatrix();
41
+ }
42
+ options.onResize?.(width, height);
43
+ }
44
+ renderer.resetState();
45
+ renderer.setRenderTarget(null);
46
+ if (options.renderScene)
47
+ options.renderScene(renderer, scene, camera);
48
+ else
49
+ renderer.render(scene, camera);
50
+ pixi.resetState();
51
+ pixi.render({ container: app.stage, clear: false });
52
+ },
53
+ captureScreenshot() {
54
+ layer.render();
55
+ const captureWidth = Math.max(1, Math.floor(app.screen.width));
56
+ const captureHeight = Math.max(1, Math.floor(app.screen.height));
57
+ const canvas = DOMAdapter.get().createCanvas(captureWidth, captureHeight);
58
+ const context = canvas.getContext('2d');
59
+ context.drawImage(app.canvas, 0, 0, captureWidth, captureHeight);
60
+ return { width: captureWidth, height: captureHeight, dataUrl: canvas.toDataURL('image/png') };
61
+ },
62
+ destroy() {
63
+ if (destroyed)
64
+ return;
65
+ destroyed = true;
66
+ app.ticker.remove(layer.render, app);
67
+ app.render = originalRender;
68
+ app.destroy = originalDestroy;
69
+ app.ticker.add(originalRender, app, UPDATE_PRIORITY.LOW);
70
+ input.destroy();
71
+ renderer.dispose();
72
+ pixi.resetState();
73
+ },
74
+ };
75
+ app.ticker.remove(originalRender, app);
76
+ app.render = layer.render;
77
+ app.ticker.add(layer.render, app, UPDATE_PRIORITY.LOW);
78
+ app.destroy = (...args) => {
79
+ layer.destroy();
80
+ originalDestroy.apply(app, args);
81
+ };
82
+ return layer;
83
+ }
@@ -0,0 +1,17 @@
1
+ import { Loader } from 'three/src/loaders/Loader.js';
2
+ type Progress = (event: ProgressEvent) => void;
3
+ type Failure = (error: unknown) => void;
4
+ /** Three 原生 Loader 协议,传输复用 Pixifact;不依赖浏览器 Request/Response/stream。 */
5
+ export declare class FileLoader extends Loader {
6
+ responseType: string;
7
+ mimeType: string;
8
+ private generation;
9
+ load(url: string, onLoad: (data: unknown) => void, onProgress?: Progress, onError?: Failure): void;
10
+ setResponseType(value: string): this;
11
+ setMimeType(value: string): this;
12
+ abort(): this;
13
+ }
14
+ export declare class ImageLoader extends Loader<HTMLImageElement> {
15
+ load(url: string, onLoad: (image: HTMLImageElement) => void, _onProgress?: Progress, onError?: Failure): HTMLImageElement;
16
+ }
17
+ export {};
@@ -0,0 +1,92 @@
1
+ import { DOMAdapter } from 'pixi.js';
2
+ import { Cache } from 'three/src/loaders/Cache.js';
3
+ import { Loader } from 'three/src/loaders/Loader.js';
4
+ import { resources } from 'pixifact:three-platform';
5
+ import { NativeBlob, NativeTextDecoder } from './nativeResources.js';
6
+ /** Three 原生 Loader 协议,传输复用 Pixifact;不依赖浏览器 Request/Response/stream。 */
7
+ export class FileLoader extends Loader {
8
+ responseType = '';
9
+ mimeType = '';
10
+ generation = 0;
11
+ load(url, onLoad, onProgress, onError) {
12
+ url = this.manager.resolveURL(this.path + url);
13
+ const generation = this.generation;
14
+ this.manager.itemStart(url);
15
+ void (async () => {
16
+ try {
17
+ let data = Cache.get(`file:${url}`);
18
+ if (data === undefined) {
19
+ const buffer = await resources.read(url);
20
+ if (generation !== this.generation)
21
+ throw new Error(`Three 资源加载已取消: ${url}`);
22
+ onProgress?.({ type: 'progress', lengthComputable: true, loaded: buffer.byteLength, total: buffer.byteLength });
23
+ if (this.responseType === 'arraybuffer')
24
+ data = buffer;
25
+ else if (this.responseType === 'blob')
26
+ data = new NativeBlob([buffer], { type: this.mimeType });
27
+ else {
28
+ const text = new NativeTextDecoder().decode(buffer);
29
+ if (this.responseType === 'json')
30
+ data = JSON.parse(text);
31
+ else if (this.responseType === 'document')
32
+ data = new DOMParser().parseFromString(text, this.mimeType);
33
+ else
34
+ data = text;
35
+ }
36
+ Cache.add(`file:${url}`, data);
37
+ }
38
+ onLoad?.(data);
39
+ }
40
+ catch (error) {
41
+ if (onError)
42
+ onError(error);
43
+ else
44
+ console.error(error);
45
+ this.manager.itemError(url);
46
+ }
47
+ finally {
48
+ this.manager.itemEnd(url);
49
+ }
50
+ })();
51
+ }
52
+ setResponseType(value) { this.responseType = value; return this; }
53
+ setMimeType(value) { this.mimeType = value; return this; }
54
+ abort() { this.generation++; return this; }
55
+ }
56
+ export class ImageLoader extends Loader {
57
+ load(url, onLoad, _onProgress, onError) {
58
+ url = this.manager.resolveURL(this.path + url);
59
+ const image = DOMAdapter.get().createImage();
60
+ image.crossOrigin = this.crossOrigin;
61
+ this.manager.itemStart(url);
62
+ void (async () => {
63
+ try {
64
+ const prepared = await resources.prepareImage(url);
65
+ try {
66
+ await new Promise((resolve, reject) => {
67
+ image.onload = () => resolve();
68
+ image.onerror = reject;
69
+ image.src = prepared.src;
70
+ });
71
+ }
72
+ finally {
73
+ await prepared.release?.();
74
+ }
75
+ onLoad?.(image);
76
+ }
77
+ catch (error) {
78
+ if (onError)
79
+ onError(error);
80
+ else
81
+ console.error(error);
82
+ this.manager.itemError(url);
83
+ }
84
+ finally {
85
+ image.onload = null;
86
+ image.onerror = null;
87
+ this.manager.itemEnd(url);
88
+ }
89
+ })();
90
+ return image;
91
+ }
92
+ }
@@ -0,0 +1,50 @@
1
+ import { type MiniGameApi } from 'pixifact/internal/minigame';
2
+ export interface ThreeMiniGameApi extends MiniGameApi {
3
+ env: {
4
+ USER_DATA_PATH: string;
5
+ };
6
+ base64ToArrayBuffer(data: string): ArrayBuffer;
7
+ getFileSystemManager(): ReturnType<MiniGameApi['getFileSystemManager']> & {
8
+ writeFile(options: {
9
+ filePath: string;
10
+ data: ArrayBuffer;
11
+ success(): void;
12
+ fail(error: {
13
+ errMsg: string;
14
+ }): void;
15
+ }): void;
16
+ unlink(options: {
17
+ filePath: string;
18
+ success(): void;
19
+ fail(error: {
20
+ errMsg: string;
21
+ }): void;
22
+ }): void;
23
+ };
24
+ }
25
+ /** glTF 只需要 UTF-8 解码,复用小游戏资源层实现,不修改全局 TextDecoder。 */
26
+ export declare class NativeTextDecoder {
27
+ decode(input: ArrayBuffer | ArrayBufferView): string;
28
+ }
29
+ export declare class NativeBlob {
30
+ readonly type: string;
31
+ readonly bytes: Uint8Array;
32
+ constructor(parts: ArrayBuffer[], options?: {
33
+ type?: string;
34
+ });
35
+ arrayBuffer(): Promise<ArrayBuffer>;
36
+ }
37
+ export declare function createNativeThreeResources(getApi: () => ThreeMiniGameApi, platform: string): {
38
+ Blob: typeof NativeBlob;
39
+ TextDecoder: typeof NativeTextDecoder;
40
+ URL: {
41
+ createObjectURL(blob: NativeBlob): string;
42
+ revokeObjectURL(url: string): void;
43
+ };
44
+ read(url: string): Promise<ArrayBuffer>;
45
+ prepareImage(url: string): Promise<{
46
+ src: string;
47
+ release?: () => Promise<void>;
48
+ }>;
49
+ };
50
+ export type NativeThreeResources = ReturnType<typeof createNativeThreeResources>;
@@ -0,0 +1,86 @@
1
+ import { DOMAdapter } from 'pixi.js';
2
+ import { createMiniGameSubpackageLoader, decodeUtf8, encodeUtf8, miniGameResourceSubpackage } from 'pixifact/internal/minigame';
3
+ /** glTF 只需要 UTF-8 解码,复用小游戏资源层实现,不修改全局 TextDecoder。 */
4
+ export class NativeTextDecoder {
5
+ decode(input) {
6
+ const buffer = ArrayBuffer.isView(input)
7
+ ? input.buffer.slice(input.byteOffset, input.byteOffset + input.byteLength)
8
+ : input;
9
+ return decodeUtf8(buffer).replace(/^\uFEFF/, '');
10
+ }
11
+ }
12
+ export class NativeBlob {
13
+ type;
14
+ bytes;
15
+ constructor(parts, options = {}) {
16
+ this.type = options.type ?? '';
17
+ this.bytes = new Uint8Array(parts.reduce((size, part) => size + part.byteLength, 0));
18
+ let offset = 0;
19
+ for (const part of parts) {
20
+ this.bytes.set(new Uint8Array(part), offset);
21
+ offset += part.byteLength;
22
+ }
23
+ }
24
+ async arrayBuffer() { return this.bytes.slice().buffer; }
25
+ }
26
+ let nextResourceId = 0;
27
+ export function createNativeThreeResources(getApi, platform) {
28
+ const blobs = new Map();
29
+ const loadPackage = createMiniGameSubpackageLoader(getApi, platform);
30
+ const decodeData = (url) => {
31
+ const comma = url.indexOf(',');
32
+ const header = url.slice(5, comma);
33
+ const payload = url.slice(comma + 1);
34
+ return {
35
+ type: header.split(';')[0],
36
+ data: header.endsWith(';base64') ? getApi().base64ToArrayBuffer(payload) : encodeUtf8(decodeURIComponent(payload)),
37
+ };
38
+ };
39
+ return {
40
+ Blob: NativeBlob,
41
+ TextDecoder: NativeTextDecoder,
42
+ URL: {
43
+ createObjectURL(blob) {
44
+ const url = `blob:pixifact-three/${++nextResourceId}`;
45
+ blobs.set(url, blob);
46
+ return url;
47
+ },
48
+ revokeObjectURL(url) { blobs.delete(url); },
49
+ },
50
+ async read(url) {
51
+ if (url.startsWith('data:'))
52
+ return decodeData(url).data;
53
+ if (url.startsWith('blob:pixifact-three/'))
54
+ return blobs.get(url).arrayBuffer();
55
+ const response = await DOMAdapter.get().fetch(url);
56
+ if (!response.ok)
57
+ throw new Error(`Three 资源加载失败 (${response.status}): ${url}`);
58
+ return response.arrayBuffer();
59
+ },
60
+ async prepareImage(url) {
61
+ const pack = miniGameResourceSubpackage(url);
62
+ if (pack)
63
+ await loadPackage(pack);
64
+ if (!url.startsWith('data:') && !url.startsWith('blob:pixifact-three/'))
65
+ return { src: url };
66
+ const embedded = url.startsWith('data:')
67
+ ? decodeData(url)
68
+ : { type: blobs.get(url).type, data: await blobs.get(url).arrayBuffer() };
69
+ const extension = embedded.type.split('/')[1].replace('jpeg', 'jpg');
70
+ const api = getApi();
71
+ const fs = api.getFileSystemManager();
72
+ const src = `${api.env.USER_DATA_PATH}/pixifact-three-${Date.now()}-${++nextResourceId}.${extension}`;
73
+ await new Promise((resolve, reject) => fs.writeFile({
74
+ filePath: src, data: embedded.data, success: resolve,
75
+ fail: (error) => reject(new Error(error.errMsg)),
76
+ }));
77
+ return {
78
+ src,
79
+ release: () => new Promise((resolve, reject) => fs.unlink({
80
+ filePath: src, success: resolve,
81
+ fail: (error) => reject(new Error(error.errMsg)),
82
+ })),
83
+ };
84
+ },
85
+ };
86
+ }
package/dist/vite.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ import type { Plugin } from 'vite';
2
+ /** 与 pixifact() 一起注册,小游戏端局部适配 Three r186 资源加载。 */
3
+ export declare function three(): Plugin;
package/dist/vite.js ADDED
@@ -0,0 +1,49 @@
1
+ import { loadPixifactEnv } from 'pixifact/compiler-node';
2
+ const platformModule = 'pixifact:three-platform';
3
+ const virtualPlatform = '\0' + platformModule;
4
+ /** 与 pixifact() 一起注册,小游戏端局部适配 Three r186 资源加载。 */
5
+ export function three() {
6
+ let platform;
7
+ return {
8
+ name: 'pixifact-three',
9
+ enforce: 'pre',
10
+ config() { return { resolve: { dedupe: ['pixi.js', 'three', '@pixifact/three'] } }; },
11
+ configResolved(config) { platform = loadPixifactEnv(config.root, config.mode).platform; },
12
+ async resolveId(id, importer) {
13
+ if (platform === 'web')
14
+ return;
15
+ if (id === platformModule)
16
+ return virtualPlatform;
17
+ // Three 的 build 入口将 loader 内联;使用同一份源码模块,保留可替换的 loader 边界。
18
+ if (id === 'three')
19
+ return this.resolve('three/src/Three.js', importer, { skipSelf: true });
20
+ return undefined;
21
+ },
22
+ load(id) {
23
+ if (id !== virtualPlatform)
24
+ return;
25
+ return `import { createNativeThreeResources } from '@pixifact/three/internal/resources';
26
+ export const resources = createNativeThreeResources(() => ${platform === 'wechat' ? 'wx' : 'tt'}, ${JSON.stringify(platform)});`;
27
+ },
28
+ transform(code, id) {
29
+ if (platform === 'web')
30
+ return;
31
+ const source = id.replaceAll('\\', '/').split('?')[0];
32
+ const loader = source.match(/\/three\/src\/loaders\/(FileLoader|ImageLoader)\.js$/)?.[1];
33
+ if (loader)
34
+ return { code: `export { ${loader} } from '@pixifact/three/internal/loaders';`, map: null };
35
+ if (source.endsWith('/three/examples/jsm/loaders/GLTFLoader.js')) {
36
+ return {
37
+ code: `import { resources as __pixifactResources } from '${platformModule}';
38
+ const Blob = __pixifactResources.Blob;
39
+ const TextDecoder = __pixifactResources.TextDecoder;
40
+ const self = { URL: __pixifactResources.URL };
41
+ const createImageBitmap = undefined;
42
+ ${code}`,
43
+ map: null,
44
+ };
45
+ }
46
+ return undefined;
47
+ },
48
+ };
49
+ }
package/package.json CHANGED
@@ -1,6 +1,53 @@
1
1
  {
2
2
  "name": "@pixifact/three",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.20.0",
4
+ "description": "Pixifact 可选 Three.js 3D 世界与 2D UI 合成插件。",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "files": [
10
+ "dist/",
11
+ "README.md"
12
+ ],
13
+ "sideEffects": false,
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "https://github.com/hxg2050/pixifact",
17
+ "directory": "packages/three"
18
+ },
19
+ "exports": {
20
+ ".": {
21
+ "types": "./dist/index.d.ts",
22
+ "import": "./dist/index.js",
23
+ "require": "./dist/index.js"
24
+ },
25
+ "./vite": {
26
+ "types": "./dist/vite.d.ts",
27
+ "import": "./dist/vite.js",
28
+ "require": "./dist/vite.js"
29
+ },
30
+ "./internal/loaders": {
31
+ "types": "./dist/nativeLoaders.d.ts",
32
+ "import": "./dist/nativeLoaders.js"
33
+ },
34
+ "./internal/resources": {
35
+ "types": "./dist/nativeResources.d.ts",
36
+ "import": "./dist/nativeResources.js"
37
+ }
38
+ },
39
+ "scripts": {
40
+ "build": "bun ../pixifact/scripts/clean-dist.mjs dist && bunx --no-install tsc -p tsconfig.build.json && bun ../pixifact/scripts/fix-dist-esm-imports.mjs dist"
41
+ },
42
+ "peerDependencies": {
43
+ "pixifact": ">=0.20.0 <1",
44
+ "pixi.js": ">=8.18.1 <9",
45
+ "three": ">=0.186.1 <0.187.0",
46
+ "vite": ">=8.0.0"
47
+ },
48
+ "publishConfig": {
49
+ "access": "public",
50
+ "registry": "https://registry.npmjs.org/"
51
+ },
52
+ "license": "MIT"
53
+ }