@call-me-sensei/toonlab 0.4.21 → 0.4.23
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 +87 -27
- package/README.md +11 -10
- package/agents/PROMPTS.md +17 -3
- package/agents/README.md +13 -3
- package/agents/claude/CLAUDE.md +3 -0
- package/agents/codex/AGENTS.md +112 -159
- package/agents/cursor/toonlab.mdc +7 -3
- package/agents/examples/game-foundation.mjs +104 -0
- package/agents/references/game-lifecycle.md +57 -0
- package/agents/references/runtime-entry-points.md +26 -0
- package/agents/references/style-bundles.md +4 -1
- package/agents/skills/claude/asset-sourcing/SKILL.md +7 -4
- package/agents/skills/claude/environment/SKILL.md +4 -0
- package/agents/skills/claude/game-dev/SKILL.md +63 -87
- package/agents/skills/claude/lighting/SKILL.md +36 -0
- package/agents/skills/claude/post-processing/SKILL.md +8 -3
- package/agents/skills/claude/rock-ground-shaders/SKILL.md +10 -15
- package/agents/skills/claude/rockgen/SKILL.md +52 -21
- package/agents/skills/claude/scene-style-application/SKILL.md +4 -0
- package/agents/skills/claude/style-presets/SKILL.md +11 -8
- package/agents/skills/claude/toon-shading/SKILL.md +4 -0
- package/agents/skills/claude/vegetation-sky/SKILL.md +13 -7
- package/agents/skills/claude/visual-verification/SKILL.md +45 -119
- package/agents/skills/claude/water/SKILL.md +6 -2
- package/agents/skills/codex/asset-sourcing/SKILL.md +7 -4
- package/agents/skills/codex/environment/SKILL.md +4 -0
- package/agents/skills/codex/game-dev/SKILL.md +63 -87
- package/agents/skills/codex/lighting/SKILL.md +36 -0
- package/agents/skills/codex/post-processing/SKILL.md +8 -3
- package/agents/skills/codex/rock-ground-shaders/SKILL.md +10 -15
- package/agents/skills/codex/rockgen/SKILL.md +52 -21
- package/agents/skills/codex/scene-style-application/SKILL.md +4 -0
- package/agents/skills/codex/style-presets/SKILL.md +11 -8
- package/agents/skills/codex/toon-shading/SKILL.md +4 -0
- package/agents/skills/codex/vegetation-sky/SKILL.md +13 -7
- package/agents/skills/codex/visual-verification/SKILL.md +45 -119
- package/agents/skills/codex/water/SKILL.md +6 -2
- package/database/apply-one-catalog-seed.mjs +63 -0
- package/database/providers.mjs +1 -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/mcp/lab-management.mjs +11 -8
- package/mcp/public-catalog.mjs +0 -16
- package/mcp/tree-lab-contract.mjs +2381 -0
- package/mcp/vite-plugin.mjs +5 -1
- package/package.json +29 -18
- 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/cloud/cloudReprojection.js +40 -7
- package/src/cloud/cloudVolume.js +44 -7
- package/src/cloud/noise/baseShapeVolume.js +8 -6
- package/src/cloud/noise/cirrusMap.js +2 -1
- package/src/cloud/noise/curlNoise.js +2 -1
- package/src/cloud/noise/erosionVolume.js +2 -1
- package/src/cloud/noise/textureCache.js +77 -0
- package/src/cloud/noise/weatherMap.js +2 -1
- package/src/core/sha256.js +83 -0
- package/src/ground-shader/groundShaderMaterial.js +12 -3
- 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/index.js +9 -0
- package/src/rockgen/lod/index.js +1 -0
- package/src/rockgen/lod/rockDenseFieldPolicy.js +143 -0
- package/src/rockgen/rockDocument.js +899 -35
- package/src/rockgen/surface/c7GeologySurface.js +39 -9
- package/src/rockgen/surface/naturalRockSurface.js +1948 -0
- package/src/shaders-tsl/chunks/projected-water-caustics.js +14 -2
- package/src/shaders-tsl/chunks/water-foam.js +6 -6
- package/src/shaders-tsl/chunks/water-lighting.js +28 -5
- package/src/shaders-tsl/chunks/water-shore-state.js +5 -2
- package/src/shaders-tsl/chunks/water-waves.js +10 -5
- package/src/shaders-tsl/flower.js +7 -4
- package/src/shaders-tsl/grass.js +4 -3
- package/src/shaders-tsl/water-shore-state-simulation.js +3 -2
- package/src/shaders-tsl/water.js +209 -41
- package/src/sky/skySystem.js +30 -7
- package/src/toon/settings/perspectiveRemovalSettings.js +1 -1
- package/src/toon/toonSettings.js +74 -0
- package/src/vegetation/stylizedFlower.js +6 -0
- package/src/version.js +1 -1
- package/src/water/waterDetailSpectrum.js +91 -0
- package/src/water/waterDynamics.js +422 -0
- package/src/water/waterFoamParticles.js +196 -0
- package/src/water/waterFoamTexture.js +57 -0
- package/src/water/waterHydrodynamics.js +209 -0
- package/src/water/waterMaterial.js +1 -0
- package/src/water/waterRenderExtension.js +107 -0
- package/src/water/waterScenePasses.js +14 -6
- package/src/water/waterSettings.js +53 -26
- package/src/water/waterShoreMaterial.js +38 -14
- package/src/water/waterSpectralOcean.js +137 -0
- package/src/water/waterStageSettings.js +19 -1
- package/src/water/waterSurface.js +62 -18
- package/src/water/waterUnderwaterAtmosphere.js +30 -3
- 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/cloud/noise/baseShapeVolume.d.ts +1 -1
- package/types/cloud/noise/textureCache.d.ts +12 -0
- package/types/core/sha256.d.ts +2 -0
- package/types/index.d.ts +30 -0
- package/types/rock-shader/rockMaterial.d.ts +3 -0
- package/types/rock-shader/rockShaderSettings.d.ts +2 -0
- package/types/rockgen/index.d.ts +1 -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 +654 -71
- package/types/rockgen/surface/naturalRockSurface.d.ts +100 -0
- package/types/shaders-tsl/chunks/projected-water-caustics.d.ts +7 -1
- package/types/shaders-tsl/chunks/water-lighting.d.ts +4 -3
- package/types/shaders-tsl/water.d.ts +2 -1
- package/types/version.d.ts +1 -1
- package/types/water/waterDetailSpectrum.d.ts +13 -0
- package/types/water/waterDynamics.d.ts +89 -0
- package/types/water/waterFoamParticles.d.ts +32 -0
- package/types/water/waterFoamTexture.d.ts +1 -0
- package/types/water/waterHydrodynamics.d.ts +49 -0
- package/types/water/waterRenderExtension.d.ts +16 -0
- package/types/water/waterScenePasses.d.ts +1 -1
- package/types/water/waterSettings.d.ts +1 -0
- package/types/water/waterSpectralOcean.d.ts +17 -0
- package/types/water/waterStageSettings.d.ts +1 -1
- package/types/water/waterUnderwaterAtmosphere.d.ts +19 -4
|
@@ -19,18 +19,19 @@ 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 = 9;
|
|
28
29
|
|
|
29
|
-
/**
|
|
30
|
-
*
|
|
31
|
-
* is still the convenient array form consumed by the editor and compiler. */
|
|
30
|
+
/** Legacy MCP request budgets. The interactive Rock Lab editor no longer uses
|
|
31
|
+
* these as sculpt, drill, persistence, or replay ceilings. */
|
|
32
32
|
export const ROCKGEN_MAX_MESH_EDIT_OPERATIONS = 200;
|
|
33
33
|
export const ROCKGEN_MAX_MESH_EDIT_DELTAS = 10_000;
|
|
34
|
+
export const ROCKGEN_MAX_MESH_CUT_OPERATIONS = 64;
|
|
34
35
|
export const ROCKGEN_MESH_EDIT_ENCODING = 'base64-f32le-v1';
|
|
35
36
|
|
|
36
37
|
const COMBINE_OPS = Object.freeze(['union', 'smoothUnion', 'subtract', 'intersect']);
|
|
@@ -72,6 +73,49 @@ 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
|
+
}
|
|
96
|
+
const schema = String(value.schema ?? '').trim();
|
|
97
|
+
const profileId = String(value.profileId ?? '').trim();
|
|
98
|
+
const assetId = String(value.assetId ?? '').trim();
|
|
99
|
+
const projectionMode = String(value.projection?.mode ?? '').trim();
|
|
100
|
+
const projectionScaleMetres = Number(value.projection?.scaleMetres);
|
|
101
|
+
const genericMapResolution = Math.round(Number(value.mapResolution));
|
|
102
|
+
if (schema.startsWith('toonlab/')
|
|
103
|
+
&& Number.isInteger(c8SurfaceVersion) && c8SurfaceVersion >= 1
|
|
104
|
+
&& assetId && profileId
|
|
105
|
+
&& ['triplanar', 'directional-bedding', 'world-metre-triplanar-isotropic'].includes(projectionMode)
|
|
106
|
+
&& Number.isFinite(projectionScaleMetres) && projectionScaleMetres > 0
|
|
107
|
+
&& Number.isInteger(genericMapResolution)
|
|
108
|
+
&& genericMapResolution >= 32 && genericMapResolution <= 4096) {
|
|
109
|
+
return {
|
|
110
|
+
...structuredClone(value),
|
|
111
|
+
assetId,
|
|
112
|
+
mapResolution: genericMapResolution,
|
|
113
|
+
profileId,
|
|
114
|
+
schema,
|
|
115
|
+
seed: Math.round(Number(value.seed) || 0) >>> 0,
|
|
116
|
+
version: c8SurfaceVersion,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
75
119
|
if (value.schema !== 'toonlab/c7-geology-surface' || Number(value.version) !== 1) return null;
|
|
76
120
|
const geology = String(value.geology ?? '').trim();
|
|
77
121
|
if (!geology) return null;
|
|
@@ -85,6 +129,146 @@ function surfacePackageOption(value) {
|
|
|
85
129
|
};
|
|
86
130
|
}
|
|
87
131
|
|
|
132
|
+
const SHA256_CONTENT_HASH = /^sha256:[0-9a-f]{64}$/u;
|
|
133
|
+
|
|
134
|
+
function contentHashOption(value) {
|
|
135
|
+
return SHA256_CONTENT_HASH.test(value ?? '') ? value : null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function customMeshArtifactOption(value, role) {
|
|
139
|
+
if (!value || typeof value !== 'object' || value.role !== role) return null;
|
|
140
|
+
if (value.mimeType !== 'model/gltf-binary' || value.unit !== 'metre'
|
|
141
|
+
|| Number(value.metreUnitScale) !== 1 || value.coordinateSystem !== 'right-handed-y-up') return null;
|
|
142
|
+
const dimensionsMetres = vector3Option(value.dimensionsMetres, [0, 0, 0]);
|
|
143
|
+
const min = vector3Option(value.boundsMetres?.min, [0, 0, 0]);
|
|
144
|
+
const max = vector3Option(value.boundsMetres?.max, [0, 0, 0]);
|
|
145
|
+
const contentHash = contentHashOption(value.contentHash);
|
|
146
|
+
const topologyHash = contentHashOption(value.topologyHash);
|
|
147
|
+
const uri = String(value.uri ?? '').trim();
|
|
148
|
+
const id = String(value.id ?? '').trim();
|
|
149
|
+
const byteLength = positiveInteger(value.byteLength, 0);
|
|
150
|
+
const geometryAudit = {
|
|
151
|
+
meshVertexCounts: Array.isArray(value.geometryAudit?.meshVertexCounts)
|
|
152
|
+
? value.geometryAudit.meshVertexCounts.map((entry) => positiveInteger(entry, 0))
|
|
153
|
+
: [],
|
|
154
|
+
topologyHash: contentHashOption(value.geometryAudit?.topologyHash),
|
|
155
|
+
triangleCount: positiveInteger(value.geometryAudit?.triangleCount, 0),
|
|
156
|
+
vertexCount: positiveInteger(value.geometryAudit?.vertexCount, 0),
|
|
157
|
+
};
|
|
158
|
+
if (!contentHash || !topologyHash || !uri.toLowerCase().endsWith('.glb')
|
|
159
|
+
|| !id || byteLength < 20
|
|
160
|
+
|| dimensionsMetres.some((entry) => entry <= 0)
|
|
161
|
+
|| !min.every((entry, axis) => entry < max[axis])
|
|
162
|
+
|| !dimensionsMetres.every((entry, axis) => (
|
|
163
|
+
Math.abs(entry - (max[axis] - min[axis])) <= Math.max(1e-6, entry * 1e-5)
|
|
164
|
+
))
|
|
165
|
+
|| geometryAudit.topologyHash !== topologyHash
|
|
166
|
+
|| geometryAudit.vertexCount < 4 || geometryAudit.triangleCount < 4
|
|
167
|
+
|| geometryAudit.meshVertexCounts.length === 0
|
|
168
|
+
|| geometryAudit.meshVertexCounts.some((entry) => entry <= 0)) return null;
|
|
169
|
+
return {
|
|
170
|
+
boundsMetres: { max, min },
|
|
171
|
+
byteLength,
|
|
172
|
+
contentHash,
|
|
173
|
+
coordinateSystem: 'right-handed-y-up',
|
|
174
|
+
dimensionsMetres,
|
|
175
|
+
geometryAudit: { ...geometryAudit, topologyHash },
|
|
176
|
+
id,
|
|
177
|
+
metreUnitScale: 1,
|
|
178
|
+
mimeType: 'model/gltf-binary',
|
|
179
|
+
role,
|
|
180
|
+
sourceRevision: positiveInteger(value.sourceRevision, 1),
|
|
181
|
+
topologyHash,
|
|
182
|
+
unit: 'metre',
|
|
183
|
+
uri,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function identityLandmarksOption(value) {
|
|
188
|
+
if (!Array.isArray(value)) return [];
|
|
189
|
+
return value.flatMap((entry) => {
|
|
190
|
+
const id = String(entry?.id ?? '').trim();
|
|
191
|
+
if (!id) return [];
|
|
192
|
+
return [{
|
|
193
|
+
id,
|
|
194
|
+
positionMetres: vector3Option(entry.positionMetres, [0, 0, 0]),
|
|
195
|
+
role: String(entry.role ?? 'identity').trim(),
|
|
196
|
+
}];
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function semanticRegionsOption(value, landmarkIds) {
|
|
201
|
+
if (!Array.isArray(value)) return [];
|
|
202
|
+
return value.flatMap((entry) => {
|
|
203
|
+
const id = String(entry?.id ?? '').trim();
|
|
204
|
+
const attribute = String(entry?.selector?.attribute ?? '').trim();
|
|
205
|
+
const selectorValue = Math.round(Number(entry?.selector?.value));
|
|
206
|
+
if (!id || !/^_[A-Z0-9_]+$/u.test(attribute) || !Number.isInteger(selectorValue) || selectorValue < 0) return [];
|
|
207
|
+
return [{
|
|
208
|
+
id,
|
|
209
|
+
label: String(entry.label ?? id),
|
|
210
|
+
landmarkIds: Array.isArray(entry.landmarkIds)
|
|
211
|
+
? entry.landmarkIds.map(String).filter((landmarkId) => landmarkIds.has(landmarkId))
|
|
212
|
+
: [],
|
|
213
|
+
role: String(entry.role ?? 'primary-mass'),
|
|
214
|
+
selector: { attribute, kind: 'gltf-attribute', value: selectorValue },
|
|
215
|
+
}];
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function customMeshSourceOption(value, identityLandmarks) {
|
|
220
|
+
if (!value || value.schema !== 'toonlab/c8-custom-mesh-source' || Number(value.version) !== 1) return null;
|
|
221
|
+
if (value.authority !== 'toonlab-editable-control-plus-retained-high') return null;
|
|
222
|
+
const control = customMeshArtifactOption(value.control, 'editable-control');
|
|
223
|
+
const highDetail = customMeshArtifactOption(value.highDetail, 'retained-high-detail');
|
|
224
|
+
if (!control || !highDetail || !control.id || !highDetail.id || control.id === highDetail.id
|
|
225
|
+
|| control.contentHash === highDetail.contentHash
|
|
226
|
+
|| control.geometryAudit.meshVertexCounts.some((entry) => entry <= 0)
|
|
227
|
+
|| highDetail.geometryAudit.meshVertexCounts.some((entry) => entry <= 0)
|
|
228
|
+
|| control.geometryAudit.meshVertexCounts.reduce((sum, entry) => sum + entry, 0) !== control.geometryAudit.vertexCount
|
|
229
|
+
|| highDetail.geometryAudit.meshVertexCounts.reduce((sum, entry) => sum + entry, 0) !== highDetail.geometryAudit.vertexCount
|
|
230
|
+
|| highDetail.geometryAudit.vertexCount <= control.geometryAudit.vertexCount
|
|
231
|
+
|| highDetail.geometryAudit.triangleCount <= control.geometryAudit.triangleCount
|
|
232
|
+
|| control.byteLength < 20 || highDetail.byteLength < 20) return null;
|
|
233
|
+
const landmarkIds = new Set(identityLandmarks.map((landmark) => landmark.id));
|
|
234
|
+
if (landmarkIds.size < 3 || landmarkIds.size !== identityLandmarks.length) return null;
|
|
235
|
+
const semanticRegions = semanticRegionsOption(value.semanticRegions, landmarkIds);
|
|
236
|
+
const maximumDisplacementMetres = Number(value.editEnvelope?.maximumDisplacementMetres);
|
|
237
|
+
const maximumVariationStrength = Number(value.editEnvelope?.maximumVariationStrength);
|
|
238
|
+
const semanticRegionIds = new Set(semanticRegions.map((region) => region.id));
|
|
239
|
+
const semanticSelectors = new Set(semanticRegions.map((region) => `${region.selector.attribute}:${region.selector.value}`));
|
|
240
|
+
const supportToleranceMetres = Math.max(...control.dimensionsMetres) * 1e-5;
|
|
241
|
+
const highCorrespondenceToleranceMetres = Math.max(...control.dimensionsMetres) * 0.05;
|
|
242
|
+
if (semanticRegions.length < 3 || semanticRegionIds.size !== semanticRegions.length
|
|
243
|
+
|| semanticSelectors.size !== semanticRegions.length
|
|
244
|
+
|| !semanticRegions.some((region) => region.role === 'base-support')
|
|
245
|
+
|| Math.abs(control.boundsMetres.min[1]) > Math.max(1e-6, supportToleranceMetres)
|
|
246
|
+
|| ['min', 'max'].some((key) => control.boundsMetres[key].some((entry, axis) => (
|
|
247
|
+
Math.abs(entry - highDetail.boundsMetres[key][axis]) > highCorrespondenceToleranceMetres
|
|
248
|
+
)))
|
|
249
|
+
|| !Number.isFinite(maximumDisplacementMetres)
|
|
250
|
+
|| maximumDisplacementMetres <= 0 || !Number.isFinite(maximumVariationStrength)
|
|
251
|
+
|| maximumVariationStrength <= 0 || maximumVariationStrength > 1) return null;
|
|
252
|
+
return {
|
|
253
|
+
authority: 'toonlab-editable-control-plus-retained-high',
|
|
254
|
+
control,
|
|
255
|
+
editEnvelope: {
|
|
256
|
+
maximumDisplacementMetres,
|
|
257
|
+
maximumVariationStrength,
|
|
258
|
+
mode: 'bounded-source-space',
|
|
259
|
+
},
|
|
260
|
+
highDetail,
|
|
261
|
+
schema: 'toonlab/c8-custom-mesh-source',
|
|
262
|
+
semanticRegions,
|
|
263
|
+
version: 1,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export function isRockSourceMeshReference(reference) {
|
|
268
|
+
return reference?.sourceMode === 'mesh-template'
|
|
269
|
+
|| Boolean(reference?.sourceMode === 'c8-custom-mesh' && reference.customMeshSource);
|
|
270
|
+
}
|
|
271
|
+
|
|
88
272
|
function bytesToBase64(bytes) {
|
|
89
273
|
let binary = '';
|
|
90
274
|
const chunkSize = 0x8000;
|
|
@@ -101,9 +285,13 @@ function base64ToBytes(value) {
|
|
|
101
285
|
return bytes;
|
|
102
286
|
}
|
|
103
287
|
|
|
104
|
-
function unpackMeshEdits(value) {
|
|
105
|
-
if (
|
|
106
|
-
if (typeof value
|
|
288
|
+
function unpackMeshEdits(value, { strict = false } = {}) {
|
|
289
|
+
if (value === null || value === undefined) return [];
|
|
290
|
+
if (typeof value !== 'object' || value.encoding !== ROCKGEN_MESH_EDIT_ENCODING
|
|
291
|
+
|| typeof value.data !== 'string' || value.data.length === 0) {
|
|
292
|
+
if (strict) throw new Error('C8 packed mesh edits require the exact supported encoding and non-empty data.');
|
|
293
|
+
return [];
|
|
294
|
+
}
|
|
107
295
|
try {
|
|
108
296
|
const bytes = base64ToBytes(value.data);
|
|
109
297
|
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
@@ -120,35 +308,65 @@ function unpackMeshEdits(value) {
|
|
|
120
308
|
offset += 4;
|
|
121
309
|
return result;
|
|
122
310
|
};
|
|
123
|
-
const operationCount =
|
|
311
|
+
const operationCount = readUint32();
|
|
312
|
+
if (value.operationCount !== undefined && Number(value.operationCount) !== operationCount) {
|
|
313
|
+
throw new Error('operation count metadata does not match the packed stream');
|
|
314
|
+
}
|
|
124
315
|
const edits = [];
|
|
125
|
-
let
|
|
126
|
-
for (let operationIndex = 0; operationIndex < operationCount
|
|
316
|
+
let totalDeltaCount = 0;
|
|
317
|
+
for (let operationIndex = 0; operationIndex < operationCount; operationIndex += 1) {
|
|
127
318
|
const meshIndex = readUint32();
|
|
128
319
|
const storedDeltaCount = readUint32();
|
|
129
|
-
|
|
320
|
+
if (storedDeltaCount === 0) throw new Error('delta count is empty');
|
|
130
321
|
const deltas = [];
|
|
131
322
|
for (let deltaIndex = 0; deltaIndex < storedDeltaCount; deltaIndex += 1) {
|
|
132
323
|
const delta = [readUint32(), readFloat32(), readFloat32(), readFloat32()];
|
|
133
|
-
if (
|
|
324
|
+
if (!delta.slice(1).every(Number.isFinite)) throw new Error('packed delta contains a non-finite value');
|
|
325
|
+
deltas.push(delta);
|
|
134
326
|
}
|
|
135
|
-
|
|
136
|
-
|
|
327
|
+
edits.push({ deltas, meshIndex });
|
|
328
|
+
totalDeltaCount += deltas.length;
|
|
329
|
+
}
|
|
330
|
+
if (value.deltaCount !== undefined && Number(value.deltaCount) !== totalDeltaCount) {
|
|
331
|
+
throw new Error('delta count metadata does not match the packed stream');
|
|
332
|
+
}
|
|
333
|
+
if (offset !== view.byteLength) {
|
|
334
|
+
throw new Error('packed mesh-edit stream contains trailing bytes');
|
|
137
335
|
}
|
|
138
336
|
return edits;
|
|
139
|
-
} catch {
|
|
337
|
+
} catch (error) {
|
|
338
|
+
if (strict) throw new Error(`Invalid C8 packed mesh edits: ${error.message}`);
|
|
140
339
|
return [];
|
|
141
340
|
}
|
|
142
341
|
}
|
|
143
342
|
|
|
144
|
-
function meshEditsOption(value, packedValue = null) {
|
|
145
|
-
|
|
343
|
+
function meshEditsOption(value, packedValue = null, { strict = false } = {}) {
|
|
344
|
+
if (strict && Array.isArray(value) && packedValue) {
|
|
345
|
+
throw new Error('C8 mesh edits must use either the unpacked or packed representation, never both.');
|
|
346
|
+
}
|
|
347
|
+
const source = Array.isArray(value) ? value : unpackMeshEdits(packedValue, { strict });
|
|
146
348
|
if (!Array.isArray(source)) return [];
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
349
|
+
if (strict) {
|
|
350
|
+
return source.map((entry) => {
|
|
351
|
+
if (!entry || typeof entry !== 'object' || !Array.isArray(entry.deltas) || entry.deltas.length === 0
|
|
352
|
+
|| !Number.isInteger(entry.meshIndex) || entry.meshIndex < 0) {
|
|
353
|
+
throw new Error('C8 mesh edits require a non-negative integer mesh index and non-empty delta array.');
|
|
354
|
+
}
|
|
355
|
+
const deltas = entry.deltas.map((delta) => {
|
|
356
|
+
if (!Array.isArray(delta) || delta.length !== 4
|
|
357
|
+
|| !Number.isInteger(delta[0]) || delta[0] < 0
|
|
358
|
+
|| !delta.slice(1).every(Number.isFinite)
|
|
359
|
+
|| (delta[1] === 0 && delta[2] === 0 && delta[3] === 0)) {
|
|
360
|
+
throw new Error('C8 mesh edits require exact [vertexIndex, x, y, z] finite non-zero deltas.');
|
|
361
|
+
}
|
|
362
|
+
return [delta[0], Number(delta[1]), Number(delta[2]), Number(delta[3])];
|
|
363
|
+
});
|
|
364
|
+
return { deltas, meshIndex: entry.meshIndex };
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
return source.flatMap((entry) => {
|
|
150
368
|
if (!entry || typeof entry !== 'object' || !Array.isArray(entry.deltas)) return [];
|
|
151
|
-
const deltas = entry.deltas.
|
|
369
|
+
const deltas = entry.deltas.flatMap((delta) => {
|
|
152
370
|
if (!Array.isArray(delta) || delta.length < 4) return [];
|
|
153
371
|
const vertexIndex = Math.round(Number(delta[0]));
|
|
154
372
|
const x = Number(delta[1]);
|
|
@@ -159,7 +377,6 @@ function meshEditsOption(value, packedValue = null) {
|
|
|
159
377
|
return [[vertexIndex, x, y, z]];
|
|
160
378
|
});
|
|
161
379
|
if (deltas.length === 0) return [];
|
|
162
|
-
remaining -= deltas.length;
|
|
163
380
|
return [{
|
|
164
381
|
deltas,
|
|
165
382
|
meshIndex: Math.max(0, Math.round(Number(entry.meshIndex) || 0)),
|
|
@@ -167,6 +384,476 @@ function meshEditsOption(value, packedValue = null) {
|
|
|
167
384
|
});
|
|
168
385
|
}
|
|
169
386
|
|
|
387
|
+
function meshCutsOption(value, { strict = false } = {}) {
|
|
388
|
+
if (!Array.isArray(value)) return [];
|
|
389
|
+
return value.flatMap((entry) => {
|
|
390
|
+
if (!entry || typeof entry !== 'object') {
|
|
391
|
+
if (strict) throw new Error('Drill cuts must be objects.');
|
|
392
|
+
return [];
|
|
393
|
+
}
|
|
394
|
+
const point = Array.isArray(entry.point) ? entry.point.slice(0, 3).map(Number) : [];
|
|
395
|
+
const normal = Array.isArray(entry.normal) ? entry.normal.slice(0, 3).map(Number) : [];
|
|
396
|
+
const radius = Number(entry.radius);
|
|
397
|
+
const roughness = Math.min(Math.max(Number(entry.roughness) || 0, 0), 1);
|
|
398
|
+
const seed = Math.round(Number(entry.seed) || 0) >>> 0;
|
|
399
|
+
const depth = Number(entry.depth);
|
|
400
|
+
const meshIndex = Math.round(Number(entry.meshIndex));
|
|
401
|
+
const valid = point.length === 3 && normal.length === 3
|
|
402
|
+
&& [...point, ...normal, radius, depth, meshIndex].every(Number.isFinite)
|
|
403
|
+
&& Math.hypot(...normal) > 1e-6
|
|
404
|
+
&& meshIndex >= 0 && radius >= 0.01
|
|
405
|
+
&& depth >= 0.01;
|
|
406
|
+
if (!valid) {
|
|
407
|
+
if (strict) throw new Error('Drill cuts require finite point/normal vectors, mesh index, radius, and depth.');
|
|
408
|
+
return [];
|
|
409
|
+
}
|
|
410
|
+
const length = Math.hypot(...normal);
|
|
411
|
+
return [{
|
|
412
|
+
depth,
|
|
413
|
+
meshIndex,
|
|
414
|
+
normal: normal.map((component) => component / length),
|
|
415
|
+
point,
|
|
416
|
+
radius,
|
|
417
|
+
roughness,
|
|
418
|
+
seed,
|
|
419
|
+
through: Boolean(entry.through),
|
|
420
|
+
}];
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const ROCKGEN_MESH_SNAPSHOT_FLOAT_ENCODING = 'f32-base64-le';
|
|
425
|
+
const ROCKGEN_MESH_SNAPSHOT_INDEX_ENCODING = 'u32-base64-le';
|
|
426
|
+
|
|
427
|
+
function unpackMeshSnapshots(value, { strict = false } = {}) {
|
|
428
|
+
if (value === null || value === undefined) return [];
|
|
429
|
+
const reject = (message) => {
|
|
430
|
+
if (strict) throw new Error(message);
|
|
431
|
+
return [];
|
|
432
|
+
};
|
|
433
|
+
if (!Array.isArray(value)) return reject('Packed mesh snapshots must be an array.');
|
|
434
|
+
try {
|
|
435
|
+
return value.map((entry) => {
|
|
436
|
+
const attributes = {};
|
|
437
|
+
for (const [name, attribute] of Object.entries(entry?.attributes ?? {})) {
|
|
438
|
+
if (attribute?.encoding !== ROCKGEN_MESH_SNAPSHOT_FLOAT_ENCODING
|
|
439
|
+
|| typeof attribute.data !== 'string' || !Number.isInteger(attribute.count)
|
|
440
|
+
|| !Number.isInteger(attribute.itemSize) || attribute.count <= 0 || attribute.itemSize <= 0) {
|
|
441
|
+
throw new Error(`packed attribute ${name || '(unnamed)'} is invalid`);
|
|
442
|
+
}
|
|
443
|
+
const bytes = base64ToBytes(attribute.data);
|
|
444
|
+
if (bytes.byteLength !== attribute.count * attribute.itemSize * 4) {
|
|
445
|
+
throw new Error(`packed attribute ${name} has the wrong byte length`);
|
|
446
|
+
}
|
|
447
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
448
|
+
const data = Array.from(
|
|
449
|
+
{ length: attribute.count * attribute.itemSize },
|
|
450
|
+
(_, index) => view.getFloat32(index * 4, true),
|
|
451
|
+
);
|
|
452
|
+
attributes[name] = {
|
|
453
|
+
data,
|
|
454
|
+
itemSize: attribute.itemSize,
|
|
455
|
+
normalized: Boolean(attribute.normalized),
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
let index = null;
|
|
459
|
+
if (entry?.index !== null && entry?.index !== undefined) {
|
|
460
|
+
if (entry.index.encoding !== ROCKGEN_MESH_SNAPSHOT_INDEX_ENCODING
|
|
461
|
+
|| typeof entry.index.data !== 'string' || !Number.isInteger(entry.index.count)
|
|
462
|
+
|| entry.index.count <= 0) throw new Error('packed indices are invalid');
|
|
463
|
+
const bytes = base64ToBytes(entry.index.data);
|
|
464
|
+
if (bytes.byteLength !== entry.index.count * 4) throw new Error('packed indices have the wrong byte length');
|
|
465
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
466
|
+
index = Array.from({ length: entry.index.count }, (_, offset) => view.getUint32(offset * 4, true));
|
|
467
|
+
}
|
|
468
|
+
return {
|
|
469
|
+
attributes,
|
|
470
|
+
groups: entry?.groups,
|
|
471
|
+
index,
|
|
472
|
+
meshIndex: entry?.meshIndex,
|
|
473
|
+
...(entry?.variationOrigin ? { variationOrigin: variationOriginOption(entry.variationOrigin) } : {}),
|
|
474
|
+
...(entry?.fillPatches?.length ? { fillPatches: entry.fillPatches.map((patch) => ({
|
|
475
|
+
point: patch.point,
|
|
476
|
+
geometry: unpackMeshSnapshots([{ ...patch.geometry, fillPatches: [] }], { strict })[0],
|
|
477
|
+
})) } : {}),
|
|
478
|
+
};
|
|
479
|
+
});
|
|
480
|
+
} catch (error) {
|
|
481
|
+
return reject(`Invalid packed mesh snapshot: ${error.message}.`);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function variationSettingsOption(value = {}) {
|
|
486
|
+
const settings = {};
|
|
487
|
+
if (value?.version === 2) settings.version = 2;
|
|
488
|
+
if (value?.scope !== undefined) settings.scope = value.scope === 'component' ? 'component' : 'whole';
|
|
489
|
+
if (value?.anchorBase !== undefined) settings.anchorBase = value.anchorBase !== false;
|
|
490
|
+
if (value?.locks) settings.locks = {};
|
|
491
|
+
for (const key of ['width', 'height', 'depth', 'leanX', 'leanZ', 'twist', 'taper', 'bulge', 'noiseAmplitude', 'noiseFrequency']) {
|
|
492
|
+
if (Number.isFinite(value?.[key])) settings[key] = value[key];
|
|
493
|
+
if (value?.locks?.[key]) settings.locks[key] = true;
|
|
494
|
+
}
|
|
495
|
+
return settings;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function variationOriginOption(value) {
|
|
499
|
+
return value ? { strength: Number(value.strength) || 0, seed: Math.round(Number(value.seed) || 0) >>> 0,
|
|
500
|
+
settings: variationSettingsOption(value.settings) } : undefined;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function meshSnapshotsOption(value, packedValue = null, { strict = false } = {}) {
|
|
504
|
+
const source = Array.isArray(value) ? value : unpackMeshSnapshots(packedValue, { strict });
|
|
505
|
+
if (!Array.isArray(source)) return [];
|
|
506
|
+
return source.flatMap((entry) => {
|
|
507
|
+
const reject = (message) => {
|
|
508
|
+
if (strict) throw new Error(message);
|
|
509
|
+
return [];
|
|
510
|
+
};
|
|
511
|
+
if (!entry || typeof entry !== 'object' || !Number.isInteger(entry.meshIndex) || entry.meshIndex < 0
|
|
512
|
+
|| !entry.attributes || typeof entry.attributes !== 'object') {
|
|
513
|
+
return reject('Mesh snapshots require a non-negative mesh index and geometry attributes.');
|
|
514
|
+
}
|
|
515
|
+
const attributes = {};
|
|
516
|
+
for (const [name, attribute] of Object.entries(entry.attributes)) {
|
|
517
|
+
const itemSize = Math.round(Number(attribute?.itemSize));
|
|
518
|
+
const data = Array.isArray(attribute?.data) ? attribute.data.map(Number) : [];
|
|
519
|
+
if (!name || itemSize < 1 || itemSize > 4 || data.length === 0
|
|
520
|
+
|| data.length % itemSize !== 0 || !data.every(Number.isFinite)) {
|
|
521
|
+
return reject(`Mesh snapshot attribute ${name || '(unnamed)'} is invalid.`);
|
|
522
|
+
}
|
|
523
|
+
attributes[name] = { data, itemSize, normalized: Boolean(attribute.normalized) };
|
|
524
|
+
}
|
|
525
|
+
const position = attributes.position;
|
|
526
|
+
if (!position || position.itemSize !== 3) return reject('Mesh snapshots require a vec3 position attribute.');
|
|
527
|
+
const vertexCount = position.data.length / 3;
|
|
528
|
+
const index = entry.index === null || entry.index === undefined
|
|
529
|
+
? null
|
|
530
|
+
: Array.isArray(entry.index) ? entry.index.map(Number) : [];
|
|
531
|
+
if (index && (index.length === 0 || index.some((value) => (
|
|
532
|
+
!Number.isInteger(value) || value < 0 || value >= vertexCount
|
|
533
|
+
)))) return reject('Mesh snapshot indices must reference existing vertices.');
|
|
534
|
+
const groups = Array.isArray(entry.groups) ? entry.groups.flatMap((group) => {
|
|
535
|
+
const start = Math.round(Number(group?.start));
|
|
536
|
+
const count = Math.round(Number(group?.count));
|
|
537
|
+
const materialIndex = Math.max(0, Math.round(Number(group?.materialIndex) || 0));
|
|
538
|
+
return start >= 0 && count > 0 ? [{ count, materialIndex, start }] : [];
|
|
539
|
+
}) : [];
|
|
540
|
+
const fillPatches = (Array.isArray(entry.fillPatches) ? entry.fillPatches : []).flatMap((patch) => {
|
|
541
|
+
if (!Array.isArray(patch?.point) || patch.point.length !== 3 || !patch.point.every(Number.isFinite)) {
|
|
542
|
+
return reject('Fill patches require a finite three-dimensional point.');
|
|
543
|
+
}
|
|
544
|
+
const geometry = meshSnapshotsOption([{ ...patch.geometry, fillPatches: [] }], null, { strict })[0];
|
|
545
|
+
return geometry ? [{ point: [...patch.point], geometry }] : [];
|
|
546
|
+
});
|
|
547
|
+
return [{ attributes, groups, index, meshIndex: entry.meshIndex,
|
|
548
|
+
...(entry.variationOrigin ? { variationOrigin: variationOriginOption(entry.variationOrigin) } : {}),
|
|
549
|
+
...(fillPatches.length ? { fillPatches } : {}),
|
|
550
|
+
}];
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function packMeshSnapshots(value) {
|
|
555
|
+
return meshSnapshotsOption(value).map((entry) => {
|
|
556
|
+
const attributes = Object.fromEntries(Object.entries(entry.attributes).map(([name, attribute]) => {
|
|
557
|
+
const bytes = new Uint8Array(attribute.data.length * 4);
|
|
558
|
+
const view = new DataView(bytes.buffer);
|
|
559
|
+
for (let index = 0; index < attribute.data.length; index += 1) {
|
|
560
|
+
view.setFloat32(index * 4, attribute.data[index], true);
|
|
561
|
+
}
|
|
562
|
+
return [name, {
|
|
563
|
+
count: attribute.data.length / attribute.itemSize,
|
|
564
|
+
data: bytesToBase64(bytes),
|
|
565
|
+
encoding: ROCKGEN_MESH_SNAPSHOT_FLOAT_ENCODING,
|
|
566
|
+
itemSize: attribute.itemSize,
|
|
567
|
+
normalized: attribute.normalized,
|
|
568
|
+
}];
|
|
569
|
+
}));
|
|
570
|
+
let index = null;
|
|
571
|
+
if (entry.index) {
|
|
572
|
+
const bytes = new Uint8Array(entry.index.length * 4);
|
|
573
|
+
const view = new DataView(bytes.buffer);
|
|
574
|
+
for (let offset = 0; offset < entry.index.length; offset += 1) {
|
|
575
|
+
view.setUint32(offset * 4, entry.index[offset], true);
|
|
576
|
+
}
|
|
577
|
+
index = {
|
|
578
|
+
count: entry.index.length,
|
|
579
|
+
data: bytesToBase64(bytes),
|
|
580
|
+
encoding: ROCKGEN_MESH_SNAPSHOT_INDEX_ENCODING,
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
return {
|
|
584
|
+
attributes,
|
|
585
|
+
groups: entry.groups,
|
|
586
|
+
index,
|
|
587
|
+
meshIndex: entry.meshIndex,
|
|
588
|
+
...(entry.variationOrigin ? { variationOrigin: variationOriginOption(entry.variationOrigin) } : {}),
|
|
589
|
+
...(entry.fillPatches?.length ? { fillPatches: entry.fillPatches.map((patch) => ({
|
|
590
|
+
point: patch.point,
|
|
591
|
+
geometry: packMeshSnapshots([patch.geometry])[0],
|
|
592
|
+
})) } : {}),
|
|
593
|
+
};
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
function meshOperationOrderOption(value, meshEdits, meshCuts, { strict = false } = {}) {
|
|
598
|
+
const fallback = [
|
|
599
|
+
...meshEdits.map((_, index) => ({ index, type: 'sculpt' })),
|
|
600
|
+
...meshCuts.map((_, index) => ({ index, type: 'drill' })),
|
|
601
|
+
];
|
|
602
|
+
if (!Array.isArray(value)) return fallback;
|
|
603
|
+
const seen = new Set();
|
|
604
|
+
const normalized = [];
|
|
605
|
+
for (const entry of value) {
|
|
606
|
+
const type = entry?.type === 'drill' ? 'drill' : entry?.type === 'sculpt' ? 'sculpt' : null;
|
|
607
|
+
const index = Math.round(Number(entry?.index));
|
|
608
|
+
const limit = type === 'drill' ? meshCuts.length : meshEdits.length;
|
|
609
|
+
const key = `${type}:${index}`;
|
|
610
|
+
if (!type || !Number.isFinite(index) || index < 0 || index >= limit || seen.has(key)) {
|
|
611
|
+
if (strict) throw new Error('Mesh operation order contains an invalid or duplicate operation reference.');
|
|
612
|
+
return fallback;
|
|
613
|
+
}
|
|
614
|
+
seen.add(key);
|
|
615
|
+
normalized.push({ index, type });
|
|
616
|
+
}
|
|
617
|
+
if (normalized.length !== fallback.length) {
|
|
618
|
+
if (strict) throw new Error('Mesh operation order must reference every sculpt and drill operation exactly once.');
|
|
619
|
+
return fallback;
|
|
620
|
+
}
|
|
621
|
+
return normalized;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
function stableJsonValue(value) {
|
|
625
|
+
if (Array.isArray(value)) return `[${value.map(stableJsonValue).join(',')}]`;
|
|
626
|
+
if (value && typeof value === 'object') {
|
|
627
|
+
return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableJsonValue(value[key])}`).join(',')}}`;
|
|
628
|
+
}
|
|
629
|
+
return JSON.stringify(value);
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
function fnv1a64(value) {
|
|
633
|
+
let hash = 0xcbf29ce484222325n;
|
|
634
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
635
|
+
hash ^= BigInt(value.charCodeAt(index));
|
|
636
|
+
hash = BigInt.asUintN(64, hash * 0x100000001b3n);
|
|
637
|
+
}
|
|
638
|
+
return hash.toString(16).padStart(16, '0');
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
/** Deterministic browser-safe identity for the complete saved C8 control/high/semantic authority. */
|
|
642
|
+
export function createC8ReferenceAuthoritySourceId(reference) {
|
|
643
|
+
if (reference?.sourceMode !== 'c8-custom-mesh' || !reference.customMeshSource) return null;
|
|
644
|
+
return `c8-authority-v1:fnv1a64:${fnv1a64(stableJsonValue(reference.customMeshSource))}`;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
/** Deterministic browser-safe identity for the exact live C8 control geometry. */
|
|
648
|
+
export function createC8ReferenceGeometrySourceId(reference) {
|
|
649
|
+
if (reference?.sourceMode !== 'c8-custom-mesh' || !reference.customMeshSource?.control) return null;
|
|
650
|
+
const payload = {
|
|
651
|
+
authoritySourceId: createC8ReferenceAuthoritySourceId(reference),
|
|
652
|
+
controlContentHash: reference.customMeshSource.control.contentHash,
|
|
653
|
+
controlTopologyHash: reference.customMeshSource.control.topologyHash,
|
|
654
|
+
meshCuts: reference.meshCuts ?? [],
|
|
655
|
+
meshEdits: reference.meshEdits ?? [],
|
|
656
|
+
meshOperationOrder: reference.meshOperationOrder ?? [],
|
|
657
|
+
meshSnapshots: reference.meshSnapshots ?? [],
|
|
658
|
+
variation: Number(reference.variation) || 0,
|
|
659
|
+
variationSeed: Math.round(Number(reference.variationSeed) || 0) >>> 0,
|
|
660
|
+
...(reference.variationSettings ? { variationSettings: variationSettingsOption(reference.variationSettings) } : {}),
|
|
661
|
+
};
|
|
662
|
+
return `c8-geometry-v2:fnv1a64:${fnv1a64(stableJsonValue(payload))}`;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/** Surface derivative identity, separate from geometry and explicit about the C7 recipe. */
|
|
666
|
+
export function createC8ReferenceSurfaceSourceId(reference) {
|
|
667
|
+
if (reference?.sourceMode !== 'c8-custom-mesh') return null;
|
|
668
|
+
const payload = {
|
|
669
|
+
authoritySourceId: createC8ReferenceAuthoritySourceId(reference),
|
|
670
|
+
geometrySourceId: reference.geometrySourceId ?? createC8ReferenceGeometrySourceId(reference),
|
|
671
|
+
geology: String(reference.geology ?? ''),
|
|
672
|
+
surfaceMode: reference.surfaceMode === 'generated' ? 'generated' : 'source',
|
|
673
|
+
surfacePackage: reference.surfacePackage ?? null,
|
|
674
|
+
topFinish: reference.topFinish ?? 'source',
|
|
675
|
+
};
|
|
676
|
+
return `c8-surface-v1:fnv1a64:${fnv1a64(stableJsonValue(payload))}`;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
function c8SurfaceReprojectionOption(value, {
|
|
680
|
+
authoritySourceId,
|
|
681
|
+
controlContentHash,
|
|
682
|
+
geometrySourceId,
|
|
683
|
+
geology,
|
|
684
|
+
meshEditCount,
|
|
685
|
+
sourceId,
|
|
686
|
+
sourceRevision,
|
|
687
|
+
surfacePackage,
|
|
688
|
+
surfaceSourceId,
|
|
689
|
+
}) {
|
|
690
|
+
if (value === null || value === undefined) return null;
|
|
691
|
+
if (!value || typeof value !== 'object'
|
|
692
|
+
|| value.schema !== 'toonlab/c8-surface-reprojection'
|
|
693
|
+
|| Number(value.version) !== 1
|
|
694
|
+
|| value.method !== 'deterministic-metre-triplanar-map-reprojection'
|
|
695
|
+
|| value.trueHighToLowBake !== false) {
|
|
696
|
+
throw new Error('C8 surface reprojection metadata must use the deterministic metre-triplanar schema and can never claim a true high-to-low bake.');
|
|
697
|
+
}
|
|
698
|
+
if (Number(value.sourceRevision) !== sourceRevision) {
|
|
699
|
+
throw new Error('C8 surface reprojection metadata is stale for the saved source revision.');
|
|
700
|
+
}
|
|
701
|
+
if (value.controlContentHash && value.controlContentHash !== controlContentHash) {
|
|
702
|
+
throw new Error('C8 surface reprojection metadata is stale for the admitted control GLB.');
|
|
703
|
+
}
|
|
704
|
+
if (value.geometrySourceId && value.geometrySourceId !== geometrySourceId) {
|
|
705
|
+
throw new Error('C8 surface reprojection metadata is stale for the saved geometry content ID.');
|
|
706
|
+
}
|
|
707
|
+
if (value.authoritySourceId && value.authoritySourceId !== authoritySourceId) {
|
|
708
|
+
throw new Error('C8 surface reprojection metadata is stale for the complete control/high authority.');
|
|
709
|
+
}
|
|
710
|
+
if (value.surfaceSourceId && value.surfaceSourceId !== surfaceSourceId) {
|
|
711
|
+
throw new Error('C8 surface reprojection metadata is stale for the saved surface recipe.');
|
|
712
|
+
}
|
|
713
|
+
const dimensionsMetres = vector3Option(value.dimensionsMetres ?? value.projection?.dimensionsMetres, [0, 0, 0]);
|
|
714
|
+
if (dimensionsMetres.some((entry) => entry <= 0)) throw new Error('C8 surface reprojection requires positive metre dimensions.');
|
|
715
|
+
const projection = value.projection;
|
|
716
|
+
const scaleMetres = Number(projection?.scaleMetres);
|
|
717
|
+
const axisScale = vector3Option(projection?.axisScale, [0, 0, 0]);
|
|
718
|
+
if (projection?.mode !== 'world-metre-triplanar-isotropic'
|
|
719
|
+
|| !Number.isFinite(scaleMetres) || scaleMetres <= 0
|
|
720
|
+
|| axisScale.some((entry) => Math.abs(entry - scaleMetres) > 1e-9)) {
|
|
721
|
+
throw new Error('C8 surface reprojection must remain metre-isotropic on all three axes.');
|
|
722
|
+
}
|
|
723
|
+
const expectedProjection = resolveC7Projection({ dimensionsMetres, geology });
|
|
724
|
+
if (stableJsonValue(projection) !== stableJsonValue(expectedProjection)) {
|
|
725
|
+
throw new Error('C8 surface reprojection projection must be deterministically derived from its saved metre dimensions and geology.');
|
|
726
|
+
}
|
|
727
|
+
const mapResolution = Math.round(Number(value.mapResolution));
|
|
728
|
+
if (!Number.isInteger(mapResolution) || mapResolution < 32 || mapResolution > 2048) {
|
|
729
|
+
throw new Error('C8 surface reprojection resolution must be between 32 and 2048.');
|
|
730
|
+
}
|
|
731
|
+
const resolvedGeology = String(value.geology ?? '').trim();
|
|
732
|
+
if (!resolvedGeology || resolvedGeology !== geology) throw new Error('C8 surface reprojection geology does not match its saved surface package.');
|
|
733
|
+
const resolvedSeed = Math.round(Number(value.seed) || 0) >>> 0;
|
|
734
|
+
if (!surfacePackage || surfacePackage.geology !== resolvedGeology
|
|
735
|
+
|| surfacePackage.mapResolution !== mapResolution || surfacePackage.seed !== resolvedSeed) {
|
|
736
|
+
throw new Error('C8 surface reprojection must exactly match the saved C7 geology surface package.');
|
|
737
|
+
}
|
|
738
|
+
const record = {
|
|
739
|
+
authoritySourceId,
|
|
740
|
+
controlContentHash,
|
|
741
|
+
dimensionsMetres,
|
|
742
|
+
editOperationCount: meshEditCount,
|
|
743
|
+
geology: resolvedGeology,
|
|
744
|
+
geometrySourceId,
|
|
745
|
+
mapResolution,
|
|
746
|
+
method: 'deterministic-metre-triplanar-map-reprojection',
|
|
747
|
+
projection: structuredClone(expectedProjection),
|
|
748
|
+
schema: 'toonlab/c8-surface-reprojection',
|
|
749
|
+
seed: resolvedSeed,
|
|
750
|
+
sourceContentId: geometrySourceId,
|
|
751
|
+
sourceId,
|
|
752
|
+
sourceRevision,
|
|
753
|
+
surfaceSourceId,
|
|
754
|
+
trueHighToLowBake: false,
|
|
755
|
+
version: 1,
|
|
756
|
+
};
|
|
757
|
+
return { ...record, reprojectionContentId: `c8-reprojection-v1:fnv1a64:${fnv1a64(stableJsonValue(record))}` };
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
function portableDerivedArtifactState(value, {
|
|
761
|
+
authoritySourceId,
|
|
762
|
+
customMeshSource,
|
|
763
|
+
geometrySourceId,
|
|
764
|
+
sourceRevision,
|
|
765
|
+
surfaceReprojection,
|
|
766
|
+
surfaceSourceId,
|
|
767
|
+
}) {
|
|
768
|
+
const source = value && typeof value === 'object' ? value : {};
|
|
769
|
+
const result = {};
|
|
770
|
+
for (const role of ['collision', 'geometricResidual', 'lods', 'pbrBake', 'runtimePackage']) {
|
|
771
|
+
const entry = source[role] && typeof source[role] === 'object' ? source[role] : {};
|
|
772
|
+
const status = entry.status === 'current' ? 'current' : 'stale';
|
|
773
|
+
if (status === 'current') {
|
|
774
|
+
const boundGeometryId = entry.geometrySourceId ?? entry.sourceGeometryContentId ?? entry.sourceContentId;
|
|
775
|
+
if (boundGeometryId !== geometrySourceId || !contentHashOption(entry.contentHash)
|
|
776
|
+
|| entry.authoritySourceId !== authoritySourceId
|
|
777
|
+
|| Number(entry.sourceRevision) !== sourceRevision) {
|
|
778
|
+
throw new Error(`Current C8 ${role} metadata must carry a content hash bound to the exact geometry source ID.`);
|
|
779
|
+
}
|
|
780
|
+
if (['pbrBake', 'runtimePackage'].includes(role) && entry.surfaceSourceId !== surfaceSourceId) {
|
|
781
|
+
throw new Error(`Current C8 ${role} metadata must be bound to the exact surface recipe ID.`);
|
|
782
|
+
}
|
|
783
|
+
if (role === 'pbrBake' && (entry.method !== 'blender-high-to-low-bake'
|
|
784
|
+
|| entry.trueHighToLowBake !== true
|
|
785
|
+
|| entry.highDetailContentHash !== customMeshSource.highDetail.contentHash)) {
|
|
786
|
+
throw new Error('A current C8 PBR bake must be an explicit Blender high-to-low bake bound to the retained high-detail GLB.');
|
|
787
|
+
}
|
|
788
|
+
if (role === 'geometricResidual' && (entry.method !== 'blender-signed-high-to-lod-residual'
|
|
789
|
+
|| entry.highDetailContentHash !== customMeshSource.highDetail.contentHash)) {
|
|
790
|
+
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.');
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
result[role] = {
|
|
794
|
+
authoritySourceId,
|
|
795
|
+
contentHash: contentHashOption(entry.contentHash),
|
|
796
|
+
geometrySourceId,
|
|
797
|
+
reason: String(entry.reason ?? (status === 'current' ? 'verified-derived-artifact' : 'geometry-source-not-baked')),
|
|
798
|
+
sourceRevision,
|
|
799
|
+
status,
|
|
800
|
+
...(['pbrBake', 'runtimePackage'].includes(role) ? { surfaceSourceId } : {}),
|
|
801
|
+
...(role === 'pbrBake' && status === 'current' ? {
|
|
802
|
+
highDetailContentHash: customMeshSource.highDetail.contentHash,
|
|
803
|
+
method: 'blender-high-to-low-bake',
|
|
804
|
+
trueHighToLowBake: true,
|
|
805
|
+
} : {}),
|
|
806
|
+
...(role === 'geometricResidual' && status === 'current' ? {
|
|
807
|
+
highDetailContentHash: customMeshSource.highDetail.contentHash,
|
|
808
|
+
method: 'blender-signed-high-to-lod-residual',
|
|
809
|
+
} : {}),
|
|
810
|
+
};
|
|
811
|
+
}
|
|
812
|
+
result.surfaceReprojection = surfaceReprojection
|
|
813
|
+
? {
|
|
814
|
+
authoritySourceId,
|
|
815
|
+
contentHash: surfaceReprojection.reprojectionContentId,
|
|
816
|
+
geometrySourceId,
|
|
817
|
+
method: surfaceReprojection.method,
|
|
818
|
+
sourceRevision,
|
|
819
|
+
status: 'current',
|
|
820
|
+
surfaceSourceId,
|
|
821
|
+
trueHighToLowBake: false,
|
|
822
|
+
}
|
|
823
|
+
: {
|
|
824
|
+
authoritySourceId,
|
|
825
|
+
contentHash: null,
|
|
826
|
+
geometrySourceId,
|
|
827
|
+
reason: 'geometry-source-not-reprojected',
|
|
828
|
+
sourceRevision,
|
|
829
|
+
status: 'stale',
|
|
830
|
+
surfaceSourceId,
|
|
831
|
+
trueHighToLowBake: false,
|
|
832
|
+
};
|
|
833
|
+
return result;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
function validateC8MeshEditSequence(meshEdits, customMeshSource) {
|
|
837
|
+
const meshVertexCounts = customMeshSource.control.geometryAudit.meshVertexCounts;
|
|
838
|
+
for (const edit of meshEdits) {
|
|
839
|
+
const meshVertexCount = meshVertexCounts[edit.meshIndex];
|
|
840
|
+
if (!Number.isInteger(meshVertexCount)) {
|
|
841
|
+
throw new Error(`C8 sparse sculpt references missing control mesh ${edit.meshIndex}.`);
|
|
842
|
+
}
|
|
843
|
+
const operationVertices = new Set();
|
|
844
|
+
for (const [vertexIndex, x, y, z] of edit.deltas) {
|
|
845
|
+
if (vertexIndex >= meshVertexCount) {
|
|
846
|
+
throw new Error(`C8 sparse sculpt references missing vertex ${vertexIndex} on control mesh ${edit.meshIndex}.`);
|
|
847
|
+
}
|
|
848
|
+
if (operationVertices.has(vertexIndex)) {
|
|
849
|
+
throw new Error('C8 sparse sculpt operations may target each vertex only once.');
|
|
850
|
+
}
|
|
851
|
+
operationVertices.add(vertexIndex);
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
return meshEdits;
|
|
855
|
+
}
|
|
856
|
+
|
|
170
857
|
function packMeshEdits(value) {
|
|
171
858
|
const edits = meshEditsOption(value);
|
|
172
859
|
if (edits.length === 0) return null;
|
|
@@ -203,14 +890,22 @@ function packMeshEdits(value) {
|
|
|
203
890
|
|
|
204
891
|
/**
|
|
205
892
|
* Portable identity for a source-GLB project. Geometry stays outside the JSON
|
|
206
|
-
* document; the document stores
|
|
207
|
-
*
|
|
208
|
-
* decode the
|
|
893
|
+
* document; the document stores either the stable catalog id or the exact C8
|
|
894
|
+
* control/high hashes, plus deterministic edit state needed to rebuild and
|
|
895
|
+
* decode the source GLB.
|
|
209
896
|
*/
|
|
210
897
|
export function createRockReferenceIdentity(options = null) {
|
|
211
898
|
if (!options || typeof options !== 'object') return null;
|
|
899
|
+
const c8Intent = options.sourceMode === 'c8-custom-mesh'
|
|
900
|
+
|| (options.customMeshSource !== null && options.customMeshSource !== undefined);
|
|
212
901
|
const id = String(options.id ?? '').trim();
|
|
213
|
-
if (!id)
|
|
902
|
+
if (!id) {
|
|
903
|
+
if (c8Intent) throw new Error('C8 custom-mesh documents require a stable source ID.');
|
|
904
|
+
return null;
|
|
905
|
+
}
|
|
906
|
+
if (c8Intent && options.sourceMode !== 'c8-custom-mesh') {
|
|
907
|
+
throw new Error('A C8 custom-mesh manifest cannot silently downgrade to a legacy mesh-template reference.');
|
|
908
|
+
}
|
|
214
909
|
const lodRatios = lodRatiosOption(options.lodRatios);
|
|
215
910
|
const lodTriangles = lodTrianglesOption(
|
|
216
911
|
options.lodTriangles,
|
|
@@ -221,6 +916,115 @@ export function createRockReferenceIdentity(options = null) {
|
|
|
221
916
|
.includes(options.topFinish)
|
|
222
917
|
? options.topFinish
|
|
223
918
|
: 'source';
|
|
919
|
+
const identityLandmarks = identityLandmarksOption(options.identityLandmarks);
|
|
920
|
+
const customMeshSource = customMeshSourceOption(options.customMeshSource, identityLandmarks);
|
|
921
|
+
if (options.sourceMode === 'c8-custom-mesh' && !customMeshSource) {
|
|
922
|
+
throw new Error('C8 custom-mesh documents require valid content-bound control/high GLBs, landmarks, semantic regions, and edit bounds.');
|
|
923
|
+
}
|
|
924
|
+
if (options.sourceMode === 'c8-custom-mesh'
|
|
925
|
+
&& !String(options.geology ?? options.surfacePackage?.geology ?? '').trim()) {
|
|
926
|
+
throw new Error('C8 custom-mesh documents require an explicit geology surface profile.');
|
|
927
|
+
}
|
|
928
|
+
const sourceMode = options.sourceMode === 'c8-custom-mesh' && customMeshSource
|
|
929
|
+
? 'c8-custom-mesh'
|
|
930
|
+
: 'mesh-template';
|
|
931
|
+
const sourceRevision = positiveInteger(options.sourceRevision, 1);
|
|
932
|
+
if (sourceMode === 'c8-custom-mesh'
|
|
933
|
+
&& (customMeshSource.control.sourceRevision > sourceRevision
|
|
934
|
+
|| customMeshSource.highDetail.sourceRevision > sourceRevision)) {
|
|
935
|
+
throw new Error('C8 control/high artifacts cannot claim a future source revision.');
|
|
936
|
+
}
|
|
937
|
+
const surfacePackage = surfacePackageOption(options.surfacePackage);
|
|
938
|
+
const geology = String(options.geology ?? options.surfacePackage?.geology ?? '').trim();
|
|
939
|
+
if (sourceMode === 'c8-custom-mesh' && (!surfacePackage || surfacePackage.geology !== geology)) {
|
|
940
|
+
throw new Error('C8 custom-mesh documents require one exact saved C7 geology surface package.');
|
|
941
|
+
}
|
|
942
|
+
const meshCuts = meshCutsOption(options.meshCuts, { strict: true });
|
|
943
|
+
const meshSnapshots = meshSnapshotsOption(options.meshSnapshots, options.meshSnapshotsPacked, {
|
|
944
|
+
strict: options.sourceMode === 'c8-custom-mesh',
|
|
945
|
+
});
|
|
946
|
+
// Sparse edits made after a Boolean cut address the cut result rather than
|
|
947
|
+
// the admitted control topology, so they cannot be range-checked against
|
|
948
|
+
// the original manifest here. Runtime replay still audits every operation.
|
|
949
|
+
const hasCuts = meshCuts.length > 0;
|
|
950
|
+
const hasSnapshots = meshSnapshots.length > 0;
|
|
951
|
+
let meshEdits = meshEditsOption(options.meshEdits, options.meshEditsPacked, {
|
|
952
|
+
strict: sourceMode === 'c8-custom-mesh' && !hasCuts && !hasSnapshots,
|
|
953
|
+
});
|
|
954
|
+
if (sourceMode === 'c8-custom-mesh') {
|
|
955
|
+
meshEdits = meshEdits.map((edit) => ({
|
|
956
|
+
...edit,
|
|
957
|
+
deltas: edit.deltas.map(([vertexIndex, x, y, z]) => [vertexIndex, Math.fround(x), Math.fround(y), Math.fround(z)]),
|
|
958
|
+
}));
|
|
959
|
+
if (!hasCuts && !hasSnapshots) validateC8MeshEditSequence(meshEdits, customMeshSource);
|
|
960
|
+
}
|
|
961
|
+
const meshOperationOrder = meshOperationOrderOption(
|
|
962
|
+
options.meshOperationOrder,
|
|
963
|
+
meshEdits,
|
|
964
|
+
meshCuts,
|
|
965
|
+
{ strict: true },
|
|
966
|
+
);
|
|
967
|
+
const maximumVariationStrength = sourceMode === 'c8-custom-mesh'
|
|
968
|
+
? customMeshSource.editEnvelope.maximumVariationStrength
|
|
969
|
+
: 1;
|
|
970
|
+
const variation = Math.min(Math.max(Number(options.variation) || 0, 0), maximumVariationStrength);
|
|
971
|
+
const variationSeed = Math.round(Number(options.variationSeed) || 0) >>> 0;
|
|
972
|
+
const surfaceMode = options.surfaceMode === 'generated' ? 'generated' : 'source';
|
|
973
|
+
const authoritySourceId = sourceMode === 'c8-custom-mesh'
|
|
974
|
+
? createC8ReferenceAuthoritySourceId({ customMeshSource, sourceMode })
|
|
975
|
+
: null;
|
|
976
|
+
const geometrySourceId = sourceMode === 'c8-custom-mesh'
|
|
977
|
+
? createC8ReferenceGeometrySourceId({
|
|
978
|
+
customMeshSource,
|
|
979
|
+
meshCuts,
|
|
980
|
+
meshEdits,
|
|
981
|
+
meshOperationOrder,
|
|
982
|
+
meshSnapshots,
|
|
983
|
+
sourceMode,
|
|
984
|
+
variation,
|
|
985
|
+
variationSeed,
|
|
986
|
+
})
|
|
987
|
+
: null;
|
|
988
|
+
const surfaceSourceId = sourceMode === 'c8-custom-mesh'
|
|
989
|
+
? createC8ReferenceSurfaceSourceId({
|
|
990
|
+
customMeshSource,
|
|
991
|
+
geology,
|
|
992
|
+
geometrySourceId,
|
|
993
|
+
sourceMode,
|
|
994
|
+
surfaceMode,
|
|
995
|
+
surfacePackage,
|
|
996
|
+
topFinish,
|
|
997
|
+
})
|
|
998
|
+
: null;
|
|
999
|
+
if (sourceMode === 'c8-custom-mesh'
|
|
1000
|
+
&& ((options.authoritySourceId && options.authoritySourceId !== authoritySourceId)
|
|
1001
|
+
|| (options.geometrySourceId && options.geometrySourceId !== geometrySourceId)
|
|
1002
|
+
|| (options.surfaceSourceId && options.surfaceSourceId !== surfaceSourceId))) {
|
|
1003
|
+
throw new Error('Saved C8 authority, geometry, or surface identity is stale for the admitted source and edits.');
|
|
1004
|
+
}
|
|
1005
|
+
const surfaceReprojection = sourceMode === 'c8-custom-mesh'
|
|
1006
|
+
? c8SurfaceReprojectionOption(options.surfaceReprojection, {
|
|
1007
|
+
authoritySourceId,
|
|
1008
|
+
controlContentHash: customMeshSource.control.contentHash,
|
|
1009
|
+
geology,
|
|
1010
|
+
geometrySourceId,
|
|
1011
|
+
meshEditCount: meshOperationOrder.length + meshSnapshots.length,
|
|
1012
|
+
sourceId: id,
|
|
1013
|
+
sourceRevision,
|
|
1014
|
+
surfacePackage,
|
|
1015
|
+
surfaceSourceId,
|
|
1016
|
+
})
|
|
1017
|
+
: null;
|
|
1018
|
+
const derivedArtifactState = sourceMode === 'c8-custom-mesh'
|
|
1019
|
+
? portableDerivedArtifactState(options.derivedArtifactState, {
|
|
1020
|
+
authoritySourceId,
|
|
1021
|
+
customMeshSource,
|
|
1022
|
+
geometrySourceId,
|
|
1023
|
+
sourceRevision,
|
|
1024
|
+
surfaceReprojection,
|
|
1025
|
+
surfaceSourceId,
|
|
1026
|
+
})
|
|
1027
|
+
: null;
|
|
224
1028
|
return {
|
|
225
1029
|
archetype: String(options.archetype ?? '').trim(),
|
|
226
1030
|
catalogVersion: positiveInteger(options.catalogVersion, 1),
|
|
@@ -230,16 +1034,33 @@ export function createRockReferenceIdentity(options = null) {
|
|
|
230
1034
|
? lodTriangles.map((triangles) => triangles / lodTriangles[0])
|
|
231
1035
|
: lodRatios,
|
|
232
1036
|
lodTriangles,
|
|
233
|
-
|
|
1037
|
+
meshCuts,
|
|
1038
|
+
meshEdits,
|
|
1039
|
+
meshOperationOrder,
|
|
1040
|
+
meshSnapshots,
|
|
1041
|
+
...(sourceMode === 'c8-custom-mesh' ? {
|
|
1042
|
+
authoritySourceId,
|
|
1043
|
+
customMeshSource,
|
|
1044
|
+
derivedArtifactState,
|
|
1045
|
+
geology,
|
|
1046
|
+
geometrySourceId,
|
|
1047
|
+
identityLandmarks,
|
|
1048
|
+
label: String(options.label ?? id),
|
|
1049
|
+
sourceRevision,
|
|
1050
|
+
surfaceSourceId,
|
|
1051
|
+
surfaceReprojection,
|
|
1052
|
+
thumbnailUrl: String(options.thumbnailUrl ?? ''),
|
|
1053
|
+
} : {}),
|
|
234
1054
|
role: String(options.role ?? '').trim(),
|
|
235
1055
|
series: String(options.series ?? '').trim(),
|
|
236
|
-
sourceMode
|
|
237
|
-
surfacePackage
|
|
238
|
-
surfaceMode
|
|
1056
|
+
sourceMode,
|
|
1057
|
+
surfacePackage,
|
|
1058
|
+
surfaceMode,
|
|
239
1059
|
targetTriangles: positiveInteger(options.targetTriangles, lodTriangles[0] ?? 0),
|
|
240
1060
|
topFinish,
|
|
241
|
-
variation
|
|
242
|
-
variationSeed
|
|
1061
|
+
variation,
|
|
1062
|
+
variationSeed,
|
|
1063
|
+
...(options.variationSettings ? { variationSettings: variationSettingsOption(options.variationSettings) } : {}),
|
|
243
1064
|
};
|
|
244
1065
|
}
|
|
245
1066
|
|
|
@@ -507,11 +1328,14 @@ export function computeDocumentBounds(document) {
|
|
|
507
1328
|
/** Serializes a document to JSON (dropping the runtime `revision`). */
|
|
508
1329
|
export function serializeRockDocument(document, { pretty = false } = {}) {
|
|
509
1330
|
const { revision, ...serializable } = document;
|
|
510
|
-
if (serializable.reference
|
|
511
|
-
const { meshEdits, ...reference } = serializable.reference;
|
|
1331
|
+
if (isRockSourceMeshReference(serializable.reference)) {
|
|
1332
|
+
const { meshEdits, meshSnapshots, ...reference } = serializable.reference;
|
|
512
1333
|
serializable.reference = {
|
|
513
1334
|
...reference,
|
|
514
1335
|
...(meshEdits?.length > 0 ? { meshEditsPacked: packMeshEdits(meshEdits) } : { meshEdits: [] }),
|
|
1336
|
+
...(meshSnapshots?.length > 0
|
|
1337
|
+
? { meshSnapshotsPacked: packMeshSnapshots(meshSnapshots) }
|
|
1338
|
+
: { meshSnapshots: [] }),
|
|
515
1339
|
};
|
|
516
1340
|
}
|
|
517
1341
|
return JSON.stringify(serializable, null, pretty ? 2 : undefined);
|
|
@@ -526,7 +1350,12 @@ export function serializeRockDocument(document, { pretty = false } = {}) {
|
|
|
526
1350
|
// those deltas as float32 binary in portable JSON so normal sculpt sessions
|
|
527
1351
|
// fit the hosted creation limit. v6 adds the optional deterministic C7 surface
|
|
528
1352
|
// package identity used by the 480 source-mesh catalog. Existing
|
|
529
|
-
// preset/custom projects remain reference-free.
|
|
1353
|
+
// preset/custom projects remain reference-free. v7 preserves the content-bound
|
|
1354
|
+
// C8 control/high GLBs, semantic regions, landmarks, and derivative state. v8
|
|
1355
|
+
// adds ordered, replayable Boolean drill cuts alongside sparse sculpt edits.
|
|
1356
|
+
// v9 packs topology-changing geometry snapshots into portable base64 typed
|
|
1357
|
+
// arrays so local drafts and hosted project payloads do not overflow JSON
|
|
1358
|
+
// storage after remesh, union, subdivision, or fracture operations.
|
|
530
1359
|
const MIGRATIONS = Object.freeze([
|
|
531
1360
|
(document) => ({
|
|
532
1361
|
...document,
|
|
@@ -571,6 +1400,41 @@ const MIGRATIONS = Object.freeze([
|
|
|
571
1400
|
: null,
|
|
572
1401
|
schemaVersion: 6,
|
|
573
1402
|
}),
|
|
1403
|
+
(document) => ({
|
|
1404
|
+
...document,
|
|
1405
|
+
reference: document.reference && typeof document.reference === 'object'
|
|
1406
|
+
? {
|
|
1407
|
+
...document.reference,
|
|
1408
|
+
customMeshSource: document.reference.customMeshSource ?? null,
|
|
1409
|
+
derivedArtifactState: document.reference.derivedArtifactState ?? null,
|
|
1410
|
+
identityLandmarks: document.reference.identityLandmarks ?? [],
|
|
1411
|
+
sourceRevision: document.reference.sourceRevision ?? 1,
|
|
1412
|
+
surfaceReprojection: document.reference.surfaceReprojection ?? null,
|
|
1413
|
+
}
|
|
1414
|
+
: null,
|
|
1415
|
+
schemaVersion: 7,
|
|
1416
|
+
}),
|
|
1417
|
+
(document) => ({
|
|
1418
|
+
...document,
|
|
1419
|
+
reference: document.reference && typeof document.reference === 'object'
|
|
1420
|
+
? {
|
|
1421
|
+
...document.reference,
|
|
1422
|
+
meshCuts: document.reference.meshCuts ?? [],
|
|
1423
|
+
meshOperationOrder: document.reference.meshOperationOrder ?? null,
|
|
1424
|
+
}
|
|
1425
|
+
: null,
|
|
1426
|
+
schemaVersion: 8,
|
|
1427
|
+
}),
|
|
1428
|
+
(document) => ({
|
|
1429
|
+
...document,
|
|
1430
|
+
reference: document.reference && typeof document.reference === 'object'
|
|
1431
|
+
? {
|
|
1432
|
+
...document.reference,
|
|
1433
|
+
meshSnapshots: document.reference.meshSnapshots ?? [],
|
|
1434
|
+
}
|
|
1435
|
+
: null,
|
|
1436
|
+
schemaVersion: 9,
|
|
1437
|
+
}),
|
|
574
1438
|
]);
|
|
575
1439
|
|
|
576
1440
|
/**
|