@threenative/core 0.1.0 → 0.3.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,287 @@
1
+ diff --git a/build/three.cjs b/build/three.cjs
2
+ index 8939a67d..b0f91724 100644
3
+ --- a/build/three.cjs
4
+ +++ b/build/three.cjs
5
+ @@ -26116,6 +26116,8 @@ function copyArrayContents( src, target ) {
6
+ */
7
+ class BatchedMesh extends Mesh {
8
+
9
+ + static threeNativeBatchedVelocityPatch = true;
10
+ +
11
+ /**
12
+ * Constructs a new batched mesh.
13
+ *
14
+ diff --git a/build/three.core.js b/build/three.core.js
15
+ index 0bb9262c..868a634b 100644
16
+ --- a/build/three.core.js
17
+ +++ b/build/three.core.js
18
+ @@ -26136,6 +26136,8 @@ function copyArrayContents( src, target ) {
19
+ */
20
+ class BatchedMesh extends Mesh {
21
+
22
+ + static threeNativeBatchedVelocityPatch = true;
23
+ +
24
+ /**
25
+ * Constructs a new batched mesh.
26
+ *
27
+ diff --git a/build/three.webgpu.js b/build/three.webgpu.js
28
+ index 2e490796..325b497a 100644
29
+ --- a/build/three.webgpu.js
30
+ +++ b/build/three.webgpu.js
31
+ @@ -18500,7 +18500,8 @@ const instance = /*@__PURE__*/ Fn( ( [ matrices, colors = null ], builder ) => {
32
+
33
+ const previousInstanceData = _previousInstanceMatrices.get( object );
34
+
35
+ - previousInstanceData.previousInstanceMatrix.array.set( matrices.array );
36
+ + const scheduledPrevious = object[ Symbol.for( 'threenative.velocity.previousInstanceMatrices' ) ];
37
+ + previousInstanceData.previousInstanceMatrix.array.set( scheduledPrevious ?? matrices.array );
38
+
39
+ } );
40
+
41
+ @@ -18599,6 +18600,7 @@ const batch = /*@__PURE__*/ Fn( ( [ batchMesh ], builder ) => {
42
+ const indirectId = getIndirectIndex( batchMesh._indirectTexture, int( batchingIdNode ) );
43
+
44
+ const matricesTexture = batchMesh._matricesTexture;
45
+ + const previousMatricesTexture = batchMesh._previousMatricesTexture ?? matricesTexture;
46
+
47
+ const size = int( textureSize( textureLoad( matricesTexture ), 0 ).x ).toConst();
48
+ const j = float( indirectId ).mul( 4 ).toInt().toConst();
49
+ @@ -18626,6 +18628,19 @@ const batch = /*@__PURE__*/ Fn( ( [ batchMesh ], builder ) => {
50
+
51
+ positionLocal.assign( batchingMatrix.mul( positionLocal ) );
52
+
53
+ + if ( builder.needsPreviousData() ) {
54
+ +
55
+ + const previousBatchingMatrix = mat4(
56
+ + textureLoad( previousMatricesTexture, ivec2( x, y ) ),
57
+ + textureLoad( previousMatricesTexture, ivec2( x.add( 1 ), y ) ),
58
+ + textureLoad( previousMatricesTexture, ivec2( x.add( 2 ), y ) ),
59
+ + textureLoad( previousMatricesTexture, ivec2( x.add( 3 ), y ) )
60
+ + );
61
+ +
62
+ + positionPrevious.assign( previousBatchingMatrix.mul( positionPrevious ).xyz );
63
+ +
64
+ + }
65
+ +
66
+ const transformedNormal = normalLocal.div( vec3( bm[ 0 ].dot( bm[ 0 ] ), bm[ 1 ].dot( bm[ 1 ] ), bm[ 2 ].dot( bm[ 2 ] ) ) );
67
+
68
+ const batchingNormal = bm.mul( transformedNormal ).xyz;
69
+ @@ -18777,7 +18792,8 @@ const skinning = /*@__PURE__*/ Fn( ( [ skinnedMesh ], builder ) => {
70
+
71
+ if ( skeletonData !== undefined ) {
72
+
73
+ - skeletonData.previousBoneMatrices.set( skeleton.boneMatrices );
74
+ + const scheduledPrevious = object[ Symbol.for( 'threenative.velocity.previousBoneMatrices' ) ];
75
+ + skeletonData.previousBoneMatrices.set( scheduledPrevious ?? skeleton.boneMatrices );
76
+
77
+ }
78
+
79
+ @@ -18844,7 +18860,8 @@ const computeSkinning = /*@__PURE__*/ Fn( ( [ skinnedMesh, toPosition = null ],
80
+
81
+ if ( state !== undefined ) {
82
+
83
+ - state.previousBoneMatrices.set( skeleton.boneMatrices );
84
+ + const scheduledPrevious = skinnedMesh[ Symbol.for( 'threenative.velocity.previousBoneMatrices' ) ];
85
+ + state.previousBoneMatrices.set( scheduledPrevious ?? skeleton.boneMatrices );
86
+
87
+ }
88
+
89
+ @@ -39725,7 +39742,8 @@ class VelocityNode extends TempNode {
90
+
91
+ const previousModelMatrix = getPreviousMatrix( object );
92
+
93
+ - this.previousModelWorldMatrix.value.copy( previousModelMatrix );
94
+ + const scheduledPrevious = object[ Symbol.for( 'threenative.velocity.previousWorldMatrix' ) ];
95
+ + this.previousModelWorldMatrix.value.copy( scheduledPrevious ?? previousModelMatrix );
96
+
97
+ //
98
+
99
+ @@ -53841,7 +53859,7 @@ class NodeBuilder {
100
+
101
+ const mrt = this.renderer.getMRT();
102
+
103
+ - return ( mrt && mrt.has( 'velocity' ) ) || getDataFromObject( this.object ).useVelocity === true;
104
+ + return ( mrt && mrt.has( 'velocity' ) ) || getDataFromObject( this.object ).useVelocity === true || this.object.userData?.useVelocity === true;
105
+
106
+ }
107
+
108
+ @@ -83814,6 +83832,14 @@ class WebGPUBackend extends Backend {
109
+ */
110
+ this.compatibilityMode = null;
111
+
112
+ + /**
113
+ + * A reference to the WebGPU instance that creates adapters.
114
+ + *
115
+ + * @type {?GPU}
116
+ + * @default null
117
+ + */
118
+ + this.gpu = null;
119
+ +
120
+ /**
121
+ * A reference to the device.
122
+ *
123
+ @@ -83926,7 +83952,9 @@ class WebGPUBackend extends Backend {
124
+ xrCompatible: renderer.xr.enabled
125
+ };
126
+
127
+ - const adapter = ( typeof navigator !== 'undefined' ) ? await navigator.gpu.requestAdapter( adapterOptions ) : null;
128
+ + this.gpu = ( typeof navigator !== 'undefined' ) ? navigator.gpu : null;
129
+ +
130
+ + const adapter = this.gpu !== null ? await this.gpu.requestAdapter( adapterOptions ) : null;
131
+
132
+ if ( adapter === null ) {
133
+
134
+ diff --git a/src/nodes/accessors/Batch.js b/src/nodes/accessors/Batch.js
135
+ index ab62e98d..2cf4bb60 100644
136
+ --- a/src/nodes/accessors/Batch.js
137
+ +++ b/src/nodes/accessors/Batch.js
138
+ @@ -1,6 +1,6 @@
139
+
140
+ import { normalLocal } from './Normal.js';
141
+ -import { positionLocal } from './Position.js';
142
+ +import { positionLocal, positionPrevious } from './Position.js';
143
+ import { vec3, mat3, mat4, int, ivec2, float, Fn } from '../tsl/TSLBase.js';
144
+ import { textureLoad } from './TextureNode.js';
145
+ import { textureSize } from './TextureSizeNode.js';
146
+ @@ -64,6 +64,7 @@ export const batch = /*@__PURE__*/ Fn( ( [ batchMesh ], builder ) => {
147
+ const indirectId = getIndirectIndex( batchMesh._indirectTexture, int( batchingIdNode ) );
148
+
149
+ const matricesTexture = batchMesh._matricesTexture;
150
+ + const previousMatricesTexture = batchMesh._previousMatricesTexture ?? matricesTexture;
151
+
152
+ const size = int( textureSize( textureLoad( matricesTexture ), 0 ).x ).toConst();
153
+ const j = float( indirectId ).mul( 4 ).toInt().toConst();
154
+ @@ -90,6 +91,19 @@ export const batch = /*@__PURE__*/ Fn( ( [ batchMesh ], builder ) => {
155
+ const bm = mat3( batchingMatrix );
156
+
157
+ positionLocal.assign( batchingMatrix.mul( positionLocal ) );
158
+ + if ( builder.needsPreviousData() ) {
159
+ +
160
+ + const previousBatchingMatrix = mat4(
161
+ + textureLoad( previousMatricesTexture, ivec2( x, y ) ),
162
+ + textureLoad( previousMatricesTexture, ivec2( x.add( 1 ), y ) ),
163
+ + textureLoad( previousMatricesTexture, ivec2( x.add( 2 ), y ) ),
164
+ + textureLoad( previousMatricesTexture, ivec2( x.add( 3 ), y ) )
165
+ + );
166
+ +
167
+ + positionPrevious.assign( previousBatchingMatrix.mul( positionPrevious ).xyz );
168
+ +
169
+ + }
170
+ +
171
+
172
+ const transformedNormal = normalLocal.div( vec3( bm[ 0 ].dot( bm[ 0 ] ), bm[ 1 ].dot( bm[ 1 ] ), bm[ 2 ].dot( bm[ 2 ] ) ) );
173
+
174
+ @@ -104,5 +118,3 @@ export const batch = /*@__PURE__*/ Fn( ( [ batchMesh ], builder ) => {
175
+ }
176
+
177
+ }, 'void' );
178
+ -
179
+ -
180
+ diff --git a/src/nodes/accessors/Instance.js b/src/nodes/accessors/Instance.js
181
+ index b89834b9..c7b7bdd7 100644
182
+ --- a/src/nodes/accessors/Instance.js
183
+ +++ b/src/nodes/accessors/Instance.js
184
+ @@ -220,7 +220,8 @@ export const instance = /*@__PURE__*/ Fn( ( [ matrices, colors = null ], builder
185
+
186
+ const previousInstanceData = _previousInstanceMatrices.get( object );
187
+
188
+ - previousInstanceData.previousInstanceMatrix.array.set( matrices.array );
189
+ + const scheduledPrevious = object[ Symbol.for( 'threenative.velocity.previousInstanceMatrices' ) ];
190
+ + previousInstanceData.previousInstanceMatrix.array.set( scheduledPrevious ?? matrices.array );
191
+
192
+ } );
193
+
194
+ diff --git a/src/nodes/accessors/Skinning.js b/src/nodes/accessors/Skinning.js
195
+ index 2356aed8..aaeea633 100644
196
+ --- a/src/nodes/accessors/Skinning.js
197
+ +++ b/src/nodes/accessors/Skinning.js
198
+ @@ -149,7 +149,8 @@ export const skinning = /*@__PURE__*/ Fn( ( [ skinnedMesh ], builder ) => {
199
+
200
+ if ( skeletonData !== undefined ) {
201
+
202
+ - skeletonData.previousBoneMatrices.set( skeleton.boneMatrices );
203
+ + const scheduledPrevious = object[ Symbol.for( 'threenative.velocity.previousBoneMatrices' ) ];
204
+ + skeletonData.previousBoneMatrices.set( scheduledPrevious ?? skeleton.boneMatrices );
205
+
206
+ }
207
+
208
+ @@ -216,7 +217,8 @@ export const computeSkinning = /*@__PURE__*/ Fn( ( [ skinnedMesh, toPosition = n
209
+
210
+ if ( state !== undefined ) {
211
+
212
+ - state.previousBoneMatrices.set( skeleton.boneMatrices );
213
+ + const scheduledPrevious = skinnedMesh[ Symbol.for( 'threenative.velocity.previousBoneMatrices' ) ];
214
+ + state.previousBoneMatrices.set( scheduledPrevious ?? skeleton.boneMatrices );
215
+
216
+ }
217
+
218
+ diff --git a/src/nodes/accessors/VelocityNode.js b/src/nodes/accessors/VelocityNode.js
219
+ index 373548b0..c48002ab 100644
220
+ --- a/src/nodes/accessors/VelocityNode.js
221
+ +++ b/src/nodes/accessors/VelocityNode.js
222
+ @@ -106,7 +106,8 @@ class VelocityNode extends TempNode {
223
+
224
+ const previousModelMatrix = getPreviousMatrix( object );
225
+
226
+ - this.previousModelWorldMatrix.value.copy( previousModelMatrix );
227
+ + const scheduledPrevious = object[ Symbol.for( 'threenative.velocity.previousWorldMatrix' ) ];
228
+ + this.previousModelWorldMatrix.value.copy( scheduledPrevious ?? previousModelMatrix );
229
+
230
+ //
231
+
232
+ diff --git a/src/nodes/core/NodeBuilder.js b/src/nodes/core/NodeBuilder.js
233
+ index be00b81e..1990da8f 100644
234
+ --- a/src/nodes/core/NodeBuilder.js
235
+ +++ b/src/nodes/core/NodeBuilder.js
236
+ @@ -3450,7 +3450,7 @@ class NodeBuilder {
237
+
238
+ const mrt = this.renderer.getMRT();
239
+
240
+ - return ( mrt && mrt.has( 'velocity' ) ) || getDataFromObject( this.object ).useVelocity === true;
241
+ + return ( mrt && mrt.has( 'velocity' ) ) || getDataFromObject( this.object ).useVelocity === true || this.object.userData?.useVelocity === true;
242
+
243
+ }
244
+
245
+ diff --git a/src/objects/BatchedMesh.js b/src/objects/BatchedMesh.js
246
+ index 2ef7cb25..35cc0ffe 100644
247
+ --- a/src/objects/BatchedMesh.js
248
+ +++ b/src/objects/BatchedMesh.js
249
+ @@ -181,6 +181,8 @@ function copyArrayContents( src, target ) {
250
+ */
251
+ class BatchedMesh extends Mesh {
252
+
253
+ + static threeNativeBatchedVelocityPatch = true;
254
+ +
255
+ /**
256
+ * Constructs a new batched mesh.
257
+ *
258
+ diff --git a/src/renderers/webgpu/WebGPUBackend.js b/src/renderers/webgpu/WebGPUBackend.js
259
+ index 06e20369..37b5e0be 100644
260
+ --- a/src/renderers/webgpu/WebGPUBackend.js
261
+ +++ b/src/renderers/webgpu/WebGPUBackend.js
262
+ @@ -102,6 +102,14 @@ class WebGPUBackend extends Backend {
263
+ */
264
+ this.compatibilityMode = null;
265
+
266
+ + /**
267
+ + * A reference to the WebGPU instance that creates adapters.
268
+ + *
269
+ + * @type {?GPU}
270
+ + * @default null
271
+ + */
272
+ + this.gpu = null;
273
+ +
274
+ /**
275
+ * A reference to the device.
276
+ *
277
+ @@ -214,7 +222,9 @@ class WebGPUBackend extends Backend {
278
+ xrCompatible: renderer.xr.enabled
279
+ };
280
+
281
+ - const adapter = ( typeof navigator !== 'undefined' ) ? await navigator.gpu.requestAdapter( adapterOptions ) : null;
282
+ + this.gpu = ( typeof navigator !== 'undefined' ) ? navigator.gpu : null;
283
+ +
284
+ + const adapter = this.gpu !== null ? await this.gpu.requestAdapter( adapterOptions ) : null;
285
+
286
+ if ( adapter === null ) {
287
+
@@ -0,0 +1,297 @@
1
+ #!/usr/bin/env node
2
+ import { existsSync } from "node:fs";
3
+ import { readFile, writeFile } from "node:fs/promises";
4
+ import { createRequire } from "node:module";
5
+ import path from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+
8
+ const THREE_VERSION = "0.185.1";
9
+ const PATCH_NAME = `three@${THREE_VERSION}.patch`;
10
+
11
+ /** Apply the package-owned Three.js patch to the dependency resolved for the consumer. */
12
+ export async function applyThreePatch(options = {}) {
13
+ const packageRoot =
14
+ options.packageRoot ?? path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
15
+ const consumerRoot = path.resolve(
16
+ options.consumerRoot ??
17
+ process.env.INIT_CWD ??
18
+ process.env.npm_config_local_prefix ??
19
+ process.cwd(),
20
+ );
21
+ const threeRoot = options.threeRoot ?? resolveThreeRoot(consumerRoot, packageRoot);
22
+ const manifestPath = path.join(threeRoot, "package.json");
23
+ const manifest = await readJson(manifestPath, "Three.js package manifest");
24
+ if (manifest.name !== "three" || manifest.version !== THREE_VERSION) {
25
+ throw new Error(
26
+ `TN_THREE_PATCH_VERSION: expected three@${THREE_VERSION}, found ${String(manifest.name)}@${String(manifest.version)} at ${manifestPath}.`,
27
+ );
28
+ }
29
+
30
+ const patchPath = path.join(packageRoot, "patches", PATCH_NAME);
31
+ const patch = parsePatch(await readFile(patchPath, "utf8"), patchPath);
32
+ const plans = await planPatch(threeRoot, patch);
33
+ if (plans.every((plan) => plan.status === "patched")) return "unchanged";
34
+ if (plans.some((plan) => plan.status !== "stock")) {
35
+ throw new Error(
36
+ `TN_THREE_PATCH_PARTIAL: ${manifestPath} is neither stock nor fully patched; refusing to modify a partial installation.`,
37
+ );
38
+ }
39
+
40
+ for (const plan of plans) await writeFile(plan.file, plan.contents, "utf8");
41
+ return "patched";
42
+ }
43
+
44
+ function resolveThreeRoot(consumerRoot, packageRoot) {
45
+ const attempted = [];
46
+ for (const root of [packageRoot, consumerRoot]) {
47
+ try {
48
+ const resolver = createRequire(path.join(root, "package.json"));
49
+ const entry = resolver.resolve("three");
50
+ const resolvedRoot = findPackageRoot(entry);
51
+ if (!isWithinModuleSearchPath(resolvedRoot, root)) {
52
+ throw new Error(`resolved outside local module paths: ${resolvedRoot}`);
53
+ }
54
+ return resolvedRoot;
55
+ } catch (error) {
56
+ attempted.push(`${root}: ${error instanceof Error ? error.message : String(error)}`);
57
+ }
58
+ }
59
+ throw new Error(`TN_THREE_PATCH_MISSING: could not resolve three from ${attempted.join("; ")}.`);
60
+ }
61
+
62
+ function isWithinModuleSearchPath(candidate, root) {
63
+ return moduleSearchPaths(root).some((searchPath) => isWithinRoot(candidate, searchPath));
64
+ }
65
+
66
+ function moduleSearchPaths(root) {
67
+ const paths = [];
68
+ let current = path.resolve(root);
69
+ while (true) {
70
+ if (path.basename(current) !== "node_modules") paths.push(path.join(current, "node_modules"));
71
+ const parent = path.dirname(current);
72
+ if (parent === current) return paths;
73
+ current = parent;
74
+ }
75
+ }
76
+
77
+ function isWithinRoot(candidate, root) {
78
+ const relative = path.relative(root, candidate);
79
+ return (
80
+ relative === "" ||
81
+ (relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative))
82
+ );
83
+ }
84
+
85
+ function findPackageRoot(entry) {
86
+ let current = path.dirname(entry);
87
+ while (true) {
88
+ if (existsSync(path.join(current, "package.json"))) return current;
89
+ const parent = path.dirname(current);
90
+ if (parent === current) break;
91
+ current = parent;
92
+ }
93
+ throw new Error(`TN_THREE_PATCH_MISSING: resolved three entry has no package.json: ${entry}.`);
94
+ }
95
+
96
+ async function readJson(file, description) {
97
+ let text;
98
+ try {
99
+ text = await readFile(file, "utf8");
100
+ } catch (error) {
101
+ throw new Error(
102
+ `TN_THREE_PATCH_MISSING: could not read ${description} at ${file}: ${String(error)}.`,
103
+ );
104
+ }
105
+ try {
106
+ return JSON.parse(text);
107
+ } catch (error) {
108
+ throw new Error(
109
+ `TN_THREE_PATCH_INVALID: ${description} at ${file} is not valid JSON: ${String(error)}.`,
110
+ );
111
+ }
112
+ }
113
+
114
+ function parsePatch(text, patchPath) {
115
+ const files = parsePatchFiles(text, patchPath);
116
+ if (files.length === 0)
117
+ throw new Error(`TN_THREE_PATCH_INVALID: no file patches in ${patchPath}.`);
118
+ for (const file of files) {
119
+ validatePatchFile(file, patchPath);
120
+ }
121
+ return files;
122
+ }
123
+
124
+ function parsePatchFiles(text, patchPath) {
125
+ const files = [];
126
+ let current;
127
+ let hunk;
128
+ for (const line of text.replaceAll("\r\n", "\n").split("\n")) {
129
+ if (line.startsWith("diff --git ")) {
130
+ if (current !== undefined) files.push(finishPatchFile(current, patchPath));
131
+ current = { hunks: [], oldPath: undefined, newPath: undefined };
132
+ hunk = undefined;
133
+ continue;
134
+ }
135
+ if (current === undefined) continue;
136
+ hunk = parsePatchLine(line, current, hunk, patchPath);
137
+ }
138
+ if (current !== undefined) files.push(finishPatchFile(current, patchPath));
139
+ return files;
140
+ }
141
+
142
+ function parsePatchLine(line, current, hunk, patchPath) {
143
+ if (line.startsWith("--- ")) {
144
+ current.oldPath = patchPathFromHeader(line.slice(4), patchPath);
145
+ return hunk;
146
+ }
147
+ if (line.startsWith("+++ ")) {
148
+ current.newPath = patchPathFromHeader(line.slice(4), patchPath);
149
+ return hunk;
150
+ }
151
+ const parsedHunk = parsePatchHunk(line);
152
+ if (parsedHunk !== undefined) {
153
+ current.hunks.push(parsedHunk);
154
+ return parsedHunk;
155
+ }
156
+ if (line !== "\" && hunk !== undefined && isPatchLine(line))
157
+ hunk.lines.push(line);
158
+ return hunk;
159
+ }
160
+
161
+ function finishPatchFile(file, patchPath) {
162
+ if (file.oldPath === undefined || file.newPath === undefined || file.hunks.length === 0) {
163
+ throw new Error(`TN_THREE_PATCH_INVALID: incomplete file patch in ${patchPath}.`);
164
+ }
165
+ if (file.oldPath !== file.newPath) {
166
+ throw new Error(`TN_THREE_PATCH_INVALID: rename patches are not supported in ${patchPath}.`);
167
+ }
168
+ return file;
169
+ }
170
+
171
+ function parsePatchHunk(line) {
172
+ const header = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/u.exec(line);
173
+ if (header === null) return undefined;
174
+ return {
175
+ lines: [],
176
+ newCount: Number(header[4] ?? 1),
177
+ newStart: Number(header[3]),
178
+ oldCount: Number(header[2] ?? 1),
179
+ oldStart: Number(header[1]),
180
+ };
181
+ }
182
+
183
+ function isPatchLine(line) {
184
+ return line[0] === " " || line[0] === "+" || line[0] === "-";
185
+ }
186
+
187
+ function validatePatchFile(file, patchPath) {
188
+ for (const candidate of file.hunks) {
189
+ const oldCount = candidate.lines.filter((line) => line[0] === " " || line[0] === "-").length;
190
+ const newCount = candidate.lines.filter((line) => line[0] === " " || line[0] === "+").length;
191
+ if (oldCount !== candidate.oldCount || newCount !== candidate.newCount) {
192
+ throw new Error(`TN_THREE_PATCH_INVALID: hunk line counts do not match in ${patchPath}.`);
193
+ }
194
+ }
195
+ }
196
+
197
+ function patchPathFromHeader(header, patchPath) {
198
+ const value = header.trim().split(/\s+/u)[0];
199
+ if (value === undefined || (!value.startsWith("a/") && !value.startsWith("b/"))) {
200
+ throw new Error(`TN_THREE_PATCH_INVALID: unsupported file path in ${patchPath}.`);
201
+ }
202
+ const relative = value.slice(2);
203
+ if (relative.length === 0 || relative.split("/").some((part) => part === ".." || part === "")) {
204
+ throw new Error(`TN_THREE_PATCH_INVALID: unsafe file path '${value}' in ${patchPath}.`);
205
+ }
206
+ return relative;
207
+ }
208
+
209
+ async function planPatch(threeRoot, files) {
210
+ const plans = [];
211
+ for (const file of files) {
212
+ const target = path.resolve(threeRoot, file.oldPath);
213
+ if (path.relative(threeRoot, target).startsWith(`..${path.sep}`)) {
214
+ throw new Error(
215
+ `TN_THREE_PATCH_INVALID: patch escapes the Three.js package: ${file.oldPath}.`,
216
+ );
217
+ }
218
+ const original = await readFile(target, "utf8");
219
+ const lines = original.replaceAll("\r\n", "\n").split("\n");
220
+ const status = classifyFile(lines, file.hunks);
221
+ if (status === "patched") {
222
+ plans.push({ contents: original, file: target, status });
223
+ continue;
224
+ }
225
+ if (status !== "stock") {
226
+ throw new Error(
227
+ `TN_THREE_PATCH_PARTIAL: hunk state is mixed in ${target}; refusing to modify a partial installation.`,
228
+ );
229
+ }
230
+ plans.push({ contents: applyFile(lines, file.hunks, target).join("\n"), file: target, status });
231
+ }
232
+ return plans;
233
+ }
234
+
235
+ function classifyFile(lines, hunks) {
236
+ let stock = true;
237
+ let patched = true;
238
+ let offset = 0;
239
+ for (const hunk of hunks) {
240
+ const oldLines = hunk.lines
241
+ .filter((line) => line[0] === " " || line[0] === "-")
242
+ .map((line) => line.slice(1));
243
+ const newLines = hunk.lines
244
+ .filter((line) => line[0] === " " || line[0] === "+")
245
+ .map((line) => line.slice(1));
246
+ if (locate(lines, oldLines, hunk.oldStart - 1 + offset) === undefined) stock = false;
247
+ if (locate(lines, newLines, hunk.newStart - 1) === undefined) patched = false;
248
+ offset += newLines.length - oldLines.length;
249
+ }
250
+ if (stock) return "stock";
251
+ if (patched) return "patched";
252
+ return "mixed";
253
+ }
254
+
255
+ function applyFile(lines, hunks, file) {
256
+ const result = [...lines];
257
+ let offset = 0;
258
+ for (const hunk of hunks) {
259
+ const oldLines = hunk.lines
260
+ .filter((line) => line[0] === " " || line[0] === "-")
261
+ .map((line) => line.slice(1));
262
+ const newLines = hunk.lines
263
+ .filter((line) => line[0] === " " || line[0] === "+")
264
+ .map((line) => line.slice(1));
265
+ const index = locate(result, oldLines, hunk.oldStart - 1 + offset);
266
+ if (index === undefined) {
267
+ throw new Error(`TN_THREE_PATCH_INVALID: stock hunk no longer matches ${file}.`);
268
+ }
269
+ result.splice(index, oldLines.length, ...newLines);
270
+ offset += newLines.length - oldLines.length;
271
+ }
272
+ return result;
273
+ }
274
+
275
+ function locate(lines, expected, preferred) {
276
+ const candidates = [];
277
+ for (let index = 0; index <= lines.length - expected.length; index += 1) {
278
+ if (expected.every((line, offset) => lines[index + offset] === line)) candidates.push(index);
279
+ }
280
+ if (candidates.length === 0) return undefined;
281
+ candidates.sort((left, right) => Math.abs(left - preferred) - Math.abs(right - preferred));
282
+ if (
283
+ candidates.length > 1 &&
284
+ Math.abs(candidates[0] - preferred) === Math.abs(candidates[1] - preferred)
285
+ ) {
286
+ throw new Error(
287
+ `TN_THREE_PATCH_INVALID: ambiguous hunk location near line ${String(preferred + 1)}.`,
288
+ );
289
+ }
290
+ return candidates[0];
291
+ }
292
+
293
+ const entry = process.argv[1];
294
+ if (entry !== undefined && path.resolve(entry) === path.resolve(fileURLToPath(import.meta.url))) {
295
+ const result = await applyThreePatch();
296
+ process.stdout.write(`threenative: Three.js velocity patch ${result}\n`);
297
+ }
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env node
2
+ import { copyFileSync, existsSync } from "node:fs";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+
6
+ const coreRoot = path.resolve(fileURLToPath(import.meta.url), "..", "..");
7
+ const source = path.resolve(coreRoot, "..", "engine-mcp", "dist", "index.js");
8
+ const target = path.join(coreRoot, "mcp", "engine-server.mjs");
9
+
10
+ if (!existsSync(source)) {
11
+ throw new Error(
12
+ `TN_ENGINE_MCP_BUNDLE: build threenative-engine-mcp before core; missing ${source}`,
13
+ );
14
+ }
15
+ copyFileSync(source, target);
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env node
2
+ import path from "node:path";
3
+ import { pathToFileURL } from "node:url";
4
+ // Installing ThreeNative wires its MCP servers into the project. An agent authoring a game here
5
+ // should not have to be told the asset, sculpt and capability tools exist, or run a setup command
6
+ // to get them. `.mcp.json` serves compatible hosts; Codex reads project-scoped `.codex/config.toml`.
7
+ // Set THREENATIVE_SKIP_MCP_SETUP=1 to opt out.
8
+ import { ensureCodexMcpConfig, ensureMcpConfig, installTarget } from "../mcp/install.mjs";
9
+
10
+ export function runMcpSetup(environment = process.env, cwd = process.cwd()) {
11
+ const skip = environment.THREENATIVE_SKIP_MCP_SETUP;
12
+ if (skip === "1" || skip === "true") return;
13
+ // Never fail an install over this. A game that cannot write .mcp.json still runs.
14
+ try {
15
+ const target = installTarget(environment, cwd);
16
+ if (target === undefined) return;
17
+ const outcome = ensureMcpConfig(target);
18
+ const codexOutcome = ensureCodexMcpConfig(target);
19
+ if (outcome === "created" || outcome === "updated") {
20
+ process.stdout.write(`threenative: ${outcome} .mcp.json with the ThreeNative MCP servers\n`);
21
+ } else if (outcome === "unreadable") {
22
+ process.stdout.write("threenative: .mcp.json is not valid JSON; left it alone\n");
23
+ }
24
+ if (codexOutcome === "created" || codexOutcome === "updated") {
25
+ process.stdout.write(
26
+ `threenative: ${codexOutcome} .codex/config.toml with the ThreeNative MCP servers\n`,
27
+ );
28
+ }
29
+ } catch (error) {
30
+ process.stdout.write(`threenative: could not wire .mcp.json (${error})\n`);
31
+ }
32
+ }
33
+
34
+ const entry = process.argv[1];
35
+ if (entry !== undefined && pathToFileURL(path.resolve(entry)).href === import.meta.url)
36
+ runMcpSetup();
@@ -0,0 +1,13 @@
1
+ // Single owner of the core version (PRD-181): package.json is the source of truth and
2
+ // src/version.ts is generated from it and checked in. A hand-edited constant goes stale the
3
+ // moment the manifest bumps; the pinning spec turns that red.
4
+ import { readFile, writeFile } from "node:fs/promises";
5
+
6
+ const manifest = JSON.parse(await readFile(new URL("../package.json", import.meta.url), "utf8"));
7
+ if (typeof manifest.version !== "string" || manifest.version.length === 0) {
8
+ throw new Error("generate-version: package.json has no version");
9
+ }
10
+ await writeFile(
11
+ new URL("../src/version.ts", import.meta.url),
12
+ `// Generated by scripts/generate-version.mjs - do not edit. Source: package.json.\nexport const CORE_VERSION = ${JSON.stringify(manifest.version)};\n`,
13
+ );
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env node
2
+ import { applyThreePatch } from "./apply-three-patch.mjs";
3
+ import { runMcpSetup } from "./ensure-mcp.mjs";
4
+
5
+ await applyThreePatch();
6
+ runMcpSetup();
@@ -1,35 +0,0 @@
1
- import { AudioListener, Object3D, Audio, Vector3, PositionalAudio } from 'three';
2
-
3
- interface AudioBusOptions {
4
- readonly camera: Object3D;
5
- readonly gestureTarget?: EventTarget;
6
- readonly listener?: AudioListener;
7
- readonly source?: () => EventTarget | undefined;
8
- }
9
- interface AudioPlayOptions {
10
- readonly fade?: number;
11
- readonly loop?: boolean;
12
- readonly volume?: number;
13
- }
14
- interface AudioRuntimeSnapshot {
15
- readonly queued: number;
16
- readonly voices: number;
17
- }
18
- declare class AudioBus {
19
- #private;
20
- readonly listener: AudioListener;
21
- constructor(options: AudioBusOptions);
22
- get queued(): number;
23
- get voices(): number;
24
- setCamera(camera: Object3D): void;
25
- reparent(camera: Object3D): void;
26
- unlock(): Promise<void>;
27
- play(buffer: AudioBuffer, options?: AudioPlayOptions): Audio;
28
- playAt(buffer: AudioBuffer, source: Object3D | Vector3, options?: AudioPlayOptions): PositionalAudio;
29
- music(buffer: AudioBuffer, options?: AudioPlayOptions): Audio;
30
- stop(): void;
31
- dispose(): void;
32
- }
33
- declare function audioRuntimeSnapshot(): AudioRuntimeSnapshot;
34
-
35
- export { AudioBus as A, audioRuntimeSnapshot as a, type AudioBusOptions as b, type AudioPlayOptions as c };