@call-me-sensei/toonlab 0.4.21 → 0.4.22
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/NPM-LIBRARY.md +51 -2
- package/README.md +4 -3
- package/agents/PROMPTS.md +1 -1
- package/database/apply-one-catalog-seed.mjs +63 -0
- package/database/seeds/catalog/0005_2026-08-c8-first12.sql +1101 -0
- package/database/seeds/catalog/0006_2026-09-c8-first100.sql +14245 -0
- package/database/seeds/catalog/0007_2026-09-c8-first100-primary-model.sql +14845 -0
- package/package.json +4 -2
- package/src/catalog/officialCatalog.js +1 -0
- package/src/catalog/officialCatalogAssetRuntime.js +98 -1
- package/src/catalog/officialCatalogLod.js +160 -16
- package/src/catalog/officialCatalogPlacement.js +52 -12
- package/src/catalog/officialCatalogProvider.js +79 -8
- package/src/catalog/officialCatalogRockPackage.js +172 -0
- package/src/rock-shader/rockMaterial.js +205 -28
- package/src/rock-shader/rockShaderRuntime.js +17 -1
- package/src/rock-shader/rockShaderSettings.js +16 -0
- package/src/rockgen/lod/index.js +1 -0
- package/src/rockgen/lod/rockDenseFieldPolicy.js +143 -0
- package/src/rockgen/rockDocument.js +604 -26
- package/src/version.js +1 -1
- package/types/catalog/officialCatalog.d.ts +1 -0
- package/types/catalog/officialCatalogPlacement.d.ts +4 -0
- package/types/catalog/officialCatalogRockPackage.d.ts +68 -0
- package/types/index.d.ts +24 -0
- package/types/rock-shader/rockMaterial.d.ts +3 -0
- package/types/rock-shader/rockShaderSettings.d.ts +2 -0
- package/types/rockgen/lod/index.d.ts +1 -0
- package/types/rockgen/lod/rockDenseFieldPolicy.d.ts +181 -0
- package/types/rockgen/rockDocument.d.ts +579 -68
- package/types/version.d.ts +1 -1
|
@@ -19,12 +19,13 @@ import {
|
|
|
19
19
|
resolveRockgenPreset,
|
|
20
20
|
} from './rockgenPresets.js';
|
|
21
21
|
import { compileDocument } from './sdf/fieldCompiler.js';
|
|
22
|
+
import { resolveC7Projection } from './surface/c7GeologySurface.js';
|
|
22
23
|
|
|
23
24
|
/** Document type tag stamped on saved rockgen project JSON. */
|
|
24
25
|
export const ROCKGEN_PROJECT_DOCUMENT_TYPE = 'toonlab/rockgen-project';
|
|
25
26
|
|
|
26
27
|
/** Current schema version for rockgen project documents. */
|
|
27
|
-
export const ROCKGEN_PROJECT_SCHEMA_VERSION =
|
|
28
|
+
export const ROCKGEN_PROJECT_SCHEMA_VERSION = 7;
|
|
28
29
|
|
|
29
30
|
/** Sparse source-mesh edits are packed into the portable document so normal
|
|
30
31
|
* sculpting remains below the hosted creation-document limit. Runtime state
|
|
@@ -72,6 +73,26 @@ function lodTrianglesOption(value, targetTriangles, lodRatios) {
|
|
|
72
73
|
|
|
73
74
|
function surfacePackageOption(value) {
|
|
74
75
|
if (!value || typeof value !== 'object') return null;
|
|
76
|
+
const c8SurfaceVersion = Number(value.version);
|
|
77
|
+
if (value.schema === 'toonlab/c8-first12-geology-surface'
|
|
78
|
+
&& (c8SurfaceVersion === 1 || c8SurfaceVersion === 2)) {
|
|
79
|
+
const assetId = String(value.assetId ?? '').trim();
|
|
80
|
+
const geometrySha256 = String(value.geometrySha256 ?? '').toLowerCase();
|
|
81
|
+
const profileId = String(value.profileId ?? '').trim();
|
|
82
|
+
const mapResolution = Math.round(Number(value.mapResolution));
|
|
83
|
+
if (!assetId || !profileId || !/^[a-f0-9]{64}$/u.test(geometrySha256)
|
|
84
|
+
|| !Number.isInteger(mapResolution) || mapResolution < 1024 || mapResolution > 4096) return null;
|
|
85
|
+
return {
|
|
86
|
+
...structuredClone(value),
|
|
87
|
+
assetId,
|
|
88
|
+
geometrySha256,
|
|
89
|
+
mapResolution,
|
|
90
|
+
profileId,
|
|
91
|
+
schema: 'toonlab/c8-first12-geology-surface',
|
|
92
|
+
seed: Math.round(Number(value.seed) || 0) >>> 0,
|
|
93
|
+
version: c8SurfaceVersion,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
75
96
|
if (value.schema !== 'toonlab/c7-geology-surface' || Number(value.version) !== 1) return null;
|
|
76
97
|
const geology = String(value.geology ?? '').trim();
|
|
77
98
|
if (!geology) return null;
|
|
@@ -85,6 +106,146 @@ function surfacePackageOption(value) {
|
|
|
85
106
|
};
|
|
86
107
|
}
|
|
87
108
|
|
|
109
|
+
const SHA256_CONTENT_HASH = /^sha256:[0-9a-f]{64}$/u;
|
|
110
|
+
|
|
111
|
+
function contentHashOption(value) {
|
|
112
|
+
return SHA256_CONTENT_HASH.test(value ?? '') ? value : null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function customMeshArtifactOption(value, role) {
|
|
116
|
+
if (!value || typeof value !== 'object' || value.role !== role) return null;
|
|
117
|
+
if (value.mimeType !== 'model/gltf-binary' || value.unit !== 'metre'
|
|
118
|
+
|| Number(value.metreUnitScale) !== 1 || value.coordinateSystem !== 'right-handed-y-up') return null;
|
|
119
|
+
const dimensionsMetres = vector3Option(value.dimensionsMetres, [0, 0, 0]);
|
|
120
|
+
const min = vector3Option(value.boundsMetres?.min, [0, 0, 0]);
|
|
121
|
+
const max = vector3Option(value.boundsMetres?.max, [0, 0, 0]);
|
|
122
|
+
const contentHash = contentHashOption(value.contentHash);
|
|
123
|
+
const topologyHash = contentHashOption(value.topologyHash);
|
|
124
|
+
const uri = String(value.uri ?? '').trim();
|
|
125
|
+
const id = String(value.id ?? '').trim();
|
|
126
|
+
const byteLength = positiveInteger(value.byteLength, 0);
|
|
127
|
+
const geometryAudit = {
|
|
128
|
+
meshVertexCounts: Array.isArray(value.geometryAudit?.meshVertexCounts)
|
|
129
|
+
? value.geometryAudit.meshVertexCounts.map((entry) => positiveInteger(entry, 0))
|
|
130
|
+
: [],
|
|
131
|
+
topologyHash: contentHashOption(value.geometryAudit?.topologyHash),
|
|
132
|
+
triangleCount: positiveInteger(value.geometryAudit?.triangleCount, 0),
|
|
133
|
+
vertexCount: positiveInteger(value.geometryAudit?.vertexCount, 0),
|
|
134
|
+
};
|
|
135
|
+
if (!contentHash || !topologyHash || !uri.toLowerCase().endsWith('.glb')
|
|
136
|
+
|| !id || byteLength < 20
|
|
137
|
+
|| dimensionsMetres.some((entry) => entry <= 0)
|
|
138
|
+
|| !min.every((entry, axis) => entry < max[axis])
|
|
139
|
+
|| !dimensionsMetres.every((entry, axis) => (
|
|
140
|
+
Math.abs(entry - (max[axis] - min[axis])) <= Math.max(1e-6, entry * 1e-5)
|
|
141
|
+
))
|
|
142
|
+
|| geometryAudit.topologyHash !== topologyHash
|
|
143
|
+
|| geometryAudit.vertexCount < 4 || geometryAudit.triangleCount < 4
|
|
144
|
+
|| geometryAudit.meshVertexCounts.length === 0
|
|
145
|
+
|| geometryAudit.meshVertexCounts.some((entry) => entry <= 0)) return null;
|
|
146
|
+
return {
|
|
147
|
+
boundsMetres: { max, min },
|
|
148
|
+
byteLength,
|
|
149
|
+
contentHash,
|
|
150
|
+
coordinateSystem: 'right-handed-y-up',
|
|
151
|
+
dimensionsMetres,
|
|
152
|
+
geometryAudit: { ...geometryAudit, topologyHash },
|
|
153
|
+
id,
|
|
154
|
+
metreUnitScale: 1,
|
|
155
|
+
mimeType: 'model/gltf-binary',
|
|
156
|
+
role,
|
|
157
|
+
sourceRevision: positiveInteger(value.sourceRevision, 1),
|
|
158
|
+
topologyHash,
|
|
159
|
+
unit: 'metre',
|
|
160
|
+
uri,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function identityLandmarksOption(value) {
|
|
165
|
+
if (!Array.isArray(value)) return [];
|
|
166
|
+
return value.flatMap((entry) => {
|
|
167
|
+
const id = String(entry?.id ?? '').trim();
|
|
168
|
+
if (!id) return [];
|
|
169
|
+
return [{
|
|
170
|
+
id,
|
|
171
|
+
positionMetres: vector3Option(entry.positionMetres, [0, 0, 0]),
|
|
172
|
+
role: String(entry.role ?? 'identity').trim(),
|
|
173
|
+
}];
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function semanticRegionsOption(value, landmarkIds) {
|
|
178
|
+
if (!Array.isArray(value)) return [];
|
|
179
|
+
return value.flatMap((entry) => {
|
|
180
|
+
const id = String(entry?.id ?? '').trim();
|
|
181
|
+
const attribute = String(entry?.selector?.attribute ?? '').trim();
|
|
182
|
+
const selectorValue = Math.round(Number(entry?.selector?.value));
|
|
183
|
+
if (!id || !/^_[A-Z0-9_]+$/u.test(attribute) || !Number.isInteger(selectorValue) || selectorValue < 0) return [];
|
|
184
|
+
return [{
|
|
185
|
+
id,
|
|
186
|
+
label: String(entry.label ?? id),
|
|
187
|
+
landmarkIds: Array.isArray(entry.landmarkIds)
|
|
188
|
+
? entry.landmarkIds.map(String).filter((landmarkId) => landmarkIds.has(landmarkId))
|
|
189
|
+
: [],
|
|
190
|
+
role: String(entry.role ?? 'primary-mass'),
|
|
191
|
+
selector: { attribute, kind: 'gltf-attribute', value: selectorValue },
|
|
192
|
+
}];
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function customMeshSourceOption(value, identityLandmarks) {
|
|
197
|
+
if (!value || value.schema !== 'toonlab/c8-custom-mesh-source' || Number(value.version) !== 1) return null;
|
|
198
|
+
if (value.authority !== 'toonlab-editable-control-plus-retained-high') return null;
|
|
199
|
+
const control = customMeshArtifactOption(value.control, 'editable-control');
|
|
200
|
+
const highDetail = customMeshArtifactOption(value.highDetail, 'retained-high-detail');
|
|
201
|
+
if (!control || !highDetail || !control.id || !highDetail.id || control.id === highDetail.id
|
|
202
|
+
|| control.contentHash === highDetail.contentHash
|
|
203
|
+
|| control.geometryAudit.meshVertexCounts.some((entry) => entry <= 0)
|
|
204
|
+
|| highDetail.geometryAudit.meshVertexCounts.some((entry) => entry <= 0)
|
|
205
|
+
|| control.geometryAudit.meshVertexCounts.reduce((sum, entry) => sum + entry, 0) !== control.geometryAudit.vertexCount
|
|
206
|
+
|| highDetail.geometryAudit.meshVertexCounts.reduce((sum, entry) => sum + entry, 0) !== highDetail.geometryAudit.vertexCount
|
|
207
|
+
|| highDetail.geometryAudit.vertexCount <= control.geometryAudit.vertexCount
|
|
208
|
+
|| highDetail.geometryAudit.triangleCount <= control.geometryAudit.triangleCount
|
|
209
|
+
|| control.byteLength < 20 || highDetail.byteLength < 20) return null;
|
|
210
|
+
const landmarkIds = new Set(identityLandmarks.map((landmark) => landmark.id));
|
|
211
|
+
if (landmarkIds.size < 3 || landmarkIds.size !== identityLandmarks.length) return null;
|
|
212
|
+
const semanticRegions = semanticRegionsOption(value.semanticRegions, landmarkIds);
|
|
213
|
+
const maximumDisplacementMetres = Number(value.editEnvelope?.maximumDisplacementMetres);
|
|
214
|
+
const maximumVariationStrength = Number(value.editEnvelope?.maximumVariationStrength);
|
|
215
|
+
const semanticRegionIds = new Set(semanticRegions.map((region) => region.id));
|
|
216
|
+
const semanticSelectors = new Set(semanticRegions.map((region) => `${region.selector.attribute}:${region.selector.value}`));
|
|
217
|
+
const supportToleranceMetres = Math.max(...control.dimensionsMetres) * 1e-5;
|
|
218
|
+
const highCorrespondenceToleranceMetres = Math.max(...control.dimensionsMetres) * 0.05;
|
|
219
|
+
if (semanticRegions.length < 3 || semanticRegionIds.size !== semanticRegions.length
|
|
220
|
+
|| semanticSelectors.size !== semanticRegions.length
|
|
221
|
+
|| !semanticRegions.some((region) => region.role === 'base-support')
|
|
222
|
+
|| Math.abs(control.boundsMetres.min[1]) > Math.max(1e-6, supportToleranceMetres)
|
|
223
|
+
|| ['min', 'max'].some((key) => control.boundsMetres[key].some((entry, axis) => (
|
|
224
|
+
Math.abs(entry - highDetail.boundsMetres[key][axis]) > highCorrespondenceToleranceMetres
|
|
225
|
+
)))
|
|
226
|
+
|| !Number.isFinite(maximumDisplacementMetres)
|
|
227
|
+
|| maximumDisplacementMetres <= 0 || !Number.isFinite(maximumVariationStrength)
|
|
228
|
+
|| maximumVariationStrength <= 0 || maximumVariationStrength > 1) return null;
|
|
229
|
+
return {
|
|
230
|
+
authority: 'toonlab-editable-control-plus-retained-high',
|
|
231
|
+
control,
|
|
232
|
+
editEnvelope: {
|
|
233
|
+
maximumDisplacementMetres,
|
|
234
|
+
maximumVariationStrength,
|
|
235
|
+
mode: 'bounded-source-space',
|
|
236
|
+
},
|
|
237
|
+
highDetail,
|
|
238
|
+
schema: 'toonlab/c8-custom-mesh-source',
|
|
239
|
+
semanticRegions,
|
|
240
|
+
version: 1,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export function isRockSourceMeshReference(reference) {
|
|
245
|
+
return reference?.sourceMode === 'mesh-template'
|
|
246
|
+
|| Boolean(reference?.sourceMode === 'c8-custom-mesh' && reference.customMeshSource);
|
|
247
|
+
}
|
|
248
|
+
|
|
88
249
|
function bytesToBase64(bytes) {
|
|
89
250
|
let binary = '';
|
|
90
251
|
const chunkSize = 0x8000;
|
|
@@ -101,9 +262,13 @@ function base64ToBytes(value) {
|
|
|
101
262
|
return bytes;
|
|
102
263
|
}
|
|
103
264
|
|
|
104
|
-
function unpackMeshEdits(value) {
|
|
105
|
-
if (
|
|
106
|
-
if (typeof value
|
|
265
|
+
function unpackMeshEdits(value, { strict = false } = {}) {
|
|
266
|
+
if (value === null || value === undefined) return [];
|
|
267
|
+
if (typeof value !== 'object' || value.encoding !== ROCKGEN_MESH_EDIT_ENCODING
|
|
268
|
+
|| typeof value.data !== 'string' || value.data.length === 0) {
|
|
269
|
+
if (strict) throw new Error('C8 packed mesh edits require the exact supported encoding and non-empty data.');
|
|
270
|
+
return [];
|
|
271
|
+
}
|
|
107
272
|
try {
|
|
108
273
|
const bytes = base64ToBytes(value.data);
|
|
109
274
|
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
@@ -120,30 +285,73 @@ function unpackMeshEdits(value) {
|
|
|
120
285
|
offset += 4;
|
|
121
286
|
return result;
|
|
122
287
|
};
|
|
123
|
-
const operationCount =
|
|
288
|
+
const operationCount = readUint32();
|
|
289
|
+
if (operationCount > ROCKGEN_MAX_MESH_EDIT_OPERATIONS) throw new Error('operation count exceeds the portable limit');
|
|
290
|
+
if (value.operationCount !== undefined && Number(value.operationCount) !== operationCount) {
|
|
291
|
+
throw new Error('operation count metadata does not match the packed stream');
|
|
292
|
+
}
|
|
124
293
|
const edits = [];
|
|
125
|
-
let
|
|
126
|
-
for (let operationIndex = 0; operationIndex < operationCount
|
|
294
|
+
let totalDeltaCount = 0;
|
|
295
|
+
for (let operationIndex = 0; operationIndex < operationCount; operationIndex += 1) {
|
|
127
296
|
const meshIndex = readUint32();
|
|
128
297
|
const storedDeltaCount = readUint32();
|
|
129
|
-
|
|
298
|
+
if (storedDeltaCount === 0 || totalDeltaCount + storedDeltaCount > ROCKGEN_MAX_MESH_EDIT_DELTAS) {
|
|
299
|
+
throw new Error('delta count is empty or exceeds the portable limit');
|
|
300
|
+
}
|
|
130
301
|
const deltas = [];
|
|
131
302
|
for (let deltaIndex = 0; deltaIndex < storedDeltaCount; deltaIndex += 1) {
|
|
132
303
|
const delta = [readUint32(), readFloat32(), readFloat32(), readFloat32()];
|
|
133
|
-
if (
|
|
304
|
+
if (!delta.slice(1).every(Number.isFinite)) throw new Error('packed delta contains a non-finite value');
|
|
305
|
+
deltas.push(delta);
|
|
134
306
|
}
|
|
135
|
-
|
|
136
|
-
|
|
307
|
+
edits.push({ deltas, meshIndex });
|
|
308
|
+
totalDeltaCount += deltas.length;
|
|
309
|
+
}
|
|
310
|
+
if (value.deltaCount !== undefined && Number(value.deltaCount) !== totalDeltaCount) {
|
|
311
|
+
throw new Error('delta count metadata does not match the packed stream');
|
|
312
|
+
}
|
|
313
|
+
if (offset !== view.byteLength) {
|
|
314
|
+
throw new Error('packed mesh-edit stream contains trailing bytes');
|
|
137
315
|
}
|
|
138
316
|
return edits;
|
|
139
|
-
} catch {
|
|
317
|
+
} catch (error) {
|
|
318
|
+
if (strict) throw new Error(`Invalid C8 packed mesh edits: ${error.message}`);
|
|
140
319
|
return [];
|
|
141
320
|
}
|
|
142
321
|
}
|
|
143
322
|
|
|
144
|
-
function meshEditsOption(value, packedValue = null) {
|
|
145
|
-
|
|
323
|
+
function meshEditsOption(value, packedValue = null, { strict = false } = {}) {
|
|
324
|
+
if (strict && Array.isArray(value) && packedValue) {
|
|
325
|
+
throw new Error('C8 mesh edits must use either the unpacked or packed representation, never both.');
|
|
326
|
+
}
|
|
327
|
+
const source = Array.isArray(value) ? value : unpackMeshEdits(packedValue, { strict });
|
|
146
328
|
if (!Array.isArray(source)) return [];
|
|
329
|
+
if (strict) {
|
|
330
|
+
if (source.length > ROCKGEN_MAX_MESH_EDIT_OPERATIONS) {
|
|
331
|
+
throw new Error('C8 sparse sculpt history exceeds the portable operation limit.');
|
|
332
|
+
}
|
|
333
|
+
let deltaCount = 0;
|
|
334
|
+
return source.map((entry) => {
|
|
335
|
+
if (!entry || typeof entry !== 'object' || !Array.isArray(entry.deltas) || entry.deltas.length === 0
|
|
336
|
+
|| !Number.isInteger(entry.meshIndex) || entry.meshIndex < 0) {
|
|
337
|
+
throw new Error('C8 mesh edits require a non-negative integer mesh index and non-empty delta array.');
|
|
338
|
+
}
|
|
339
|
+
const deltas = entry.deltas.map((delta) => {
|
|
340
|
+
if (!Array.isArray(delta) || delta.length !== 4
|
|
341
|
+
|| !Number.isInteger(delta[0]) || delta[0] < 0
|
|
342
|
+
|| !delta.slice(1).every(Number.isFinite)
|
|
343
|
+
|| (delta[1] === 0 && delta[2] === 0 && delta[3] === 0)) {
|
|
344
|
+
throw new Error('C8 mesh edits require exact [vertexIndex, x, y, z] finite non-zero deltas.');
|
|
345
|
+
}
|
|
346
|
+
deltaCount += 1;
|
|
347
|
+
if (deltaCount > ROCKGEN_MAX_MESH_EDIT_DELTAS) {
|
|
348
|
+
throw new Error('C8 sparse sculpt history exceeds the portable delta limit.');
|
|
349
|
+
}
|
|
350
|
+
return [delta[0], Number(delta[1]), Number(delta[2]), Number(delta[3])];
|
|
351
|
+
});
|
|
352
|
+
return { deltas, meshIndex: entry.meshIndex };
|
|
353
|
+
});
|
|
354
|
+
}
|
|
147
355
|
let remaining = ROCKGEN_MAX_MESH_EDIT_DELTAS;
|
|
148
356
|
return source.slice(-ROCKGEN_MAX_MESH_EDIT_OPERATIONS).flatMap((entry) => {
|
|
149
357
|
if (remaining <= 0) return [];
|
|
@@ -167,6 +375,249 @@ function meshEditsOption(value, packedValue = null) {
|
|
|
167
375
|
});
|
|
168
376
|
}
|
|
169
377
|
|
|
378
|
+
function stableJsonValue(value) {
|
|
379
|
+
if (Array.isArray(value)) return `[${value.map(stableJsonValue).join(',')}]`;
|
|
380
|
+
if (value && typeof value === 'object') {
|
|
381
|
+
return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableJsonValue(value[key])}`).join(',')}}`;
|
|
382
|
+
}
|
|
383
|
+
return JSON.stringify(value);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function fnv1a64(value) {
|
|
387
|
+
let hash = 0xcbf29ce484222325n;
|
|
388
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
389
|
+
hash ^= BigInt(value.charCodeAt(index));
|
|
390
|
+
hash = BigInt.asUintN(64, hash * 0x100000001b3n);
|
|
391
|
+
}
|
|
392
|
+
return hash.toString(16).padStart(16, '0');
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/** Deterministic browser-safe identity for the complete saved C8 control/high/semantic authority. */
|
|
396
|
+
export function createC8ReferenceAuthoritySourceId(reference) {
|
|
397
|
+
if (reference?.sourceMode !== 'c8-custom-mesh' || !reference.customMeshSource) return null;
|
|
398
|
+
return `c8-authority-v1:fnv1a64:${fnv1a64(stableJsonValue(reference.customMeshSource))}`;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/** Deterministic browser-safe identity for the exact live C8 control geometry. */
|
|
402
|
+
export function createC8ReferenceGeometrySourceId(reference) {
|
|
403
|
+
if (reference?.sourceMode !== 'c8-custom-mesh' || !reference.customMeshSource?.control) return null;
|
|
404
|
+
const payload = {
|
|
405
|
+
authoritySourceId: createC8ReferenceAuthoritySourceId(reference),
|
|
406
|
+
controlContentHash: reference.customMeshSource.control.contentHash,
|
|
407
|
+
controlTopologyHash: reference.customMeshSource.control.topologyHash,
|
|
408
|
+
meshEdits: reference.meshEdits ?? [],
|
|
409
|
+
variation: Number(reference.variation) || 0,
|
|
410
|
+
variationSeed: Math.round(Number(reference.variationSeed) || 0) >>> 0,
|
|
411
|
+
};
|
|
412
|
+
return `c8-geometry-v2:fnv1a64:${fnv1a64(stableJsonValue(payload))}`;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/** Surface derivative identity, separate from geometry and explicit about the C7 recipe. */
|
|
416
|
+
export function createC8ReferenceSurfaceSourceId(reference) {
|
|
417
|
+
if (reference?.sourceMode !== 'c8-custom-mesh') return null;
|
|
418
|
+
const payload = {
|
|
419
|
+
authoritySourceId: createC8ReferenceAuthoritySourceId(reference),
|
|
420
|
+
geometrySourceId: reference.geometrySourceId ?? createC8ReferenceGeometrySourceId(reference),
|
|
421
|
+
geology: String(reference.geology ?? ''),
|
|
422
|
+
surfaceMode: reference.surfaceMode === 'generated' ? 'generated' : 'source',
|
|
423
|
+
surfacePackage: reference.surfacePackage ?? null,
|
|
424
|
+
topFinish: reference.topFinish ?? 'source',
|
|
425
|
+
};
|
|
426
|
+
return `c8-surface-v1:fnv1a64:${fnv1a64(stableJsonValue(payload))}`;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function c8SurfaceReprojectionOption(value, {
|
|
430
|
+
authoritySourceId,
|
|
431
|
+
controlContentHash,
|
|
432
|
+
geometrySourceId,
|
|
433
|
+
geology,
|
|
434
|
+
meshEditCount,
|
|
435
|
+
sourceId,
|
|
436
|
+
sourceRevision,
|
|
437
|
+
surfacePackage,
|
|
438
|
+
surfaceSourceId,
|
|
439
|
+
}) {
|
|
440
|
+
if (value === null || value === undefined) return null;
|
|
441
|
+
if (!value || typeof value !== 'object'
|
|
442
|
+
|| value.schema !== 'toonlab/c8-surface-reprojection'
|
|
443
|
+
|| Number(value.version) !== 1
|
|
444
|
+
|| value.method !== 'deterministic-metre-triplanar-map-reprojection'
|
|
445
|
+
|| value.trueHighToLowBake !== false) {
|
|
446
|
+
throw new Error('C8 surface reprojection metadata must use the deterministic metre-triplanar schema and can never claim a true high-to-low bake.');
|
|
447
|
+
}
|
|
448
|
+
if (Number(value.sourceRevision) !== sourceRevision) {
|
|
449
|
+
throw new Error('C8 surface reprojection metadata is stale for the saved source revision.');
|
|
450
|
+
}
|
|
451
|
+
if (value.controlContentHash && value.controlContentHash !== controlContentHash) {
|
|
452
|
+
throw new Error('C8 surface reprojection metadata is stale for the admitted control GLB.');
|
|
453
|
+
}
|
|
454
|
+
if (value.geometrySourceId && value.geometrySourceId !== geometrySourceId) {
|
|
455
|
+
throw new Error('C8 surface reprojection metadata is stale for the saved geometry content ID.');
|
|
456
|
+
}
|
|
457
|
+
if (value.authoritySourceId && value.authoritySourceId !== authoritySourceId) {
|
|
458
|
+
throw new Error('C8 surface reprojection metadata is stale for the complete control/high authority.');
|
|
459
|
+
}
|
|
460
|
+
if (value.surfaceSourceId && value.surfaceSourceId !== surfaceSourceId) {
|
|
461
|
+
throw new Error('C8 surface reprojection metadata is stale for the saved surface recipe.');
|
|
462
|
+
}
|
|
463
|
+
const dimensionsMetres = vector3Option(value.dimensionsMetres ?? value.projection?.dimensionsMetres, [0, 0, 0]);
|
|
464
|
+
if (dimensionsMetres.some((entry) => entry <= 0)) throw new Error('C8 surface reprojection requires positive metre dimensions.');
|
|
465
|
+
const projection = value.projection;
|
|
466
|
+
const scaleMetres = Number(projection?.scaleMetres);
|
|
467
|
+
const axisScale = vector3Option(projection?.axisScale, [0, 0, 0]);
|
|
468
|
+
if (projection?.mode !== 'world-metre-triplanar-isotropic'
|
|
469
|
+
|| !Number.isFinite(scaleMetres) || scaleMetres <= 0
|
|
470
|
+
|| axisScale.some((entry) => Math.abs(entry - scaleMetres) > 1e-9)) {
|
|
471
|
+
throw new Error('C8 surface reprojection must remain metre-isotropic on all three axes.');
|
|
472
|
+
}
|
|
473
|
+
const expectedProjection = resolveC7Projection({ dimensionsMetres, geology });
|
|
474
|
+
if (stableJsonValue(projection) !== stableJsonValue(expectedProjection)) {
|
|
475
|
+
throw new Error('C8 surface reprojection projection must be deterministically derived from its saved metre dimensions and geology.');
|
|
476
|
+
}
|
|
477
|
+
const mapResolution = Math.round(Number(value.mapResolution));
|
|
478
|
+
if (!Number.isInteger(mapResolution) || mapResolution < 32 || mapResolution > 2048) {
|
|
479
|
+
throw new Error('C8 surface reprojection resolution must be between 32 and 2048.');
|
|
480
|
+
}
|
|
481
|
+
const resolvedGeology = String(value.geology ?? '').trim();
|
|
482
|
+
if (!resolvedGeology || resolvedGeology !== geology) throw new Error('C8 surface reprojection geology does not match its saved surface package.');
|
|
483
|
+
const resolvedSeed = Math.round(Number(value.seed) || 0) >>> 0;
|
|
484
|
+
if (!surfacePackage || surfacePackage.geology !== resolvedGeology
|
|
485
|
+
|| surfacePackage.mapResolution !== mapResolution || surfacePackage.seed !== resolvedSeed) {
|
|
486
|
+
throw new Error('C8 surface reprojection must exactly match the saved C7 geology surface package.');
|
|
487
|
+
}
|
|
488
|
+
const record = {
|
|
489
|
+
authoritySourceId,
|
|
490
|
+
controlContentHash,
|
|
491
|
+
dimensionsMetres,
|
|
492
|
+
editOperationCount: meshEditCount,
|
|
493
|
+
geology: resolvedGeology,
|
|
494
|
+
geometrySourceId,
|
|
495
|
+
mapResolution,
|
|
496
|
+
method: 'deterministic-metre-triplanar-map-reprojection',
|
|
497
|
+
projection: structuredClone(expectedProjection),
|
|
498
|
+
schema: 'toonlab/c8-surface-reprojection',
|
|
499
|
+
seed: resolvedSeed,
|
|
500
|
+
sourceContentId: geometrySourceId,
|
|
501
|
+
sourceId,
|
|
502
|
+
sourceRevision,
|
|
503
|
+
surfaceSourceId,
|
|
504
|
+
trueHighToLowBake: false,
|
|
505
|
+
version: 1,
|
|
506
|
+
};
|
|
507
|
+
return { ...record, reprojectionContentId: `c8-reprojection-v1:fnv1a64:${fnv1a64(stableJsonValue(record))}` };
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function portableDerivedArtifactState(value, {
|
|
511
|
+
authoritySourceId,
|
|
512
|
+
customMeshSource,
|
|
513
|
+
geometrySourceId,
|
|
514
|
+
sourceRevision,
|
|
515
|
+
surfaceReprojection,
|
|
516
|
+
surfaceSourceId,
|
|
517
|
+
}) {
|
|
518
|
+
const source = value && typeof value === 'object' ? value : {};
|
|
519
|
+
const result = {};
|
|
520
|
+
for (const role of ['collision', 'geometricResidual', 'lods', 'pbrBake', 'runtimePackage']) {
|
|
521
|
+
const entry = source[role] && typeof source[role] === 'object' ? source[role] : {};
|
|
522
|
+
const status = entry.status === 'current' ? 'current' : 'stale';
|
|
523
|
+
if (status === 'current') {
|
|
524
|
+
const boundGeometryId = entry.geometrySourceId ?? entry.sourceGeometryContentId ?? entry.sourceContentId;
|
|
525
|
+
if (boundGeometryId !== geometrySourceId || !contentHashOption(entry.contentHash)
|
|
526
|
+
|| entry.authoritySourceId !== authoritySourceId
|
|
527
|
+
|| Number(entry.sourceRevision) !== sourceRevision) {
|
|
528
|
+
throw new Error(`Current C8 ${role} metadata must carry a content hash bound to the exact geometry source ID.`);
|
|
529
|
+
}
|
|
530
|
+
if (['pbrBake', 'runtimePackage'].includes(role) && entry.surfaceSourceId !== surfaceSourceId) {
|
|
531
|
+
throw new Error(`Current C8 ${role} metadata must be bound to the exact surface recipe ID.`);
|
|
532
|
+
}
|
|
533
|
+
if (role === 'pbrBake' && (entry.method !== 'blender-high-to-low-bake'
|
|
534
|
+
|| entry.trueHighToLowBake !== true
|
|
535
|
+
|| entry.highDetailContentHash !== customMeshSource.highDetail.contentHash)) {
|
|
536
|
+
throw new Error('A current C8 PBR bake must be an explicit Blender high-to-low bake bound to the retained high-detail GLB.');
|
|
537
|
+
}
|
|
538
|
+
if (role === 'geometricResidual' && (entry.method !== 'blender-signed-high-to-lod-residual'
|
|
539
|
+
|| entry.highDetailContentHash !== customMeshSource.highDetail.contentHash)) {
|
|
540
|
+
throw new Error('A current C8 geometric residual must be an explicit Blender signed high-to-LOD residual bound to the retained high-detail GLB.');
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
result[role] = {
|
|
544
|
+
authoritySourceId,
|
|
545
|
+
contentHash: contentHashOption(entry.contentHash),
|
|
546
|
+
geometrySourceId,
|
|
547
|
+
reason: String(entry.reason ?? (status === 'current' ? 'verified-derived-artifact' : 'geometry-source-not-baked')),
|
|
548
|
+
sourceRevision,
|
|
549
|
+
status,
|
|
550
|
+
...(['pbrBake', 'runtimePackage'].includes(role) ? { surfaceSourceId } : {}),
|
|
551
|
+
...(role === 'pbrBake' && status === 'current' ? {
|
|
552
|
+
highDetailContentHash: customMeshSource.highDetail.contentHash,
|
|
553
|
+
method: 'blender-high-to-low-bake',
|
|
554
|
+
trueHighToLowBake: true,
|
|
555
|
+
} : {}),
|
|
556
|
+
...(role === 'geometricResidual' && status === 'current' ? {
|
|
557
|
+
highDetailContentHash: customMeshSource.highDetail.contentHash,
|
|
558
|
+
method: 'blender-signed-high-to-lod-residual',
|
|
559
|
+
} : {}),
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
result.surfaceReprojection = surfaceReprojection
|
|
563
|
+
? {
|
|
564
|
+
authoritySourceId,
|
|
565
|
+
contentHash: surfaceReprojection.reprojectionContentId,
|
|
566
|
+
geometrySourceId,
|
|
567
|
+
method: surfaceReprojection.method,
|
|
568
|
+
sourceRevision,
|
|
569
|
+
status: 'current',
|
|
570
|
+
surfaceSourceId,
|
|
571
|
+
trueHighToLowBake: false,
|
|
572
|
+
}
|
|
573
|
+
: {
|
|
574
|
+
authoritySourceId,
|
|
575
|
+
contentHash: null,
|
|
576
|
+
geometrySourceId,
|
|
577
|
+
reason: 'geometry-source-not-reprojected',
|
|
578
|
+
sourceRevision,
|
|
579
|
+
status: 'stale',
|
|
580
|
+
surfaceSourceId,
|
|
581
|
+
trueHighToLowBake: false,
|
|
582
|
+
};
|
|
583
|
+
return result;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function validateC8MeshEditSequence(meshEdits, customMeshSource) {
|
|
587
|
+
const maximumDisplacementMetres = customMeshSource.editEnvelope.maximumDisplacementMetres;
|
|
588
|
+
const meshVertexCounts = customMeshSource.control.geometryAudit.meshVertexCounts;
|
|
589
|
+
const accumulated = new Map();
|
|
590
|
+
let deltaCount = 0;
|
|
591
|
+
for (const edit of meshEdits) {
|
|
592
|
+
const meshVertexCount = meshVertexCounts[edit.meshIndex];
|
|
593
|
+
if (!Number.isInteger(meshVertexCount)) {
|
|
594
|
+
throw new Error(`C8 sparse sculpt references missing control mesh ${edit.meshIndex}.`);
|
|
595
|
+
}
|
|
596
|
+
const operationVertices = new Set();
|
|
597
|
+
for (const [vertexIndex, x, y, z] of edit.deltas) {
|
|
598
|
+
if (vertexIndex >= meshVertexCount) {
|
|
599
|
+
throw new Error(`C8 sparse sculpt references missing vertex ${vertexIndex} on control mesh ${edit.meshIndex}.`);
|
|
600
|
+
}
|
|
601
|
+
if (operationVertices.has(vertexIndex)) {
|
|
602
|
+
throw new Error('C8 sparse sculpt operations may target each vertex only once.');
|
|
603
|
+
}
|
|
604
|
+
operationVertices.add(vertexIndex);
|
|
605
|
+
deltaCount += 1;
|
|
606
|
+
const key = `${edit.meshIndex}:${vertexIndex}`;
|
|
607
|
+
const prior = accumulated.get(key) ?? [0, 0, 0];
|
|
608
|
+
const next = [prior[0] + x, prior[1] + y, prior[2] + z];
|
|
609
|
+
if (Math.hypot(...next) > maximumDisplacementMetres + 1e-9) {
|
|
610
|
+
throw new Error(`C8 sparse sculpt history exceeds the ${maximumDisplacementMetres} metre edit envelope.`);
|
|
611
|
+
}
|
|
612
|
+
accumulated.set(key, next);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
if (meshEdits.length > ROCKGEN_MAX_MESH_EDIT_OPERATIONS || deltaCount > ROCKGEN_MAX_MESH_EDIT_DELTAS) {
|
|
616
|
+
throw new Error('C8 sparse sculpt history exceeds the portable operation or delta limit.');
|
|
617
|
+
}
|
|
618
|
+
return meshEdits;
|
|
619
|
+
}
|
|
620
|
+
|
|
170
621
|
function packMeshEdits(value) {
|
|
171
622
|
const edits = meshEditsOption(value);
|
|
172
623
|
if (edits.length === 0) return null;
|
|
@@ -203,14 +654,22 @@ function packMeshEdits(value) {
|
|
|
203
654
|
|
|
204
655
|
/**
|
|
205
656
|
* Portable identity for a source-GLB project. Geometry stays outside the JSON
|
|
206
|
-
* document; the document stores
|
|
207
|
-
*
|
|
208
|
-
* decode the
|
|
657
|
+
* document; the document stores either the stable catalog id or the exact C8
|
|
658
|
+
* control/high hashes, plus deterministic edit state needed to rebuild and
|
|
659
|
+
* decode the source GLB.
|
|
209
660
|
*/
|
|
210
661
|
export function createRockReferenceIdentity(options = null) {
|
|
211
662
|
if (!options || typeof options !== 'object') return null;
|
|
663
|
+
const c8Intent = options.sourceMode === 'c8-custom-mesh'
|
|
664
|
+
|| (options.customMeshSource !== null && options.customMeshSource !== undefined);
|
|
212
665
|
const id = String(options.id ?? '').trim();
|
|
213
|
-
if (!id)
|
|
666
|
+
if (!id) {
|
|
667
|
+
if (c8Intent) throw new Error('C8 custom-mesh documents require a stable source ID.');
|
|
668
|
+
return null;
|
|
669
|
+
}
|
|
670
|
+
if (c8Intent && options.sourceMode !== 'c8-custom-mesh') {
|
|
671
|
+
throw new Error('A C8 custom-mesh manifest cannot silently downgrade to a legacy mesh-template reference.');
|
|
672
|
+
}
|
|
214
673
|
const lodRatios = lodRatiosOption(options.lodRatios);
|
|
215
674
|
const lodTriangles = lodTrianglesOption(
|
|
216
675
|
options.lodTriangles,
|
|
@@ -221,6 +680,97 @@ export function createRockReferenceIdentity(options = null) {
|
|
|
221
680
|
.includes(options.topFinish)
|
|
222
681
|
? options.topFinish
|
|
223
682
|
: 'source';
|
|
683
|
+
const identityLandmarks = identityLandmarksOption(options.identityLandmarks);
|
|
684
|
+
const customMeshSource = customMeshSourceOption(options.customMeshSource, identityLandmarks);
|
|
685
|
+
if (options.sourceMode === 'c8-custom-mesh' && !customMeshSource) {
|
|
686
|
+
throw new Error('C8 custom-mesh documents require valid content-bound control/high GLBs, landmarks, semantic regions, and edit bounds.');
|
|
687
|
+
}
|
|
688
|
+
if (options.sourceMode === 'c8-custom-mesh'
|
|
689
|
+
&& !String(options.geology ?? options.surfacePackage?.geology ?? '').trim()) {
|
|
690
|
+
throw new Error('C8 custom-mesh documents require an explicit geology surface profile.');
|
|
691
|
+
}
|
|
692
|
+
const sourceMode = options.sourceMode === 'c8-custom-mesh' && customMeshSource
|
|
693
|
+
? 'c8-custom-mesh'
|
|
694
|
+
: 'mesh-template';
|
|
695
|
+
const sourceRevision = positiveInteger(options.sourceRevision, 1);
|
|
696
|
+
if (sourceMode === 'c8-custom-mesh'
|
|
697
|
+
&& (customMeshSource.control.sourceRevision > sourceRevision
|
|
698
|
+
|| customMeshSource.highDetail.sourceRevision > sourceRevision)) {
|
|
699
|
+
throw new Error('C8 control/high artifacts cannot claim a future source revision.');
|
|
700
|
+
}
|
|
701
|
+
const surfacePackage = surfacePackageOption(options.surfacePackage);
|
|
702
|
+
const geology = String(options.geology ?? options.surfacePackage?.geology ?? '').trim();
|
|
703
|
+
if (sourceMode === 'c8-custom-mesh' && (!surfacePackage || surfacePackage.geology !== geology)) {
|
|
704
|
+
throw new Error('C8 custom-mesh documents require one exact saved C7 geology surface package.');
|
|
705
|
+
}
|
|
706
|
+
let meshEdits = meshEditsOption(options.meshEdits, options.meshEditsPacked, {
|
|
707
|
+
strict: sourceMode === 'c8-custom-mesh',
|
|
708
|
+
});
|
|
709
|
+
if (sourceMode === 'c8-custom-mesh') {
|
|
710
|
+
meshEdits = meshEdits.map((edit) => ({
|
|
711
|
+
...edit,
|
|
712
|
+
deltas: edit.deltas.map(([vertexIndex, x, y, z]) => [vertexIndex, Math.fround(x), Math.fround(y), Math.fround(z)]),
|
|
713
|
+
}));
|
|
714
|
+
validateC8MeshEditSequence(meshEdits, customMeshSource);
|
|
715
|
+
}
|
|
716
|
+
const maximumVariationStrength = sourceMode === 'c8-custom-mesh'
|
|
717
|
+
? customMeshSource.editEnvelope.maximumVariationStrength
|
|
718
|
+
: 1;
|
|
719
|
+
const variation = Math.min(Math.max(Number(options.variation) || 0, 0), maximumVariationStrength);
|
|
720
|
+
const variationSeed = Math.round(Number(options.variationSeed) || 0) >>> 0;
|
|
721
|
+
const surfaceMode = options.surfaceMode === 'generated' ? 'generated' : 'source';
|
|
722
|
+
const authoritySourceId = sourceMode === 'c8-custom-mesh'
|
|
723
|
+
? createC8ReferenceAuthoritySourceId({ customMeshSource, sourceMode })
|
|
724
|
+
: null;
|
|
725
|
+
const geometrySourceId = sourceMode === 'c8-custom-mesh'
|
|
726
|
+
? createC8ReferenceGeometrySourceId({
|
|
727
|
+
customMeshSource,
|
|
728
|
+
meshEdits,
|
|
729
|
+
sourceMode,
|
|
730
|
+
variation,
|
|
731
|
+
variationSeed,
|
|
732
|
+
})
|
|
733
|
+
: null;
|
|
734
|
+
const surfaceSourceId = sourceMode === 'c8-custom-mesh'
|
|
735
|
+
? createC8ReferenceSurfaceSourceId({
|
|
736
|
+
customMeshSource,
|
|
737
|
+
geology,
|
|
738
|
+
geometrySourceId,
|
|
739
|
+
sourceMode,
|
|
740
|
+
surfaceMode,
|
|
741
|
+
surfacePackage,
|
|
742
|
+
topFinish,
|
|
743
|
+
})
|
|
744
|
+
: null;
|
|
745
|
+
if (sourceMode === 'c8-custom-mesh'
|
|
746
|
+
&& ((options.authoritySourceId && options.authoritySourceId !== authoritySourceId)
|
|
747
|
+
|| (options.geometrySourceId && options.geometrySourceId !== geometrySourceId)
|
|
748
|
+
|| (options.surfaceSourceId && options.surfaceSourceId !== surfaceSourceId))) {
|
|
749
|
+
throw new Error('Saved C8 authority, geometry, or surface identity is stale for the admitted source and edits.');
|
|
750
|
+
}
|
|
751
|
+
const surfaceReprojection = sourceMode === 'c8-custom-mesh'
|
|
752
|
+
? c8SurfaceReprojectionOption(options.surfaceReprojection, {
|
|
753
|
+
authoritySourceId,
|
|
754
|
+
controlContentHash: customMeshSource.control.contentHash,
|
|
755
|
+
geology,
|
|
756
|
+
geometrySourceId,
|
|
757
|
+
meshEditCount: meshEdits.length,
|
|
758
|
+
sourceId: id,
|
|
759
|
+
sourceRevision,
|
|
760
|
+
surfacePackage,
|
|
761
|
+
surfaceSourceId,
|
|
762
|
+
})
|
|
763
|
+
: null;
|
|
764
|
+
const derivedArtifactState = sourceMode === 'c8-custom-mesh'
|
|
765
|
+
? portableDerivedArtifactState(options.derivedArtifactState, {
|
|
766
|
+
authoritySourceId,
|
|
767
|
+
customMeshSource,
|
|
768
|
+
geometrySourceId,
|
|
769
|
+
sourceRevision,
|
|
770
|
+
surfaceReprojection,
|
|
771
|
+
surfaceSourceId,
|
|
772
|
+
})
|
|
773
|
+
: null;
|
|
224
774
|
return {
|
|
225
775
|
archetype: String(options.archetype ?? '').trim(),
|
|
226
776
|
catalogVersion: positiveInteger(options.catalogVersion, 1),
|
|
@@ -230,16 +780,29 @@ export function createRockReferenceIdentity(options = null) {
|
|
|
230
780
|
? lodTriangles.map((triangles) => triangles / lodTriangles[0])
|
|
231
781
|
: lodRatios,
|
|
232
782
|
lodTriangles,
|
|
233
|
-
meshEdits
|
|
783
|
+
meshEdits,
|
|
784
|
+
...(sourceMode === 'c8-custom-mesh' ? {
|
|
785
|
+
authoritySourceId,
|
|
786
|
+
customMeshSource,
|
|
787
|
+
derivedArtifactState,
|
|
788
|
+
geology,
|
|
789
|
+
geometrySourceId,
|
|
790
|
+
identityLandmarks,
|
|
791
|
+
label: String(options.label ?? id),
|
|
792
|
+
sourceRevision,
|
|
793
|
+
surfaceSourceId,
|
|
794
|
+
surfaceReprojection,
|
|
795
|
+
thumbnailUrl: String(options.thumbnailUrl ?? ''),
|
|
796
|
+
} : {}),
|
|
234
797
|
role: String(options.role ?? '').trim(),
|
|
235
798
|
series: String(options.series ?? '').trim(),
|
|
236
|
-
sourceMode
|
|
237
|
-
surfacePackage
|
|
238
|
-
surfaceMode
|
|
799
|
+
sourceMode,
|
|
800
|
+
surfacePackage,
|
|
801
|
+
surfaceMode,
|
|
239
802
|
targetTriangles: positiveInteger(options.targetTriangles, lodTriangles[0] ?? 0),
|
|
240
803
|
topFinish,
|
|
241
|
-
variation
|
|
242
|
-
variationSeed
|
|
804
|
+
variation,
|
|
805
|
+
variationSeed,
|
|
243
806
|
};
|
|
244
807
|
}
|
|
245
808
|
|
|
@@ -507,7 +1070,7 @@ export function computeDocumentBounds(document) {
|
|
|
507
1070
|
/** Serializes a document to JSON (dropping the runtime `revision`). */
|
|
508
1071
|
export function serializeRockDocument(document, { pretty = false } = {}) {
|
|
509
1072
|
const { revision, ...serializable } = document;
|
|
510
|
-
if (serializable.reference
|
|
1073
|
+
if (isRockSourceMeshReference(serializable.reference)) {
|
|
511
1074
|
const { meshEdits, ...reference } = serializable.reference;
|
|
512
1075
|
serializable.reference = {
|
|
513
1076
|
...reference,
|
|
@@ -526,7 +1089,8 @@ export function serializeRockDocument(document, { pretty = false } = {}) {
|
|
|
526
1089
|
// those deltas as float32 binary in portable JSON so normal sculpt sessions
|
|
527
1090
|
// fit the hosted creation limit. v6 adds the optional deterministic C7 surface
|
|
528
1091
|
// package identity used by the 480 source-mesh catalog. Existing
|
|
529
|
-
// preset/custom projects remain reference-free.
|
|
1092
|
+
// preset/custom projects remain reference-free. v7 preserves the content-bound
|
|
1093
|
+
// C8 control/high GLBs, semantic regions, landmarks, and derivative state.
|
|
530
1094
|
const MIGRATIONS = Object.freeze([
|
|
531
1095
|
(document) => ({
|
|
532
1096
|
...document,
|
|
@@ -571,6 +1135,20 @@ const MIGRATIONS = Object.freeze([
|
|
|
571
1135
|
: null,
|
|
572
1136
|
schemaVersion: 6,
|
|
573
1137
|
}),
|
|
1138
|
+
(document) => ({
|
|
1139
|
+
...document,
|
|
1140
|
+
reference: document.reference && typeof document.reference === 'object'
|
|
1141
|
+
? {
|
|
1142
|
+
...document.reference,
|
|
1143
|
+
customMeshSource: document.reference.customMeshSource ?? null,
|
|
1144
|
+
derivedArtifactState: document.reference.derivedArtifactState ?? null,
|
|
1145
|
+
identityLandmarks: document.reference.identityLandmarks ?? [],
|
|
1146
|
+
sourceRevision: document.reference.sourceRevision ?? 1,
|
|
1147
|
+
surfaceReprojection: document.reference.surfaceReprojection ?? null,
|
|
1148
|
+
}
|
|
1149
|
+
: null,
|
|
1150
|
+
schemaVersion: 7,
|
|
1151
|
+
}),
|
|
574
1152
|
]);
|
|
575
1153
|
|
|
576
1154
|
/**
|