threlte-mcp 1.4.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.
@@ -0,0 +1,59 @@
1
+ <!--
2
+ MCPBridge Svelte Component
3
+
4
+ Drop-in component that initializes the MCP bridge for your Threlte app.
5
+ Auto-connects in development mode.
6
+
7
+ @example
8
+ ```svelte
9
+ <script>
10
+ import { MCPBridge } from 'threlte-mcp/client';
11
+ </script>
12
+
13
+ <MCPBridge />
14
+ ```
15
+ -->
16
+ <script lang="ts">
17
+ import { useThrelte } from "@threlte/core";
18
+ import { onMount, onDestroy } from "svelte";
19
+ import {
20
+ MCPBridge as Bridge,
21
+ getMCPBridge,
22
+ type MCPBridgeOptions,
23
+ } from "./MCPBridge.js";
24
+
25
+ interface Props {
26
+ /** Custom WebSocket URL (default: ws://127.0.0.1:8082) */
27
+ url?: string;
28
+ /** Force enable/disable (default: auto in dev mode) */
29
+ enabled?: boolean;
30
+ /** Reconnect delay in ms (default: 60000) */
31
+ reconnectDelay?: number;
32
+ }
33
+
34
+ let { url, enabled, reconnectDelay }: Props = $props();
35
+
36
+ const { scene } = useThrelte();
37
+ let bridge: Bridge | null = null;
38
+
39
+ onMount(() => {
40
+ if (scene) {
41
+ const options: MCPBridgeOptions = {
42
+ url,
43
+ reconnectDelay,
44
+ autoConnect: enabled,
45
+ };
46
+
47
+ bridge = getMCPBridge(scene) || new Bridge(scene, options);
48
+ console.log('[MCPBridge] Component initialized');
49
+ }
50
+ });
51
+
52
+ onDestroy(() => {
53
+ bridge?.disconnect();
54
+ console.log('[MCPBridge] Component destroyed');
55
+ });
56
+ </script>
57
+
58
+ <!-- No visible UI - this is a logic-only component -->
59
+
@@ -0,0 +1,18 @@
1
+ /**
2
+ * threlte-mcp Client Package
3
+ *
4
+ * Client-side bridge for connecting Threlte/Three.js apps to the MCP server.
5
+ *
6
+ * @example
7
+ * ```typescript
8
+ * // Option 1: Import the class directly
9
+ * import { MCPBridge } from 'threlte-mcp/client';
10
+ * const bridge = new MCPBridge(scene);
11
+ *
12
+ * // Option 2: Use the Svelte component
13
+ * import MCPBridgeComponent from 'threlte-mcp/client/MCPBridge.svelte';
14
+ * ```
15
+ */
16
+ export { MCPBridge, getMCPBridge, type MCPBridgeOptions } from './MCPBridge.js';
17
+ export { default as MCPBridgeComponent } from './MCPBridge.svelte';
18
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../client/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,KAAK,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAChF,OAAO,EAAE,OAAO,IAAI,kBAAkB,EAAE,MAAM,oBAAoB,CAAC"}
@@ -0,0 +1,18 @@
1
+ /**
2
+ * threlte-mcp Client Package
3
+ *
4
+ * Client-side bridge for connecting Threlte/Three.js apps to the MCP server.
5
+ *
6
+ * @example
7
+ * ```typescript
8
+ * // Option 1: Import the class directly
9
+ * import { MCPBridge } from 'threlte-mcp/client';
10
+ * const bridge = new MCPBridge(scene);
11
+ *
12
+ * // Option 2: Use the Svelte component
13
+ * import MCPBridgeComponent from 'threlte-mcp/client/MCPBridge.svelte';
14
+ * ```
15
+ */
16
+ export { MCPBridge, getMCPBridge } from './MCPBridge.js';
17
+ export { default as MCPBridgeComponent } from './MCPBridge.svelte';
18
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../client/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,SAAS,EAAE,YAAY,EAAyB,MAAM,gBAAgB,CAAC;AAChF,OAAO,EAAE,OAAO,IAAI,kBAAkB,EAAE,MAAM,oBAAoB,CAAC"}
@@ -0,0 +1,11 @@
1
+ import { type Document } from '@gltf-transform/core';
2
+ export interface GltfDocumentLoadResult {
3
+ document: Document;
4
+ resolvedPath: string;
5
+ bytes: number;
6
+ }
7
+ export declare function resolveAssetPath(input: string): string;
8
+ export declare function resolveOutputPath(inputPath: string, outputPath?: string, suffix?: string): string;
9
+ export declare function loadGltfDocument(assetPath: string): Promise<GltfDocumentLoadResult>;
10
+ export declare function writeGltfDocument(document: Document, outputPath: string): Promise<number>;
11
+ //# sourceMappingURL=gltf-io.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gltf-io.d.ts","sourceRoot":"","sources":["../src/gltf-io.ts"],"names":[],"mappings":"AAGA,OAAO,EAAU,KAAK,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAK7D,MAAM,WAAW,sBAAsB;IACnC,QAAQ,EAAE,QAAQ,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;CACjB;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAUtD;AAED,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,EAAE,MAAM,SAAe,GAAG,MAAM,CAevG;AAED,wBAAsB,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,sBAAsB,CAAC,CA8BzF;AAED,wBAAsB,iBAAiB,CAAC,QAAQ,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAK/F"}
@@ -0,0 +1,64 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ import { fileURLToPath } from 'url';
4
+ import { NodeIO } from '@gltf-transform/core';
5
+ const io = new NodeIO();
6
+ const SUPPORTED_EXTENSIONS = new Set(['.gltf', '.glb']);
7
+ export function resolveAssetPath(input) {
8
+ if (/^https?:\/\//i.test(input)) {
9
+ throw new Error('Remote URLs are not supported. Provide a local file path or file:// URL.');
10
+ }
11
+ if (input.startsWith('file://')) {
12
+ return fileURLToPath(input);
13
+ }
14
+ return path.normalize(input);
15
+ }
16
+ export function resolveOutputPath(inputPath, outputPath, suffix = '-optimized') {
17
+ if (outputPath) {
18
+ const resolvedOutput = resolveAssetPath(outputPath);
19
+ const extension = path.extname(resolvedOutput);
20
+ if (extension) {
21
+ return resolvedOutput;
22
+ }
23
+ const inputExtension = path.extname(inputPath) || '.glb';
24
+ return `${resolvedOutput}${inputExtension}`;
25
+ }
26
+ const extension = path.extname(inputPath);
27
+ const baseName = path.basename(inputPath, extension);
28
+ const dirName = path.dirname(inputPath);
29
+ return path.join(dirName, `${baseName}${suffix}${extension || '.glb'}`);
30
+ }
31
+ export async function loadGltfDocument(assetPath) {
32
+ if (!assetPath || typeof assetPath !== 'string') {
33
+ throw new Error('path is required');
34
+ }
35
+ const resolvedPath = resolveAssetPath(assetPath);
36
+ const extension = path.extname(resolvedPath).toLowerCase();
37
+ if (!SUPPORTED_EXTENSIONS.has(extension)) {
38
+ throw new Error(`Unsupported extension "${extension}". Expected .gltf or .glb.`);
39
+ }
40
+ let stat;
41
+ try {
42
+ stat = await fs.stat(resolvedPath);
43
+ }
44
+ catch {
45
+ throw new Error(`File not found: ${resolvedPath}`);
46
+ }
47
+ if (!stat.isFile()) {
48
+ throw new Error(`Not a file: ${resolvedPath}`);
49
+ }
50
+ try {
51
+ const document = await io.read(resolvedPath);
52
+ return { document, resolvedPath, bytes: stat.size };
53
+ }
54
+ catch (error) {
55
+ throw new Error(`Failed to read glTF: ${error instanceof Error ? error.message : String(error)}`);
56
+ }
57
+ }
58
+ export async function writeGltfDocument(document, outputPath) {
59
+ const resolvedOutput = resolveAssetPath(outputPath);
60
+ await io.write(resolvedOutput, document);
61
+ const stat = await fs.stat(resolvedOutput);
62
+ return stat.size;
63
+ }
64
+ //# sourceMappingURL=gltf-io.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gltf-io.js","sourceRoot":"","sources":["../src/gltf-io.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,120 @@
1
+ export interface GltfMeshPrimitiveSummary {
2
+ mode: string;
3
+ vertices: number;
4
+ triangles: number;
5
+ attributes: string[];
6
+ }
7
+ export interface GltfMeshSummary {
8
+ name: string | null;
9
+ primitiveCount: number;
10
+ estimatedVertices: number;
11
+ estimatedTriangles: number;
12
+ attributes: string[];
13
+ primitives: GltfMeshPrimitiveSummary[];
14
+ }
15
+ export interface GltfSceneSummary {
16
+ name: string | null;
17
+ nodeCount: number;
18
+ }
19
+ export interface GltfMaterialSummary {
20
+ name: string | null;
21
+ alphaMode: string;
22
+ doubleSided: boolean;
23
+ }
24
+ export interface GltfTextureSummary {
25
+ name: string | null;
26
+ mimeType: string | null;
27
+ }
28
+ export interface GltfAnimationSummary {
29
+ name: string | null;
30
+ channels: number;
31
+ samplers: number;
32
+ }
33
+ export interface GltfAnalysisSummary {
34
+ scenes: number;
35
+ nodes: number;
36
+ meshes: number;
37
+ materials: number;
38
+ textures: number;
39
+ animations: number;
40
+ skins: number;
41
+ drawCalls: number;
42
+ estimatedVertices: number;
43
+ estimatedTriangles: number;
44
+ }
45
+ export interface GltfAnalysis {
46
+ source: {
47
+ path: string;
48
+ bytes: number;
49
+ };
50
+ summary: GltfAnalysisSummary;
51
+ scenes: GltfSceneSummary[];
52
+ meshes: GltfMeshSummary[];
53
+ materials: GltfMaterialSummary[];
54
+ textures: GltfTextureSummary[];
55
+ animations: GltfAnimationSummary[];
56
+ inspection: unknown | null;
57
+ }
58
+ export interface GltfValidationLimits {
59
+ maxDrawCalls: number;
60
+ maxTriangles: number;
61
+ maxVertices: number;
62
+ maxTextures: number;
63
+ maxMaterials: number;
64
+ maxAnimations: number;
65
+ }
66
+ export interface GltfValidationReport {
67
+ source: {
68
+ path: string;
69
+ bytes: number;
70
+ };
71
+ valid: boolean;
72
+ errors: string[];
73
+ warnings: string[];
74
+ metrics: GltfAnalysisSummary;
75
+ limits: GltfValidationLimits;
76
+ issues: {
77
+ missingPosition: string[];
78
+ };
79
+ inspection: unknown | null;
80
+ }
81
+ export interface GltfOptimizationSimplifyOptions {
82
+ ratio?: number;
83
+ error?: number;
84
+ lockBorder?: boolean;
85
+ }
86
+ export interface GltfOptimizationTextureOptions {
87
+ format?: 'jpeg' | 'png' | 'webp' | 'avif';
88
+ resize?: [number, number] | 'nearest-pot' | 'ceil-pot' | 'floor-pot';
89
+ quality?: number;
90
+ useSharp?: boolean;
91
+ }
92
+ export interface GltfOptimizationOptions {
93
+ outputPath?: string;
94
+ dedup?: boolean;
95
+ prune?: boolean;
96
+ weld?: boolean;
97
+ quantize?: boolean;
98
+ simplify?: GltfOptimizationSimplifyOptions;
99
+ textures?: GltfOptimizationTextureOptions;
100
+ }
101
+ export interface GltfOptimizationReport {
102
+ source: {
103
+ path: string;
104
+ bytes: number;
105
+ };
106
+ output: {
107
+ path: string;
108
+ bytes: number;
109
+ bytesSaved: number;
110
+ percentSaved: number;
111
+ };
112
+ actions: string[];
113
+ warnings: string[];
114
+ summary: GltfAnalysisSummary;
115
+ inspection: unknown | null;
116
+ }
117
+ export declare function analyzeGltf(assetPath: string): Promise<GltfAnalysis>;
118
+ export declare function validateGltf(assetPath: string, limits?: Partial<GltfValidationLimits>): Promise<GltfValidationReport>;
119
+ export declare function optimizeGltf(assetPath: string, options?: GltfOptimizationOptions): Promise<GltfOptimizationReport>;
120
+ //# sourceMappingURL=gltf-tools.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gltf-tools.d.ts","sourceRoot":"","sources":["../src/gltf-tools.ts"],"names":[],"mappings":"AA4BA,MAAM,WAAW,wBAAwB;IACrC,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,EAAE,CAAC;CACxB;AAED,MAAM,WAAW,eAAe;IAC5B,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,cAAc,EAAE,MAAM,CAAC;IACvB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,kBAAkB,EAAE,MAAM,CAAC;IAC3B,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,UAAU,EAAE,wBAAwB,EAAE,CAAC;CAC1C;AAED,MAAM,WAAW,gBAAgB;IAC7B,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,mBAAmB;IAChC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,kBAAkB;IAC/B,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAED,MAAM,WAAW,oBAAoB;IACjC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,mBAAmB;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,kBAAkB,EAAE,MAAM,CAAC;CAC9B;AAED,MAAM,WAAW,YAAY;IACzB,MAAM,EAAE;QACJ,IAAI,EAAE,MAAM,CAAC;QACb,KAAK,EAAE,MAAM,CAAC;KACjB,CAAC;IACF,OAAO,EAAE,mBAAmB,CAAC;IAC7B,MAAM,EAAE,gBAAgB,EAAE,CAAC;IAC3B,MAAM,EAAE,eAAe,EAAE,CAAC;IAC1B,SAAS,EAAE,mBAAmB,EAAE,CAAC;IACjC,QAAQ,EAAE,kBAAkB,EAAE,CAAC;IAC/B,UAAU,EAAE,oBAAoB,EAAE,CAAC;IACnC,UAAU,EAAE,OAAO,GAAG,IAAI,CAAC;CAC9B;AAED,MAAM,WAAW,oBAAoB;IACjC,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,oBAAoB;IACjC,MAAM,EAAE;QACJ,IAAI,EAAE,MAAM,CAAC;QACb,KAAK,EAAE,MAAM,CAAC;KACjB,CAAC;IACF,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,OAAO,EAAE,mBAAmB,CAAC;IAC7B,MAAM,EAAE,oBAAoB,CAAC;IAC7B,MAAM,EAAE;QACJ,eAAe,EAAE,MAAM,EAAE,CAAC;KAC7B,CAAC;IACF,UAAU,EAAE,OAAO,GAAG,IAAI,CAAC;CAC9B;AAED,MAAM,WAAW,+BAA+B;IAC5C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,8BAA8B;IAC3C,MAAM,CAAC,EAAE,MAAM,GAAG,KAAK,GAAG,MAAM,GAAG,MAAM,CAAC;IAC1C,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,aAAa,GAAG,UAAU,GAAG,WAAW,CAAC;IACrE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACtB;AAED,MAAM,WAAW,uBAAuB;IACpC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,+BAA+B,CAAC;IAC3C,QAAQ,CAAC,EAAE,8BAA8B,CAAC;CAC7C;AAED,MAAM,WAAW,sBAAsB;IACnC,MAAM,EAAE;QACJ,IAAI,EAAE,MAAM,CAAC;QACb,KAAK,EAAE,MAAM,CAAC;KACjB,CAAC;IACF,MAAM,EAAE;QACJ,IAAI,EAAE,MAAM,CAAC;QACb,KAAK,EAAE,MAAM,CAAC;QACd,UAAU,EAAE,MAAM,CAAC;QACnB,YAAY,EAAE,MAAM,CAAC;KACxB,CAAC;IACF,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,OAAO,EAAE,mBAAmB,CAAC;IAC7B,UAAU,EAAE,OAAO,GAAG,IAAI,CAAC;CAC9B;AA6MD,wBAAsB,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAG1E;AAED,wBAAsB,YAAY,CAC9B,SAAS,EAAE,MAAM,EACjB,MAAM,CAAC,EAAE,OAAO,CAAC,oBAAoB,CAAC,GACvC,OAAO,CAAC,oBAAoB,CAAC,CAuE/B;AAkCD,wBAAsB,YAAY,CAC9B,SAAS,EAAE,MAAM,EACjB,OAAO,GAAE,uBAA4B,GACtC,OAAO,CAAC,sBAAsB,CAAC,CAgHjC"}
@@ -0,0 +1,360 @@
1
+ import { dedup, inspect, prune, quantize, simplify, textureCompress, weld } from '@gltf-transform/functions';
2
+ import { loadGltfDocument, resolveOutputPath, writeGltfDocument } from './gltf-io.js';
3
+ const KNOWN_ATTRIBUTES = [
4
+ 'POSITION',
5
+ 'NORMAL',
6
+ 'TANGENT',
7
+ 'TEXCOORD_0',
8
+ 'TEXCOORD_1',
9
+ 'COLOR_0',
10
+ 'JOINTS_0',
11
+ 'WEIGHTS_0',
12
+ ];
13
+ const MODE_LABELS = {
14
+ 0: 'POINTS',
15
+ 1: 'LINES',
16
+ 2: 'LINE_LOOP',
17
+ 3: 'LINE_STRIP',
18
+ 4: 'TRIANGLES',
19
+ 5: 'TRIANGLE_STRIP',
20
+ 6: 'TRIANGLE_FAN',
21
+ };
22
+ const DEFAULT_LIMITS = {
23
+ maxDrawCalls: 200,
24
+ maxTriangles: 200_000,
25
+ maxVertices: 400_000,
26
+ maxTextures: 12,
27
+ maxMaterials: 50,
28
+ maxAnimations: 200,
29
+ };
30
+ function normalizeName(name) {
31
+ const trimmed = name?.trim();
32
+ return trimmed ? trimmed : null;
33
+ }
34
+ function countSceneNodes(scene) {
35
+ let count = 0;
36
+ const visit = (node) => {
37
+ count += 1;
38
+ for (const child of node.listChildren()) {
39
+ visit(child);
40
+ }
41
+ };
42
+ for (const child of scene.listChildren()) {
43
+ visit(child);
44
+ }
45
+ return count;
46
+ }
47
+ function estimateTriangles(primitive, vertexCount, indexCount) {
48
+ const mode = primitive.getMode();
49
+ const count = indexCount > 0 ? indexCount : vertexCount;
50
+ if (mode === 4) {
51
+ return Math.floor(count / 3);
52
+ }
53
+ if (mode === 5 || mode === 6) {
54
+ return Math.max(0, count - 2);
55
+ }
56
+ return 0;
57
+ }
58
+ function collectMeshSummaries(document) {
59
+ const root = document.getRoot();
60
+ const meshes = root.listMeshes();
61
+ const missingPosition = [];
62
+ let totalPrimitives = 0;
63
+ let totalVertices = 0;
64
+ let totalTriangles = 0;
65
+ const meshSummaries = meshes.map((mesh, meshIndex) => {
66
+ const meshName = normalizeName(mesh.getName());
67
+ const meshLabel = meshName ?? `mesh_${meshIndex}`;
68
+ const primitives = mesh.listPrimitives();
69
+ const primitiveSummaries = primitives.map((primitive, primitiveIndex) => {
70
+ const position = primitive.getAttribute('POSITION');
71
+ if (!position) {
72
+ missingPosition.push(`${meshLabel}:primitive_${primitiveIndex}`);
73
+ }
74
+ const vertexCount = position ? position.getCount() : 0;
75
+ const indices = primitive.getIndices();
76
+ const indexCount = indices ? indices.getCount() : 0;
77
+ const triangleCount = estimateTriangles(primitive, vertexCount, indexCount);
78
+ const attributes = KNOWN_ATTRIBUTES.filter((attribute) => primitive.getAttribute(attribute) !== null);
79
+ const modeValue = primitive.getMode();
80
+ const mode = MODE_LABELS[modeValue] ?? `MODE_${modeValue}`;
81
+ totalPrimitives += 1;
82
+ totalVertices += vertexCount;
83
+ totalTriangles += triangleCount;
84
+ return {
85
+ mode,
86
+ vertices: vertexCount,
87
+ triangles: triangleCount,
88
+ attributes,
89
+ };
90
+ });
91
+ const attributes = Array.from(new Set(primitiveSummaries.flatMap((summary) => summary.attributes)));
92
+ const meshVertices = primitiveSummaries.reduce((sum, summary) => sum + summary.vertices, 0);
93
+ const meshTriangles = primitiveSummaries.reduce((sum, summary) => sum + summary.triangles, 0);
94
+ return {
95
+ name: meshName,
96
+ primitiveCount: primitives.length,
97
+ estimatedVertices: meshVertices,
98
+ estimatedTriangles: meshTriangles,
99
+ attributes,
100
+ primitives: primitiveSummaries,
101
+ };
102
+ });
103
+ return {
104
+ meshSummaries,
105
+ totalPrimitives,
106
+ totalVertices,
107
+ totalTriangles,
108
+ missingPosition,
109
+ };
110
+ }
111
+ function normalizeLimits(limits) {
112
+ if (!limits) {
113
+ return { ...DEFAULT_LIMITS };
114
+ }
115
+ const sanitized = Object.fromEntries(Object.entries(limits).filter(([, value]) => typeof value === 'number' && Number.isFinite(value)));
116
+ return {
117
+ ...DEFAULT_LIMITS,
118
+ ...sanitized,
119
+ };
120
+ }
121
+ async function safeInspect(document) {
122
+ try {
123
+ const result = inspect(document);
124
+ if (result && typeof result.then === 'function') {
125
+ return await result;
126
+ }
127
+ return result;
128
+ }
129
+ catch {
130
+ return null;
131
+ }
132
+ }
133
+ async function buildAnalysis(document, resolvedPath, bytes) {
134
+ const root = document.getRoot();
135
+ const { meshSummaries, totalPrimitives, totalVertices, totalTriangles, missingPosition } = collectMeshSummaries(document);
136
+ const scenes = root.listScenes().map((scene) => ({
137
+ name: normalizeName(scene.getName()),
138
+ nodeCount: countSceneNodes(scene),
139
+ }));
140
+ const materials = root.listMaterials().map((material) => ({
141
+ name: normalizeName(material.getName()),
142
+ alphaMode: material.getAlphaMode(),
143
+ doubleSided: material.getDoubleSided(),
144
+ }));
145
+ const textures = root.listTextures().map((texture) => ({
146
+ name: normalizeName(texture.getName()),
147
+ mimeType: texture.getMimeType(),
148
+ }));
149
+ const animations = root.listAnimations().map((animation) => ({
150
+ name: normalizeName(animation.getName()),
151
+ channels: animation.listChannels().length,
152
+ samplers: animation.listSamplers().length,
153
+ }));
154
+ const summary = {
155
+ scenes: scenes.length,
156
+ nodes: root.listNodes().length,
157
+ meshes: meshSummaries.length,
158
+ materials: materials.length,
159
+ textures: textures.length,
160
+ animations: animations.length,
161
+ skins: root.listSkins().length,
162
+ drawCalls: totalPrimitives,
163
+ estimatedVertices: totalVertices,
164
+ estimatedTriangles: totalTriangles,
165
+ };
166
+ const analysis = {
167
+ source: {
168
+ path: resolvedPath,
169
+ bytes,
170
+ },
171
+ summary,
172
+ scenes,
173
+ meshes: meshSummaries,
174
+ materials,
175
+ textures,
176
+ animations,
177
+ inspection: await safeInspect(document),
178
+ };
179
+ return { analysis, missingPosition };
180
+ }
181
+ export async function analyzeGltf(assetPath) {
182
+ const { document, resolvedPath, bytes } = await loadGltfDocument(assetPath);
183
+ return (await buildAnalysis(document, resolvedPath, bytes)).analysis;
184
+ }
185
+ export async function validateGltf(assetPath, limits) {
186
+ const { document, resolvedPath, bytes } = await loadGltfDocument(assetPath);
187
+ const { analysis, missingPosition } = await buildAnalysis(document, resolvedPath, bytes);
188
+ const normalizedLimits = normalizeLimits(limits);
189
+ const errors = [];
190
+ const warnings = [];
191
+ if (analysis.summary.scenes === 0) {
192
+ errors.push('No scenes found.');
193
+ }
194
+ if (analysis.summary.meshes === 0) {
195
+ warnings.push('No meshes found.');
196
+ }
197
+ if (missingPosition.length > 0) {
198
+ errors.push(`Missing POSITION attribute in ${missingPosition.length} primitive(s).`);
199
+ }
200
+ if (analysis.summary.drawCalls > normalizedLimits.maxDrawCalls) {
201
+ warnings.push(`High draw call count (${analysis.summary.drawCalls}). Target <= ${normalizedLimits.maxDrawCalls}.`);
202
+ }
203
+ if (analysis.summary.estimatedTriangles > normalizedLimits.maxTriangles) {
204
+ warnings.push(`High triangle count (${analysis.summary.estimatedTriangles}). Target <= ${normalizedLimits.maxTriangles}.`);
205
+ }
206
+ if (analysis.summary.estimatedVertices > normalizedLimits.maxVertices) {
207
+ warnings.push(`High vertex count (${analysis.summary.estimatedVertices}). Target <= ${normalizedLimits.maxVertices}.`);
208
+ }
209
+ if (analysis.summary.textures > normalizedLimits.maxTextures) {
210
+ warnings.push(`High texture count (${analysis.summary.textures}). Target <= ${normalizedLimits.maxTextures}.`);
211
+ }
212
+ if (analysis.summary.materials > normalizedLimits.maxMaterials) {
213
+ warnings.push(`High material count (${analysis.summary.materials}). Target <= ${normalizedLimits.maxMaterials}.`);
214
+ }
215
+ if (analysis.summary.animations > normalizedLimits.maxAnimations) {
216
+ warnings.push(`High animation count (${analysis.summary.animations}). Target <= ${normalizedLimits.maxAnimations}.`);
217
+ }
218
+ return {
219
+ source: {
220
+ path: resolvedPath,
221
+ bytes,
222
+ },
223
+ valid: errors.length === 0,
224
+ errors,
225
+ warnings,
226
+ metrics: analysis.summary,
227
+ limits: normalizedLimits,
228
+ issues: {
229
+ missingPosition,
230
+ },
231
+ inspection: analysis.inspection,
232
+ };
233
+ }
234
+ async function loadMeshoptSimplifier(warnings) {
235
+ try {
236
+ const module = await import('meshoptimizer');
237
+ const simplifier = module.MeshoptSimplifier ?? module.default?.MeshoptSimplifier;
238
+ if (!simplifier) {
239
+ warnings.push('Meshoptimizer simplifier not available. Skipping simplify.');
240
+ return null;
241
+ }
242
+ if ('ready' in simplifier && typeof simplifier.ready?.then === 'function') {
243
+ await simplifier.ready;
244
+ }
245
+ return simplifier;
246
+ }
247
+ catch (error) {
248
+ warnings.push(`Meshoptimizer not available. Skipping simplify. (${error instanceof Error ? error.message : String(error)})`);
249
+ return null;
250
+ }
251
+ }
252
+ async function loadSharpEncoder(warnings) {
253
+ try {
254
+ const module = await import('sharp');
255
+ return module.default ?? module;
256
+ }
257
+ catch (error) {
258
+ warnings.push(`Sharp encoder not available. Texture compression will use fallback. (${error instanceof Error ? error.message : String(error)})`);
259
+ return null;
260
+ }
261
+ }
262
+ export async function optimizeGltf(assetPath, options = {}) {
263
+ const { document, resolvedPath, bytes } = await loadGltfDocument(assetPath);
264
+ const outputPath = resolveOutputPath(resolvedPath, options.outputPath);
265
+ const actions = [];
266
+ const warnings = [];
267
+ const transforms = [];
268
+ const dedupEnabled = options.dedup ?? true;
269
+ if (dedupEnabled) {
270
+ transforms.push(dedup());
271
+ actions.push('dedup');
272
+ }
273
+ const weldEnabled = options.weld ?? true;
274
+ if (weldEnabled) {
275
+ transforms.push(weld());
276
+ actions.push('weld');
277
+ }
278
+ const quantizeEnabled = options.quantize ?? true;
279
+ if (quantizeEnabled) {
280
+ transforms.push(quantize());
281
+ actions.push('quantize');
282
+ }
283
+ if (options.simplify) {
284
+ const ratio = options.simplify.ratio ?? 0.75;
285
+ const error = options.simplify.error ?? 0.001;
286
+ const lockBorder = options.simplify.lockBorder ?? false;
287
+ if (ratio <= 0 || ratio >= 1) {
288
+ warnings.push('Simplify ratio must be between 0 and 1. Skipping simplify.');
289
+ }
290
+ else {
291
+ const simplifier = await loadMeshoptSimplifier(warnings);
292
+ if (simplifier) {
293
+ transforms.push(simplify({ simplifier, ratio, error, lockBorder }));
294
+ actions.push(`simplify(ratio=${ratio},error=${error})`);
295
+ }
296
+ }
297
+ }
298
+ const pruneEnabled = options.prune ?? true;
299
+ if (pruneEnabled) {
300
+ transforms.push(prune());
301
+ actions.push('prune');
302
+ }
303
+ const textures = options.textures;
304
+ if (textures) {
305
+ const root = document.getRoot();
306
+ if (root.listTextures().length === 0) {
307
+ warnings.push('No textures found to compress.');
308
+ }
309
+ else {
310
+ let resizeOption;
311
+ if (Array.isArray(textures.resize)) {
312
+ if (textures.resize.length === 2 &&
313
+ textures.resize.every((value) => typeof value === 'number' && Number.isFinite(value))) {
314
+ resizeOption = [textures.resize[0], textures.resize[1]];
315
+ }
316
+ else {
317
+ warnings.push('Texture resize must be a [width, height] array.');
318
+ }
319
+ }
320
+ else if (typeof textures.resize === 'string') {
321
+ resizeOption = textures.resize;
322
+ }
323
+ let encoder;
324
+ if (textures.useSharp) {
325
+ encoder = await loadSharpEncoder(warnings) ?? undefined;
326
+ }
327
+ transforms.push(textureCompress({
328
+ encoder,
329
+ targetFormat: textures.format,
330
+ resize: resizeOption,
331
+ quality: textures.quality,
332
+ }));
333
+ actions.push('textureCompress');
334
+ }
335
+ }
336
+ if (transforms.length > 0) {
337
+ await document.transform(...transforms);
338
+ }
339
+ const outputBytes = await writeGltfDocument(document, outputPath);
340
+ const bytesSaved = Math.max(0, bytes - outputBytes);
341
+ const percentSaved = bytes > 0 ? Math.round((bytesSaved / bytes) * 10000) / 100 : 0;
342
+ const { analysis } = await buildAnalysis(document, outputPath, outputBytes);
343
+ return {
344
+ source: {
345
+ path: resolvedPath,
346
+ bytes,
347
+ },
348
+ output: {
349
+ path: outputPath,
350
+ bytes: outputBytes,
351
+ bytesSaved,
352
+ percentSaved,
353
+ },
354
+ actions,
355
+ warnings,
356
+ summary: analysis.summary,
357
+ inspection: analysis.inspection,
358
+ };
359
+ }
360
+ //# sourceMappingURL=gltf-tools.js.map