@deepblend/dsh-blender-contracts 0.2.20 → 0.3.1
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/lib/artistic-review.js +57 -0
- package/lib/asset-content.js +6 -3
- package/lib/handled-cup.js +55 -0
- package/lib/index.js +31 -14
- package/lib/recipe.js +253 -0
- package/lib/reference-images.js +28 -0
- package/lib/runtime-types.d.ts +208 -0
- package/lib/scene-patch.js +133 -1
- package/lib/scene-spec.js +249 -4
- package/lib/schema-types.d.ts +185 -0
- package/lib/schemas/job-result.schema.json +117 -0
- package/lib/schemas/recipe.schema.json +332 -0
- package/lib/schemas/scene-patch.schema.json +793 -57
- package/lib/schemas/scene-spec.schema.json +653 -76
- package/lib/sdk.d.ts +101 -0
- package/lib/sdk.js +41 -0
- package/lib/ui-api.js +68 -3
- package/lib/visual-composition.js +38 -24
- package/lib/visual-issue.js +26 -8
- package/lib/visual-loop.js +204 -26
- package/package.json +8 -1
package/lib/scene-spec.js
CHANGED
|
@@ -22,6 +22,8 @@
|
|
|
22
22
|
|
|
23
23
|
import { canonicalStringify, sha256Canonical } from './canonical.js'
|
|
24
24
|
import { compileSchema, formatIssues } from './json-schema.js'
|
|
25
|
+
import { referenceImageIssues } from './reference-images.js'
|
|
26
|
+
import { HANDLED_CUP_FIELDS, handledCupIssues, resolveHandledCup } from './handled-cup.js'
|
|
25
27
|
import sceneSpecSchema from './schemas/scene-spec.schema.json' with { type: 'json' }
|
|
26
28
|
|
|
27
29
|
/** The one schema version this module understands. */
|
|
@@ -73,6 +75,7 @@ export const TRANSFORM_ANIMATION_PROPERTIES = Object.freeze([
|
|
|
73
75
|
*/
|
|
74
76
|
export const MATERIAL_ANIMATION_PROPERTIES = Object.freeze([
|
|
75
77
|
'emissionStrength', 'roughness', 'metallic', 'ior', 'alpha', 'coatWeight', 'transmissionWeight',
|
|
78
|
+
'anisotropic', 'anisotropicRotation',
|
|
76
79
|
'baseColor.r', 'baseColor.g', 'baseColor.b',
|
|
77
80
|
'emissionColor.r', 'emissionColor.g', 'emissionColor.b',
|
|
78
81
|
])
|
|
@@ -114,6 +117,7 @@ const NON_NEGATIVE_MATERIAL_PROPERTIES = Object.freeze([
|
|
|
114
117
|
/** Material parameters that are only meaningful inside [0, 1]. */
|
|
115
118
|
const UNIT_MATERIAL_PROPERTIES = Object.freeze([
|
|
116
119
|
'roughness', 'metallic', 'alpha', 'coatWeight', 'transmissionWeight',
|
|
120
|
+
'anisotropic', 'anisotropicRotation',
|
|
117
121
|
'baseColor.r', 'baseColor.g', 'baseColor.b',
|
|
118
122
|
])
|
|
119
123
|
|
|
@@ -136,6 +140,9 @@ export const IMPORT_OPERATOR_BY_ASSET_TYPE = Object.freeze({
|
|
|
136
140
|
blend: 'wm.append',
|
|
137
141
|
})
|
|
138
142
|
|
|
143
|
+
export const IMAGE_ASSET_TYPES = Object.freeze(['png', 'jpg', 'jpeg'])
|
|
144
|
+
export const ENVIRONMENT_ASSET_TYPES = Object.freeze([...IMAGE_ASSET_TYPES, 'hdr', 'exr'])
|
|
145
|
+
|
|
139
146
|
const validateStructure = compileSchema(sceneSpecSchema, { id: 'scene-spec.schema.json' })
|
|
140
147
|
|
|
141
148
|
/**
|
|
@@ -226,6 +233,9 @@ export function validateSceneSpec(spec) {
|
|
|
226
233
|
const idsOf = name => indexByCollection.get(name) ?? new Map()
|
|
227
234
|
|
|
228
235
|
// ---- reference integrity ------------------------------------------------
|
|
236
|
+
for (const issue of referenceImageIssues(project.referenceImages ?? [], document.assets ?? [])) {
|
|
237
|
+
errors.push({ ...issue, severity: 'error', path: `project.referenceImages${issue.path}` })
|
|
238
|
+
}
|
|
229
239
|
const requiresId = (collection, id, path, what) => {
|
|
230
240
|
if (id === undefined || id === null) return
|
|
231
241
|
if (!idsOf(collection).has(id)) {
|
|
@@ -235,6 +245,12 @@ export function validateSceneSpec(spec) {
|
|
|
235
245
|
})
|
|
236
246
|
}
|
|
237
247
|
}
|
|
248
|
+
requiresId('entities', project.reviewSubjectId, 'project.reviewSubjectId', 'review subject')
|
|
249
|
+
if (project.reviewSubjectId !== undefined && document.entities.some(entity =>
|
|
250
|
+
entity.id === project.reviewSubjectId && entity.type === 'empty')) {
|
|
251
|
+
errors.push({ severity: 'error', code: 'SCENE_SPEC_INVALID', path: 'project.reviewSubjectId',
|
|
252
|
+
message: 'The review subject must be a renderable entity, not an empty.' })
|
|
253
|
+
}
|
|
238
254
|
|
|
239
255
|
;(document.assets ?? []).forEach((asset, position) => {
|
|
240
256
|
// Path containment (SPEC §15.2). The project root is the only legal base.
|
|
@@ -254,6 +270,49 @@ export function validateSceneSpec(spec) {
|
|
|
254
270
|
}
|
|
255
271
|
})
|
|
256
272
|
|
|
273
|
+
if (document.world?.environment) {
|
|
274
|
+
const id = document.world.environment.assetId
|
|
275
|
+
requiresId('assets', id, 'world.environment.assetId', 'environment image')
|
|
276
|
+
const asset = (document.assets ?? []).find(entry => entry.id === id)
|
|
277
|
+
if (asset && !ENVIRONMENT_ASSET_TYPES.includes(asset.type)) errors.push({ severity: 'error',
|
|
278
|
+
code: 'SCENE_WORLD_ENVIRONMENT_INVALID', path: 'world.environment.assetId', message: 'environment requires an image asset' })
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
for (const [index, material] of (document.materials ?? []).entries()) {
|
|
282
|
+
if (material.texture?.coordinates === 'uv' && material.shader === 'emission') errors.push({ severity: 'error',
|
|
283
|
+
code: 'SCENE_MATERIAL_TEXTURE_INVALID', path: `materials[${index}].texture`,
|
|
284
|
+
message: 'UV procedural texture requires a principled or glass material' })
|
|
285
|
+
const anisotropicKeys = ['anisotropic', 'anisotropicRotation']
|
|
286
|
+
const anisotropicTracks = (document.animationTracks ?? []).filter(track => track.targetKind === 'material' &&
|
|
287
|
+
track.targetEntityId === material.id && anisotropicKeys.includes(track.property))
|
|
288
|
+
const declaredAnisotropy = material.tangent !== undefined || anisotropicTracks.length > 0 ||
|
|
289
|
+
anisotropicKeys.some(key => material.parameters?.[key] !== undefined)
|
|
290
|
+
if (declaredAnisotropy && material.shader === 'emission') errors.push({ severity: 'error',
|
|
291
|
+
code: 'SCENE_MATERIAL_ANISOTROPY_INVALID', path: `materials[${index}]`,
|
|
292
|
+
message: 'anisotropic parameters and tangents require a principled or glass material' })
|
|
293
|
+
const directionRequired = anisotropicKeys.some(key => (material.parameters?.[key] ?? 0) !== 0) ||
|
|
294
|
+
anisotropicTracks.some(track => track.keyframes.some(keyframe => keyframe.value !== 0))
|
|
295
|
+
if (directionRequired && material.tangent === undefined) errors.push({ severity: 'error',
|
|
296
|
+
code: 'SCENE_MATERIAL_ANISOTROPY_INVALID', path: `materials[${index}].tangent`,
|
|
297
|
+
message: 'nonzero anisotropy or rotation, including animation, requires an explicit uv or radial tangent' })
|
|
298
|
+
if (material.images && material.texture) errors.push({ severity: 'error', code: 'SCENE_MATERIAL_IMAGES_INVALID',
|
|
299
|
+
path: `materials[${index}].images`, message: 'image maps and procedural texture cannot be combined on one material yet' })
|
|
300
|
+
if (material.images && material.shader === 'emission') errors.push({ severity: 'error', code: 'SCENE_MATERIAL_IMAGES_INVALID',
|
|
301
|
+
path: `materials[${index}].images`, message: 'image maps require a principled or glass material' })
|
|
302
|
+
for (const [channel, binding] of Object.entries(material.images ?? {})) {
|
|
303
|
+
const path = `materials[${index}].images.${channel}`
|
|
304
|
+
requiresId('assets', binding.assetId, `${path}.assetId`, 'image asset')
|
|
305
|
+
const asset = (document.assets ?? []).find(entry => entry.id === binding.assetId)
|
|
306
|
+
if (asset && !IMAGE_ASSET_TYPES.includes(asset.type)) errors.push({ severity: 'error',
|
|
307
|
+
code: 'SCENE_MATERIAL_IMAGES_INVALID', path, message: 'material image maps require PNG or JPEG assets' })
|
|
308
|
+
if ((binding.strength !== undefined && channel !== 'normal') ||
|
|
309
|
+
(binding.channel !== undefined && !['roughness', 'metallic', 'alpha'].includes(channel))) {
|
|
310
|
+
errors.push({ severity: 'error', code: 'SCENE_MATERIAL_IMAGES_INVALID', path,
|
|
311
|
+
message: 'strength applies only to normals; channel selection applies only to scalar maps' })
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
257
316
|
document.entities.forEach((entity, position) => {
|
|
258
317
|
const at = `entities[${position}]`
|
|
259
318
|
if (entity.type === 'asset-instance') {
|
|
@@ -264,6 +323,10 @@ export function validateSceneSpec(spec) {
|
|
|
264
323
|
})
|
|
265
324
|
} else {
|
|
266
325
|
requiresId('assets', entity.assetId, `${at}.assetId`, 'asset')
|
|
326
|
+
if (ENVIRONMENT_ASSET_TYPES.includes((document.assets ?? []).find(asset => asset.id === entity.assetId)?.type)) {
|
|
327
|
+
errors.push({ severity: 'error', code: 'SCENE_ENTITY_ASSET_INVALID', path: `${at}.assetId`,
|
|
328
|
+
message: 'an image asset cannot be instantiated as a mesh' })
|
|
329
|
+
}
|
|
267
330
|
}
|
|
268
331
|
if (entity.generator !== undefined) {
|
|
269
332
|
errors.push({
|
|
@@ -285,20 +348,119 @@ export function validateSceneSpec(spec) {
|
|
|
285
348
|
})
|
|
286
349
|
}
|
|
287
350
|
requiresId('materials', entity.materialId, `${at}.materialId`, 'material')
|
|
351
|
+
if (entity.materialBindings !== undefined) {
|
|
352
|
+
if (entity.type !== 'asset-instance') errors.push({ severity: 'error', code: 'SCENE_MATERIAL_BINDING_INVALID',
|
|
353
|
+
path: `${at}.materialBindings`, message: 'materialBindings requires an asset-instance entity' })
|
|
354
|
+
const selectors = new Set()
|
|
355
|
+
for (const [index, binding] of entity.materialBindings.entries()) {
|
|
356
|
+
requiresId('materials', binding.materialId, `${at}.materialBindings[${index}].materialId`, 'material')
|
|
357
|
+
const key = JSON.stringify([binding.partId, binding.slotIndex ?? null])
|
|
358
|
+
if (selectors.has(key)) errors.push({ severity: 'error', code: 'SCENE_MATERIAL_BINDING_INVALID',
|
|
359
|
+
path: `${at}.materialBindings[${index}]`, message: 'duplicate part and slot material binding' })
|
|
360
|
+
selectors.add(key)
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
if (entity.generator?.shape === 'handled_cup') {
|
|
364
|
+
for (const message of handledCupIssues(entity.generator)) errors.push({ severity: 'error',
|
|
365
|
+
code: 'SCENE_GENERATOR_PARAMETERS_INVALID', path: `${at}.generator`, message })
|
|
366
|
+
} else if (entity.generator && HANDLED_CUP_FIELDS.some(key => entity.generator[key] !== undefined)) {
|
|
367
|
+
errors.push({ severity: 'error', code: 'SCENE_GENERATOR_PARAMETERS_INVALID', path: `${at}.generator`,
|
|
368
|
+
message: 'handled cup dimensions and resolutions apply only to handled_cup geometry' })
|
|
369
|
+
}
|
|
370
|
+
if (entity.generator?.shape === 'lathe') {
|
|
371
|
+
const profile = entity.generator.profile
|
|
372
|
+
const invalid = message => errors.push({ severity: 'error', code: 'SCENE_GENERATOR_PROFILE_INVALID',
|
|
373
|
+
path: `${at}.generator.profile`, message })
|
|
374
|
+
if (!Array.isArray(profile) || profile.length < 2) {
|
|
375
|
+
invalid('a lathe requires at least two [radius, height] profile points')
|
|
376
|
+
} else {
|
|
377
|
+
if (profile.some(point => point[0] < 0) || !profile.some(point => point[0] > 0)) {
|
|
378
|
+
invalid('lathe radii must be nonnegative, with at least one positive radius')
|
|
379
|
+
}
|
|
380
|
+
if (profile.some((point, index) => index > 0 && point[0] === profile[index - 1][0] && point[1] === profile[index - 1][1])) {
|
|
381
|
+
invalid('consecutive lathe profile points must differ')
|
|
382
|
+
}
|
|
383
|
+
if (profile.slice(1, -1).some(point => point[0] === 0)) invalid('axis points are allowed only at the two profile ends')
|
|
384
|
+
if (new Set(profile.map(point => point[1])).size < 2) invalid('a lathe profile must have nonzero height')
|
|
385
|
+
if (entity.generator.closedProfile && (profile.length < 3 ||
|
|
386
|
+
(profile[0][0] === profile.at(-1)[0] && profile[0][1] === profile.at(-1)[1]))) {
|
|
387
|
+
invalid('a closed profile needs at least three points and closes automatically; omit the repeated first point')
|
|
388
|
+
}
|
|
389
|
+
if (profileIntersects(profile, entity.generator.closedProfile === true)) invalid('lathe profile edges must not intersect')
|
|
390
|
+
}
|
|
391
|
+
} else if (entity.generator && ['profile', 'closedProfile', ...(entity.generator.shape === 'curve' ? [] : ['capEnds'])].some(key => entity.generator[key] !== undefined)) {
|
|
392
|
+
errors.push({ severity: 'error', code: 'SCENE_GENERATOR_PROFILE_INVALID', path: `${at}.generator`,
|
|
393
|
+
message: 'profile and closedProfile require lathe geometry; capEnds requires lathe or curve geometry' })
|
|
394
|
+
}
|
|
395
|
+
const generator = entity.generator
|
|
396
|
+
if (generator?.shape === 'curve') {
|
|
397
|
+
const path = generator.path
|
|
398
|
+
const invalid = message => errors.push({ severity: 'error', code: 'SCENE_GENERATOR_PATH_INVALID',
|
|
399
|
+
path: `${at}.generator.path`, message })
|
|
400
|
+
if (!Array.isArray(path) || path.length < (generator.pathClosed ? 3 : 2)) {
|
|
401
|
+
invalid('a curve needs two path points, or three for a closed path')
|
|
402
|
+
} else {
|
|
403
|
+
if (path.some((point, i) => i > 0 && point.every((value, axis) => value === path[i - 1][axis]))) {
|
|
404
|
+
invalid('consecutive curve path points must differ')
|
|
405
|
+
}
|
|
406
|
+
if (generator.pathClosed && path[0].every((value, axis) => value === path.at(-1)[axis])) {
|
|
407
|
+
invalid('a closed path closes automatically; omit the repeated first point')
|
|
408
|
+
}
|
|
409
|
+
for (let i = generator.pathClosed ? 0 : 1; i < (generator.pathClosed ? path.length : path.length - 1); i += 1) {
|
|
410
|
+
const a = path[(i + path.length - 1) % path.length].map((v, axis) => v - path[i][axis])
|
|
411
|
+
const b = path[(i + 1) % path.length].map((v, axis) => v - path[i][axis])
|
|
412
|
+
const cross = [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]]
|
|
413
|
+
if (Math.hypot(...cross) < 1e-12 && a.reduce((sum, v, axis) => sum + v * b[axis], 0) > 0) {
|
|
414
|
+
invalid('a curve path must not reverse along the same segment')
|
|
415
|
+
break
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
} else if (generator && ['path', 'pathClosed', 'pathInterpolation', 'curveResolution', 'bevelResolution'].some(key => generator[key] !== undefined)) {
|
|
420
|
+
errors.push({ severity: 'error', code: 'SCENE_GENERATOR_PATH_INVALID', path: `${at}.generator`,
|
|
421
|
+
message: 'curve path parameters apply only to curve geometry' })
|
|
422
|
+
}
|
|
288
423
|
// AND THE RIG IT IS SKINNED TO. MEASURED: this check was missing at first, and the contract suite
|
|
289
424
|
// caught it — the schema can say `armatureId` is a string, but only a cross-reference can say it
|
|
290
425
|
// names something the scene declares. An entity skinned to an armature nobody declares would render
|
|
291
426
|
// as a static object while the file says it is rigged, which is the failure this code exists for.
|
|
292
427
|
requiresId('armatures', entity.armatureId, `${at}.armatureId`, 'armature')
|
|
293
|
-
if (entity.type
|
|
428
|
+
if (entity.type === 'generator' && entity.materialId === undefined) {
|
|
294
429
|
notices.push({
|
|
295
430
|
severity: 'notice', code: 'SCENE_ENTITY_MATERIAL_DEFAULTED', path: at,
|
|
296
431
|
message: `entity "${entity.id}" has no materialId; the default principled material will be applied`,
|
|
297
432
|
})
|
|
298
433
|
}
|
|
299
434
|
checkScale(entity.transform?.scale, `${at}.transform.scale`, errors, entity.id)
|
|
435
|
+
for (const [i, modifier] of (entity.modifiers ?? []).entries()) {
|
|
436
|
+
const path = `${at}.modifiers[${i}]`
|
|
437
|
+
if (entity.type === 'empty' || (modifier.type === 'solidify' && modifier.thickness === 0) ||
|
|
438
|
+
(modifier.type === 'array' && modifier.offset.every(value => value === 0))) {
|
|
439
|
+
errors.push({ severity: 'error', code: 'SCENE_MODIFIER_INVALID', path,
|
|
440
|
+
message: 'mesh modifiers need geometry, nonzero thickness and nonzero array offset' })
|
|
441
|
+
}
|
|
442
|
+
if (modifier.type === 'boolean') requiresId('entities', modifier.targetEntityId, `${path}.targetEntityId`, 'boolean operand')
|
|
443
|
+
}
|
|
300
444
|
})
|
|
301
445
|
|
|
446
|
+
const modifierEntities = new Map(document.entities.map(entity => [entity.id, entity]))
|
|
447
|
+
const visitedModifiers = new Set(), activeModifiers = new Set()
|
|
448
|
+
const visitModifiers = id => {
|
|
449
|
+
if (activeModifiers.has(id)) {
|
|
450
|
+
errors.push({ severity: 'error', code: 'SCENE_MODIFIER_CYCLE', path: 'entities',
|
|
451
|
+
message: `boolean dependency cycle includes entity "${id}"` })
|
|
452
|
+
return
|
|
453
|
+
}
|
|
454
|
+
if (visitedModifiers.has(id)) return
|
|
455
|
+
activeModifiers.add(id)
|
|
456
|
+
for (const modifier of modifierEntities.get(id)?.modifiers ?? []) {
|
|
457
|
+
if (modifier.type === 'boolean') visitModifiers(modifier.targetEntityId)
|
|
458
|
+
}
|
|
459
|
+
activeModifiers.delete(id)
|
|
460
|
+
visitedModifiers.add(id)
|
|
461
|
+
}
|
|
462
|
+
for (const id of modifierEntities.keys()) visitModifiers(id)
|
|
463
|
+
|
|
302
464
|
;(document.lights ?? []).forEach((light, position) => {
|
|
303
465
|
checkScale(light.transform?.scale, `lights[${position}].transform.scale`, errors, light.id)
|
|
304
466
|
})
|
|
@@ -521,8 +683,25 @@ export function compileSceneSpec(spec) {
|
|
|
521
683
|
/** Entity centre and radius from generator footprint — the aiming substrate. */
|
|
522
684
|
const entityBounds = {}
|
|
523
685
|
for (const entity of entities) {
|
|
524
|
-
entityBounds[entity.id] = boundsOf(entity)
|
|
686
|
+
entityBounds[entity.id] = boundsOf({ ...entity, modifiers: [] })
|
|
525
687
|
}
|
|
688
|
+
const boundsVisited = new Set()
|
|
689
|
+
const expandBooleanBounds = entity => {
|
|
690
|
+
if (boundsVisited.has(entity.id)) return
|
|
691
|
+
boundsVisited.add(entity.id)
|
|
692
|
+
for (const modifier of entity.modifiers ?? []) {
|
|
693
|
+
const scale = Math.max(...entity.transform.scale.map(Math.abs))
|
|
694
|
+
if (modifier.type === 'solidify') entityBounds[entity.id].radius += Math.abs(modifier.thickness) * scale
|
|
695
|
+
if (modifier.type === 'array') entityBounds[entity.id].radius += Math.hypot(...modifier.offset) * (modifier.count - 1) * scale
|
|
696
|
+
if (modifier.type !== 'boolean' || modifier.operation !== 'union') continue
|
|
697
|
+
const operand = entities.find(candidate => candidate.id === modifier.targetEntityId)
|
|
698
|
+
if (!operand) continue
|
|
699
|
+
expandBooleanBounds(operand)
|
|
700
|
+
const own = entityBounds[entity.id], other = entityBounds[operand.id]
|
|
701
|
+
own.radius = Math.max(own.radius, Math.hypot(...own.location.map((v, axis) => v - other.location[axis])) + other.radius)
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
for (const entity of entities) expandBooleanBounds(entity)
|
|
526
705
|
|
|
527
706
|
const cameras = spec.cameras.map(camera => {
|
|
528
707
|
const targetPoint = camera.targetEntityId !== undefined
|
|
@@ -573,6 +752,28 @@ export function compileSceneSpec(spec) {
|
|
|
573
752
|
return { spec: resolved, notices, entityBounds }
|
|
574
753
|
}
|
|
575
754
|
|
|
755
|
+
/** Reject intersections and collinear reversals in a lathe boundary. */
|
|
756
|
+
function profileIntersects(points, closed) {
|
|
757
|
+
const edges = points.slice(0, closed ? points.length : -1).map((point, index) => [point, points[(index + 1) % points.length]])
|
|
758
|
+
const cross = (a, b, c) => (b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0])
|
|
759
|
+
const on = (a, b, p) => Math.abs(cross(a, b, p)) < 1e-12 &&
|
|
760
|
+
p.every((value, axis) => value >= Math.min(a[axis], b[axis]) - 1e-12 && value <= Math.max(a[axis], b[axis]) + 1e-12)
|
|
761
|
+
for (let i = closed ? 0 : 1; i < (closed ? points.length : points.length - 1); i += 1) {
|
|
762
|
+
const a = points[(i + points.length - 1) % points.length], b = points[i], c = points[(i + 1) % points.length]
|
|
763
|
+
if (Math.abs(cross(a, b, c)) < 1e-12 &&
|
|
764
|
+
(a[0] - b[0]) * (c[0] - b[0]) + (a[1] - b[1]) * (c[1] - b[1]) > 0) return true
|
|
765
|
+
}
|
|
766
|
+
for (let i = 0; i < edges.length; i += 1) {
|
|
767
|
+
for (let j = i + 2; j < edges.length; j += 1) {
|
|
768
|
+
if (closed && i === 0 && j === edges.length - 1) continue
|
|
769
|
+
const [a, b] = edges[i], [c, d] = edges[j]
|
|
770
|
+
if ((cross(a, b, c) * cross(a, b, d) < 0 && cross(c, d, a) * cross(c, d, b) < 0) ||
|
|
771
|
+
on(a, b, c) || on(a, b, d) || on(c, d, a) || on(c, d, b)) return true
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
return false
|
|
775
|
+
}
|
|
776
|
+
|
|
576
777
|
/** Generator defaults, applied per shape. */
|
|
577
778
|
function resolveGenerator(generator) {
|
|
578
779
|
const shape = generator?.shape
|
|
@@ -604,6 +805,15 @@ function resolveGenerator(generator) {
|
|
|
604
805
|
segments: generator.segments ?? 48,
|
|
605
806
|
ringCount: generator.ringCount ?? 12,
|
|
606
807
|
}
|
|
808
|
+
case 'lathe':
|
|
809
|
+
return { ...base, ...generator, segments: generator.segments ?? 96,
|
|
810
|
+
closedProfile: generator.closedProfile ?? false, capEnds: generator.capEnds ?? true }
|
|
811
|
+
case 'curve':
|
|
812
|
+
return { ...base, ...generator, radius: generator.radius ?? 0.01, pathClosed: generator.pathClosed ?? false,
|
|
813
|
+
pathInterpolation: generator.pathInterpolation ?? 'poly', curveResolution: generator.curveResolution ?? 16,
|
|
814
|
+
bevelResolution: generator.bevelResolution ?? 8, capEnds: generator.capEnds ?? true }
|
|
815
|
+
case 'handled_cup':
|
|
816
|
+
return { ...base, ...resolveHandledCup(generator) }
|
|
607
817
|
default:
|
|
608
818
|
return { ...generator }
|
|
609
819
|
}
|
|
@@ -659,9 +869,21 @@ export function entityBoundingRadius(entity) {
|
|
|
659
869
|
case 'cone': radius = Math.hypot(generator.radius, generator.depth / 2); break
|
|
660
870
|
case 'plane': radius = (generator.size / 2) * Math.SQRT2; break
|
|
661
871
|
case 'torus': radius = generator.majorRadius + generator.minorRadius; break
|
|
872
|
+
case 'lathe': radius = Math.max(...generator.profile.map(([r, z]) => Math.hypot(r, z))); break
|
|
873
|
+
case 'curve': radius = Math.max(...generator.path.map(point => Math.hypot(...point))) *
|
|
874
|
+
(generator.pathInterpolation === 'bezier' ? 2 : 1) + generator.radius; break
|
|
875
|
+
case 'handled_cup': {
|
|
876
|
+
const p = resolveHandledCup(generator)
|
|
877
|
+
radius = Math.hypot(p.radius + p.rootLength + (p.handleUpper - p.handleLower) / 2 + p.handleRadius, p.height)
|
|
878
|
+
break
|
|
879
|
+
}
|
|
662
880
|
default: radius = 1
|
|
663
881
|
}
|
|
664
882
|
}
|
|
883
|
+
for (const modifier of entity?.modifiers ?? []) {
|
|
884
|
+
if (modifier.type === 'solidify') radius += Math.abs(modifier.thickness)
|
|
885
|
+
if (modifier.type === 'array') radius += Math.hypot(...modifier.offset) * (modifier.count - 1)
|
|
886
|
+
}
|
|
665
887
|
return radius * Math.max(Math.abs(scale[0]), Math.abs(scale[1]), Math.abs(scale[2]))
|
|
666
888
|
}
|
|
667
889
|
|
|
@@ -703,7 +925,7 @@ function round4(value) {
|
|
|
703
925
|
/**
|
|
704
926
|
* The scene-relevant projection of a spec.
|
|
705
927
|
*
|
|
706
|
-
*
|
|
928
|
+
* Project metadata, including goal and referenceImages, is excluded on purpose: the digest exists
|
|
707
929
|
* to answer "is the SCENE different?", and a re-worded brief over an unchanged
|
|
708
930
|
* scene is not a new scene. Two revisions can therefore share a digest, which is
|
|
709
931
|
* exactly the signal a caller wants when deciding whether a re-render is needed.
|
|
@@ -742,7 +964,7 @@ export function sceneSpecDigest(spec) {
|
|
|
742
964
|
* Content hash of the WHOLE document, including `project`.
|
|
743
965
|
*
|
|
744
966
|
* Distinct from {@link sceneSpecDigest}, and the distinction is load-bearing.
|
|
745
|
-
* The scene digest deliberately excludes
|
|
967
|
+
* The scene digest deliberately excludes project metadata and the authored brief so
|
|
746
968
|
* that re-wording a brief over an unchanged scene is not treated as a new scene.
|
|
747
969
|
* But it also excludes `project.frameStart`, `project.frameEnd` and `project.fps`
|
|
748
970
|
* — which ARE scene-relevant: changing the frame range changes what a render
|
|
@@ -767,6 +989,24 @@ export function specHash(spec) {
|
|
|
767
989
|
return sha256Canonical(spec)
|
|
768
990
|
}
|
|
769
991
|
|
|
992
|
+
/** Identity of authored review inputs, independent of geometry and render settings.
|
|
993
|
+
* References and purposes are sets identified by id; display order does not change
|
|
994
|
+
* their meaning. Asset aliases are resolved from this document, never a live catalog.
|
|
995
|
+
* The Host must still verify these declared hashes against the actual file bytes.
|
|
996
|
+
*/
|
|
997
|
+
export function reviewInputsDigest(spec) {
|
|
998
|
+
const referenceImages = (spec?.project?.referenceImages ?? []).map(reference => {
|
|
999
|
+
const asset = (spec.assets ?? []).find(entry => entry.id === reference.assetId)
|
|
1000
|
+
return {
|
|
1001
|
+
id: reference.id, assetId: reference.assetId, sha256: reference.sha256,
|
|
1002
|
+
label: reference.label, purposes: [...reference.purposes].sort(), notes: reference.notes ?? '',
|
|
1003
|
+
asset: asset ? { id: asset.id, type: asset.type, path: asset.path, sha256: asset.sha256 ?? null } : null,
|
|
1004
|
+
}
|
|
1005
|
+
}).sort((left, right) => left.id < right.id ? -1 : left.id > right.id ? 1 : 0)
|
|
1006
|
+
return sha256Canonical({ goal: spec?.project?.goal ?? '', referenceImages,
|
|
1007
|
+
...(spec?.project?.reviewSubjectId === undefined ? {} : { reviewSubjectId: spec.project.reviewSubjectId }) })
|
|
1008
|
+
}
|
|
1009
|
+
|
|
770
1010
|
/**
|
|
771
1011
|
* The compact `SceneDigest` handed to the model (SPEC §7.2 `getScene`, §18
|
|
772
1012
|
* "FullSceneSpec 明确需要时才读取"): counts, framing facts and profiles, never
|
|
@@ -828,6 +1068,8 @@ export function summarizeSceneSpec(document, context = {}) {
|
|
|
828
1068
|
id: spec.project.id,
|
|
829
1069
|
title: spec.project.title,
|
|
830
1070
|
goal: spec.project.goal ?? null,
|
|
1071
|
+
referenceImages: structuredClone(spec.project.referenceImages ?? []),
|
|
1072
|
+
reviewSubjectId: spec.project.reviewSubjectId ?? null,
|
|
831
1073
|
fps: spec.project.fps,
|
|
832
1074
|
frameStart: spec.project.frameStart,
|
|
833
1075
|
frameEnd: spec.project.frameEnd,
|
|
@@ -849,6 +1091,7 @@ export function summarizeSceneSpec(document, context = {}) {
|
|
|
849
1091
|
type: entity.type,
|
|
850
1092
|
shape: entity.generator?.shape ?? null,
|
|
851
1093
|
materialId: entity.materialId ?? null,
|
|
1094
|
+
...(entity.materialBindings?.length ? { materialBindings: entity.materialBindings } : {}),
|
|
852
1095
|
location: entity.transform.location.map(round4),
|
|
853
1096
|
visible: entity.visible !== false,
|
|
854
1097
|
locked: entity.locked === true,
|
|
@@ -858,6 +1101,7 @@ export function summarizeSceneSpec(document, context = {}) {
|
|
|
858
1101
|
id: material.id,
|
|
859
1102
|
shader: material.shader,
|
|
860
1103
|
parameters: material.parameters ?? {},
|
|
1104
|
+
...(material.tangent ? { tangent: material.tangent } : {}),
|
|
861
1105
|
})),
|
|
862
1106
|
lights: lights.map(light => ({
|
|
863
1107
|
id: light.id,
|
|
@@ -903,6 +1147,7 @@ export function summarizeSceneSpec(document, context = {}) {
|
|
|
903
1147
|
declared: spec.world !== undefined,
|
|
904
1148
|
color: spec.world?.color ?? DEFAULT_WORLD.color,
|
|
905
1149
|
strength: spec.world?.strength ?? DEFAULT_WORLD.strength,
|
|
1150
|
+
...(spec.world?.environment ? { environment: spec.world.environment } : {}),
|
|
906
1151
|
},
|
|
907
1152
|
bounds: enclosing,
|
|
908
1153
|
subjectBounds,
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/** Generated by deepblend/tools/generate-sdk-types.mjs. Do not edit.
|
|
2
|
+
* Types describe JSON shape. Numeric bounds, ID grammar and references still require validation. */
|
|
3
|
+
|
|
4
|
+
/** Source SHA-256: e1d0fab58ba2263925efdee9e2489f205fde30e957b62b128c085f4706d6c11d */
|
|
5
|
+
export namespace SceneSpecSchema {
|
|
6
|
+
export type Id = string;
|
|
7
|
+
export type World = { "color"?: SceneSpecSchema.UnitColor; "strength"?: number; "environment"?: { "assetId": SceneSpecSchema.Id; "rotation"?: number; }; };
|
|
8
|
+
export type Vec3 = [number, number, number];
|
|
9
|
+
export type UnitColor = Array<number>;
|
|
10
|
+
export type PositiveNumber = number;
|
|
11
|
+
export type NonNegativeNumber = number;
|
|
12
|
+
export type Project = { "id": SceneSpecSchema.Id; "title": string; "goal"?: string; "units"?: "metric" | "imperial"; "fps": number; "frameStart": number; "frameEnd": number; "aspectRatio": string; "activeCamera"?: string; "referenceImages"?: SceneSpecSchema.ReferenceImages; "reviewSubjectId"?: SceneSpecSchema.Id; };
|
|
13
|
+
export type License = { "source"?: string; "commercialUse"?: boolean; "attribution"?: string; };
|
|
14
|
+
export type Asset = { "id": SceneSpecSchema.Id; "type": "glb" | "gltf" | "fbx" | "obj" | "usd" | "blend" | "png" | "jpg" | "jpeg" | "hdr" | "exr"; "path": string; "sha256"?: string; "license"?: SceneSpecSchema.License; };
|
|
15
|
+
export type ProceduralTexture = ({ "type": "noise" | "wave" | "voronoi"; "scale": number; "detail"?: number; "distortion"?: number; "stretch"?: [number, number, number]; "bump"?: number; "roughnessVariation"?: number; "colorVariation"?: number; "coordinates"?: "object"; }) | ({ "type": "noise" | "wave" | "voronoi"; "scale": number; "detail"?: number; "distortion"?: number; "stretch"?: [number, number, number]; "bump"?: number; "roughnessVariation"?: number; "colorVariation"?: number; "coordinates": "uv"; "uvMap"?: string; });
|
|
16
|
+
export type Material = { "id": SceneSpecSchema.Id; "shader": "principled" | "emission" | "glass"; "parameters"?: { "baseColor"?: SceneSpecSchema.UnitColor; "metallic"?: number; "roughness"?: number; "ior"?: number; "alpha"?: number; "emissionColor"?: SceneSpecSchema.UnitColor; "emissionStrength"?: SceneSpecSchema.NonNegativeNumber; "coatWeight"?: number; "transmissionWeight"?: number; "anisotropic"?: number; "anisotropicRotation"?: number; }; "texture"?: SceneSpecSchema.ProceduralTexture; "images"?: SceneSpecSchema.MaterialImages; "tangent"?: SceneSpecSchema.MaterialTangent; };
|
|
17
|
+
export type Transform = { "location"?: SceneSpecSchema.Vec3; "rotationEuler"?: SceneSpecSchema.Vec3; "scale"?: SceneSpecSchema.Vec3; };
|
|
18
|
+
export type Generator = { "shape": "cube" | "uv_sphere" | "cylinder" | "cone" | "plane" | "torus" | "rounded_box" | "lathe" | "curve" | "handled_cup"; "size"?: SceneSpecSchema.PositiveNumber; "radius"?: SceneSpecSchema.PositiveNumber; "depth"?: SceneSpecSchema.PositiveNumber; "segments"?: number; "ringCount"?: number; "majorRadius"?: SceneSpecSchema.PositiveNumber; "minorRadius"?: SceneSpecSchema.PositiveNumber; "bevel"?: { "width"?: SceneSpecSchema.PositiveNumber; "segments"?: number; }; "profile"?: Array<[number, number]>; "closedProfile"?: boolean; "capEnds"?: boolean; "path"?: Array<SceneSpecSchema.Vec3>; "pathClosed"?: boolean; "pathInterpolation"?: "poly" | "bezier"; "curveResolution"?: number; "bevelResolution"?: number; "height"?: SceneSpecSchema.PositiveNumber; "wallThickness"?: SceneSpecSchema.PositiveNumber; "baseThickness"?: SceneSpecSchema.PositiveNumber; "footRound"?: SceneSpecSchema.PositiveNumber; "handleRadius"?: SceneSpecSchema.PositiveNumber; "handleLower"?: SceneSpecSchema.PositiveNumber; "handleUpper"?: SceneSpecSchema.PositiveNumber; "rootRadius"?: SceneSpecSchema.PositiveNumber; "rootLength"?: SceneSpecSchema.PositiveNumber; "sectionSegments"?: number; "handleSegments"?: number; "rootSegments"?: number; "wallRows"?: number; "rootTension"?: number; };
|
|
19
|
+
export type Entity = { "id": SceneSpecSchema.Id; "type": "asset-instance" | "generator" | "empty"; "assetId"?: string; "armatureId"?: string; "rigidBody"?: SceneSpecSchema.RigidBody; "cloth"?: SceneSpecSchema.Cloth; "softBody"?: SceneSpecSchema.SoftBody; "fluid"?: SceneSpecSchema.Fluid; "generator"?: SceneSpecSchema.Generator; "materialId"?: string; "transform"?: SceneSpecSchema.Transform; "tags"?: Array<string>; "locked"?: boolean; "visible"?: boolean; "modifiers"?: Array<SceneSpecSchema.ModelModifier>; "materialBindings"?: SceneSpecSchema.MaterialBindings; };
|
|
20
|
+
export type Light = { "id": SceneSpecSchema.Id; "type": "area" | "point" | "spot" | "sun"; "transform"?: SceneSpecSchema.Transform; "energy"?: SceneSpecSchema.NonNegativeNumber; "color"?: SceneSpecSchema.UnitColor; "size"?: SceneSpecSchema.PositiveNumber; "spotSize"?: number; "spotBlend"?: number; "angle"?: number; };
|
|
21
|
+
export type Camera = { "id": SceneSpecSchema.Id; "role"?: "active-camera" | "three-quarter" | "top" | "detail"; "lens"?: SceneSpecSchema.PositiveNumber; "sensorWidth"?: SceneSpecSchema.PositiveNumber; "clipping"?: [SceneSpecSchema.PositiveNumber, SceneSpecSchema.PositiveNumber]; "transform"?: SceneSpecSchema.Transform; "targetEntityId"?: string; "targetPoint"?: SceneSpecSchema.Vec3; "fStop"?: SceneSpecSchema.PositiveNumber; };
|
|
22
|
+
export type Shot = { "id": SceneSpecSchema.Id; "cameraId": string; "frameRange"?: [number, number]; "description"?: string; };
|
|
23
|
+
export type AnimationKeyframe = { "frame": number; "value": number; "interpolation"?: "constant" | "linear" | "bezier" | "ease_in" | "ease_out" | "ease_in_out"; };
|
|
24
|
+
export type AnimationTrack = { "id": SceneSpecSchema.Id; "targetKind"?: "entity" | "camera" | "material" | "bone"; "targetEntityId": string; "boneName"?: string; "property": "location.x" | "location.y" | "location.z" | "rotationEuler.x" | "rotationEuler.y" | "rotationEuler.z" | "scale.x" | "scale.y" | "scale.z" | "emissionStrength" | "roughness" | "metallic" | "ior" | "alpha" | "coatWeight" | "transmissionWeight" | "baseColor.r" | "baseColor.g" | "baseColor.b" | "emissionColor.r" | "emissionColor.g" | "emissionColor.b" | "anisotropic" | "anisotropicRotation"; "keyframes": Array<SceneSpecSchema.AnimationKeyframe>; };
|
|
25
|
+
export type RenderProfile = { "engine": "eevee" | "cycles" | "workbench"; "resolution": [number, number]; "samples"?: number; "filmTransparent"?: boolean; "raytracing"?: boolean; "colorManagement"?: { "viewTransform"?: "Standard" | "AgX" | "Filmic" | "False Color" | "Raw"; "look"?: string; "exposure"?: number; }; "maxSamplesBudget"?: number; };
|
|
26
|
+
export type RenderProfiles = { "preview": SceneSpecSchema.RenderProfile; "final"?: SceneSpecSchema.RenderProfile; };
|
|
27
|
+
export type Bone = { "name": string; "head": SceneSpecSchema.Vec3; "tail": SceneSpecSchema.Vec3; "parent"?: string; };
|
|
28
|
+
export type Armature = { "id": SceneSpecSchema.Id; "bones": Array<SceneSpecSchema.Bone>; "transform"?: SceneSpecSchema.Transform; };
|
|
29
|
+
export type RigidBody = { "kind": "active" | "passive"; "mass"?: number; "friction"?: number; };
|
|
30
|
+
export type Cloth = { "pinTop"?: boolean; "mass"?: number; "stiffness"?: number; };
|
|
31
|
+
export type SoftBody = { "pinTop"?: boolean; "goal"?: number; "mass"?: number; };
|
|
32
|
+
export type Fluid = { "role": "domain" | "inflow" | "outflow"; "domainType"?: "liquid" | "gas"; "resolution"?: number; "flowType"?: "liquid" | "smoke" | "fire" | "both"; };
|
|
33
|
+
export type ModelModifier = ({ "type": "solidify"; "thickness": number; "offset"?: number; }) | ({ "type": "mirror"; "axis": "x" | "y" | "z"; "merge"?: boolean; }) | ({ "type": "array"; "count": number; "offset": SceneSpecSchema.Vec3; }) | ({ "type": "boolean"; "operation": "union" | "difference" | "intersect"; "targetEntityId": SceneSpecSchema.Id; }) | ({ "type": "bevel"; "width": number; "segments"?: number; "angle"?: number; "miterInner"?: "arc" | "sharp"; });
|
|
34
|
+
export type ImageBinding = { "assetId": SceneSpecSchema.Id; "scale"?: SceneSpecSchema.Vec3; "offset"?: SceneSpecSchema.Vec3; "uvMap"?: string; "channel"?: "r" | "g" | "b" | "a"; "strength"?: number; };
|
|
35
|
+
export type MaterialImages = { "baseColor"?: SceneSpecSchema.ImageBinding; "roughness"?: SceneSpecSchema.ImageBinding; "metallic"?: SceneSpecSchema.ImageBinding; "normal"?: SceneSpecSchema.ImageBinding; "alpha"?: SceneSpecSchema.ImageBinding; "emissionColor"?: SceneSpecSchema.ImageBinding; };
|
|
36
|
+
export type MaterialBinding = { "partId": string; "slotIndex"?: number; "materialId": SceneSpecSchema.Id; };
|
|
37
|
+
export type MaterialBindings = Array<SceneSpecSchema.MaterialBinding>;
|
|
38
|
+
export type MaterialTangent = ({ "mode": "uv"; "uvMap": string; }) | ({ "mode": "radial"; "axis": "x" | "y" | "z"; });
|
|
39
|
+
export type ReferenceImage = { "id": SceneSpecSchema.Id; "assetId": SceneSpecSchema.Id; "sha256": string; "label": string; "purposes": Array<"geometry" | "materials" | "lighting" | "goalFit">; "notes"?: string; };
|
|
40
|
+
export type ReferenceImages = Array<SceneSpecSchema.ReferenceImage>;
|
|
41
|
+
export type Document = { "schemaVersion": "deepblend.scene/v1"; "project": SceneSpecSchema.Project; "assets"?: Array<SceneSpecSchema.Asset>; "armatures"?: Array<SceneSpecSchema.Armature>; "simulation"?: { "gravity"?: number; "bake"?: boolean; }; "materials"?: Array<SceneSpecSchema.Material>; "entities": Array<SceneSpecSchema.Entity>; "lights"?: Array<SceneSpecSchema.Light>; "cameras": Array<SceneSpecSchema.Camera>; "shots"?: Array<SceneSpecSchema.Shot>; "animationTracks"?: Array<SceneSpecSchema.AnimationTrack>; "renderProfiles": SceneSpecSchema.RenderProfiles; "world"?: SceneSpecSchema.World; };
|
|
42
|
+
}
|
|
43
|
+
export type SceneSpec = SceneSpecSchema.Document;
|
|
44
|
+
|
|
45
|
+
/** Source SHA-256: e71dceb0a4aa93c664dbb8d894a1ffb156f4db13bc34ae4f0fdb2b10db386c06 */
|
|
46
|
+
export namespace ScenePatchSchema {
|
|
47
|
+
export type Id = string;
|
|
48
|
+
export type License = { "source"?: string; "commercialUse"?: boolean; "attribution"?: string; };
|
|
49
|
+
export type Vec3 = [number, number, number];
|
|
50
|
+
export type World = { "color"?: ScenePatchSchema.UnitColor; "strength"?: number; "environment"?: { "assetId": ScenePatchSchema.Id; "rotation"?: number; }; };
|
|
51
|
+
export type UnitColor = Array<number>;
|
|
52
|
+
export type PositiveNumber = number;
|
|
53
|
+
export type NonNegativeNumber = number;
|
|
54
|
+
export type TransformPatch = { "location"?: ScenePatchSchema.Vec3; "rotationEuler"?: ScenePatchSchema.Vec3; "scale"?: ScenePatchSchema.Vec3; };
|
|
55
|
+
export type MaterialParameters = { "baseColor"?: ScenePatchSchema.UnitColor; "metallic"?: number; "roughness"?: number; "ior"?: number; "alpha"?: number; "emissionColor"?: ScenePatchSchema.UnitColor; "emissionStrength"?: ScenePatchSchema.NonNegativeNumber; "coatWeight"?: number; "transmissionWeight"?: number; "anisotropic"?: number; "anisotropicRotation"?: number; };
|
|
56
|
+
export type ProceduralTexture = ({ "type": "noise" | "wave" | "voronoi"; "scale": number; "detail"?: number; "distortion"?: number; "stretch"?: [number, number, number]; "bump"?: number; "roughnessVariation"?: number; "colorVariation"?: number; "coordinates"?: "object"; }) | ({ "type": "noise" | "wave" | "voronoi"; "scale": number; "detail"?: number; "distortion"?: number; "stretch"?: [number, number, number]; "bump"?: number; "roughnessVariation"?: number; "colorVariation"?: number; "coordinates": "uv"; "uvMap"?: string; });
|
|
57
|
+
export type Generator = { "shape": "cube" | "uv_sphere" | "cylinder" | "cone" | "plane" | "torus" | "rounded_box" | "lathe" | "curve" | "handled_cup"; "size"?: ScenePatchSchema.PositiveNumber; "radius"?: ScenePatchSchema.PositiveNumber; "depth"?: ScenePatchSchema.PositiveNumber; "segments"?: number; "ringCount"?: number; "majorRadius"?: ScenePatchSchema.PositiveNumber; "minorRadius"?: ScenePatchSchema.PositiveNumber; "bevel"?: { "width"?: ScenePatchSchema.PositiveNumber; "segments"?: number; }; "profile"?: Array<[number, number]>; "closedProfile"?: boolean; "capEnds"?: boolean; "path"?: Array<ScenePatchSchema.Vec3>; "pathClosed"?: boolean; "pathInterpolation"?: "poly" | "bezier"; "curveResolution"?: number; "bevelResolution"?: number; "height"?: ScenePatchSchema.PositiveNumber; "wallThickness"?: ScenePatchSchema.PositiveNumber; "baseThickness"?: ScenePatchSchema.PositiveNumber; "footRound"?: ScenePatchSchema.PositiveNumber; "handleRadius"?: ScenePatchSchema.PositiveNumber; "handleLower"?: ScenePatchSchema.PositiveNumber; "handleUpper"?: ScenePatchSchema.PositiveNumber; "rootRadius"?: ScenePatchSchema.PositiveNumber; "rootLength"?: ScenePatchSchema.PositiveNumber; "sectionSegments"?: number; "handleSegments"?: number; "rootSegments"?: number; "wallRows"?: number; "rootTension"?: number; };
|
|
58
|
+
export type Entity = { "id": ScenePatchSchema.Id; "type": "asset-instance" | "generator" | "empty"; "assetId"?: string; "generator"?: ScenePatchSchema.Generator; "materialId"?: string; "transform"?: { "location"?: ScenePatchSchema.Vec3; "rotationEuler"?: ScenePatchSchema.Vec3; "scale"?: ScenePatchSchema.Vec3; }; "tags"?: Array<string>; "locked"?: boolean; "visible"?: boolean; "modifiers"?: Array<ScenePatchSchema.ModelModifier>; "materialBindings"?: ScenePatchSchema.MaterialBindings; };
|
|
59
|
+
export type Light = { "id": ScenePatchSchema.Id; "type": "area" | "point" | "spot" | "sun"; "transform"?: { "location"?: ScenePatchSchema.Vec3; "rotationEuler"?: ScenePatchSchema.Vec3; "scale"?: ScenePatchSchema.Vec3; }; "energy"?: ScenePatchSchema.NonNegativeNumber; "color"?: ScenePatchSchema.UnitColor; "size"?: ScenePatchSchema.PositiveNumber; "spotSize"?: number; "spotBlend"?: number; "angle"?: number; };
|
|
60
|
+
export type Camera = { "id": ScenePatchSchema.Id; "role"?: "active-camera" | "three-quarter" | "top" | "detail"; "lens"?: ScenePatchSchema.PositiveNumber; "sensorWidth"?: ScenePatchSchema.PositiveNumber; "clipping"?: [ScenePatchSchema.PositiveNumber, ScenePatchSchema.PositiveNumber]; "transform"?: { "location"?: ScenePatchSchema.Vec3; "rotationEuler"?: ScenePatchSchema.Vec3; "scale"?: ScenePatchSchema.Vec3; }; "targetEntityId"?: string; "targetPoint"?: ScenePatchSchema.Vec3; "fStop"?: ScenePatchSchema.PositiveNumber; };
|
|
61
|
+
export type AnimationTrack = { "id": ScenePatchSchema.Id; "targetKind"?: "entity" | "camera" | "material"; "targetEntityId": string; "property": "location.x" | "location.y" | "location.z" | "rotationEuler.x" | "rotationEuler.y" | "rotationEuler.z" | "scale.x" | "scale.y" | "scale.z" | "emissionStrength" | "roughness" | "metallic" | "ior" | "alpha" | "coatWeight" | "transmissionWeight" | "baseColor.r" | "baseColor.g" | "baseColor.b" | "emissionColor.r" | "emissionColor.g" | "emissionColor.b" | "anisotropic" | "anisotropicRotation"; "keyframes": Array<{ "frame": number; "value": number; "interpolation"?: "constant" | "linear" | "bezier" | "ease_in" | "ease_out" | "ease_in_out"; }>; };
|
|
62
|
+
export type RenderProfile = { "engine"?: "eevee" | "cycles" | "workbench"; "resolution"?: [number, number]; "samples"?: number; "filmTransparent"?: boolean; "colorManagement"?: { "viewTransform"?: "Standard" | "AgX" | "Filmic" | "False Color" | "Raw"; "look"?: string; "exposure"?: number; }; "maxSamplesBudget"?: number; "raytracing"?: boolean; };
|
|
63
|
+
export type Operation = ({ "op": "entity.transform.update"; "entityId": ScenePatchSchema.Id; "location"?: ScenePatchSchema.Vec3; "rotationEuler"?: ScenePatchSchema.Vec3; "scale"?: ScenePatchSchema.Vec3; }) | ({ "op": "entity.visibility.set"; "entityId": ScenePatchSchema.Id; "visible": boolean; }) | ({ "op": "entity.tags.set"; "entityId": ScenePatchSchema.Id; "tags": Array<string>; }) | ({ "op": "entity.add"; "entity": ScenePatchSchema.Entity; }) | ({ "op": "entity.remove"; "entityId": ScenePatchSchema.Id; }) | ({ "op": "entity.material.set"; "entityId": ScenePatchSchema.Id; "materialId": string | null; }) | ({ "op": "material.texture.set"; "materialId": ScenePatchSchema.Id; "texture": (ScenePatchSchema.ProceduralTexture) | (null); }) | ({ "op": "material.add"; "material": { "id": ScenePatchSchema.Id; "shader": "principled" | "emission" | "glass"; "parameters"?: ScenePatchSchema.MaterialParameters; "texture"?: ScenePatchSchema.ProceduralTexture; "images"?: ScenePatchSchema.MaterialImages; "tangent"?: ScenePatchSchema.MaterialTangent; }; }) | ({ "op": "material.parameter.update"; "materialId": ScenePatchSchema.Id; "parameter": "baseColor" | "metallic" | "roughness" | "ior" | "alpha" | "emissionColor" | "emissionStrength" | "coatWeight" | "transmissionWeight" | "anisotropic" | "anisotropicRotation"; "value": (number) | (ScenePatchSchema.UnitColor); }) | ({ "op": "light.add"; "light": ScenePatchSchema.Light; }) | ({ "op": "light.update"; "lightId": ScenePatchSchema.Id; "transform"?: { "location"?: ScenePatchSchema.Vec3; "rotationEuler"?: ScenePatchSchema.Vec3; "scale"?: ScenePatchSchema.Vec3; }; "energy"?: ScenePatchSchema.NonNegativeNumber; "color"?: ScenePatchSchema.UnitColor; "size"?: ScenePatchSchema.PositiveNumber; "spotSize"?: number; "spotBlend"?: number; "angle"?: number; }) | ({ "op": "light.remove"; "lightId": ScenePatchSchema.Id; }) | ({ "op": "camera.add"; "camera": ScenePatchSchema.Camera; }) | ({ "op": "camera.update"; "cameraId": ScenePatchSchema.Id; "role"?: "active-camera" | "three-quarter" | "top" | "detail"; "lens"?: ScenePatchSchema.PositiveNumber; "sensorWidth"?: ScenePatchSchema.PositiveNumber; "clipping"?: [ScenePatchSchema.PositiveNumber, ScenePatchSchema.PositiveNumber]; "transform"?: { "location"?: ScenePatchSchema.Vec3; "rotationEuler"?: ScenePatchSchema.Vec3; "scale"?: ScenePatchSchema.Vec3; }; "targetEntityId"?: string; "targetPoint"?: ScenePatchSchema.Vec3; "fStop"?: ScenePatchSchema.PositiveNumber; }) | ({ "op": "camera.remove"; "cameraId": ScenePatchSchema.Id; }) | ({ "op": "animation.track.set"; "track": ScenePatchSchema.AnimationTrack; }) | ({ "op": "animation.track.remove"; "trackId": ScenePatchSchema.Id; }) | ({ "op": "shot.set"; "shot": { "id": ScenePatchSchema.Id; "cameraId": string; "frameRange"?: [number, number]; "description"?: string; }; }) | ({ "op": "shot.remove"; "shotId": ScenePatchSchema.Id; }) | ({ "op": "project.frameRange.set"; "frameStart": number; "frameEnd": number; "fps"?: number; }) | ({ "op": "render.profile.set"; "profileName": "preview" | "final"; "profile": ScenePatchSchema.RenderProfile; }) | ({ "op": "world.set"; "world": ScenePatchSchema.World; }) | ({ "op": "asset.add"; "asset": { "id": ScenePatchSchema.Id; "type": "glb" | "gltf" | "fbx" | "obj" | "usd" | "blend" | "png" | "jpg" | "jpeg" | "hdr" | "exr"; "path": string; "sha256"?: string; "license"?: ScenePatchSchema.License; }; }) | ({ "op": "asset.remove"; "assetId": ScenePatchSchema.Id; }) | ({ "op": "entity.generator.set"; "entityId": ScenePatchSchema.Id; "generator": ScenePatchSchema.Generator; }) | ({ "op": "entity.modifiers.set"; "entityId": ScenePatchSchema.Id; "modifiers": Array<ScenePatchSchema.ModelModifier>; }) | ({ "op": "material.images.set"; "materialId": ScenePatchSchema.Id; "images": (ScenePatchSchema.MaterialImages) | (null); }) | ({ "op": "entity.materialBindings.set"; "entityId": ScenePatchSchema.Id; "materialBindings": ScenePatchSchema.MaterialBindings; }) | ({ "op": "material.tangent.set"; "materialId": ScenePatchSchema.Id; "tangent": (ScenePatchSchema.MaterialTangent) | (null); }) | ({ "op": "project.brief.set"; "goal": string; "referenceImages": ScenePatchSchema.ReferenceImages; }) | ({ "op": "project.reviewSubject.set"; "entityId": (ScenePatchSchema.Id) | (null); });
|
|
64
|
+
export type ModelModifier = ({ "type": "solidify"; "thickness": number; "offset"?: number; }) | ({ "type": "mirror"; "axis": "x" | "y" | "z"; "merge"?: boolean; }) | ({ "type": "array"; "count": number; "offset": ScenePatchSchema.Vec3; }) | ({ "type": "boolean"; "operation": "union" | "difference" | "intersect"; "targetEntityId": ScenePatchSchema.Id; }) | ({ "type": "bevel"; "width": number; "segments"?: number; "angle"?: number; "miterInner"?: "arc" | "sharp"; });
|
|
65
|
+
export type ImageBinding = { "assetId": ScenePatchSchema.Id; "scale"?: ScenePatchSchema.Vec3; "offset"?: ScenePatchSchema.Vec3; "uvMap"?: string; "channel"?: "r" | "g" | "b" | "a"; "strength"?: number; };
|
|
66
|
+
export type MaterialImages = { "baseColor"?: ScenePatchSchema.ImageBinding; "roughness"?: ScenePatchSchema.ImageBinding; "metallic"?: ScenePatchSchema.ImageBinding; "normal"?: ScenePatchSchema.ImageBinding; "alpha"?: ScenePatchSchema.ImageBinding; "emissionColor"?: ScenePatchSchema.ImageBinding; };
|
|
67
|
+
export type MaterialBinding = { "partId": string; "slotIndex"?: number; "materialId": ScenePatchSchema.Id; };
|
|
68
|
+
export type MaterialBindings = Array<ScenePatchSchema.MaterialBinding>;
|
|
69
|
+
export type MaterialTangent = ({ "mode": "uv"; "uvMap": string; }) | ({ "mode": "radial"; "axis": "x" | "y" | "z"; });
|
|
70
|
+
export type ReferenceImage = { "id": ScenePatchSchema.Id; "assetId": ScenePatchSchema.Id; "sha256": string; "label": string; "purposes": Array<"geometry" | "materials" | "lighting" | "goalFit">; "notes"?: string; };
|
|
71
|
+
export type ReferenceImages = Array<ScenePatchSchema.ReferenceImage>;
|
|
72
|
+
export type Document = { "projectId": string; "baseRevision": string; "idempotencyKey"?: string; "operations": Array<ScenePatchSchema.Operation>; "saveCheckpoint"?: boolean; "renderPreview"?: boolean; "actor"?: string; "stage"?: string; "note"?: string; };
|
|
73
|
+
}
|
|
74
|
+
export type ScenePatch = ScenePatchSchema.Document;
|
|
75
|
+
|
|
76
|
+
/** Source SHA-256: d1f5b189bbf0e5cab018be87ce52757acfd208c24b1fa07d087947ed98e20ad5 */
|
|
77
|
+
export namespace RecipeSchema {
|
|
78
|
+
export type Binding = ({ "kind": "material"; "materialId": string; "property": "baseColor" | "roughness"; }) | ({ "kind": "render-profile"; "profile": "preview" | "final"; "property": "exposure"; });
|
|
79
|
+
export type ColorParameter = { "id": string; "title": string; "description"?: string; "bindings": Array<RecipeSchema.Binding>; "type": "color"; "default": [number, number, number]; };
|
|
80
|
+
export type NumberParameter = { "id": string; "title": string; "description"?: string; "bindings": Array<RecipeSchema.Binding>; "type": "number"; "default": number; "minimum": number; "maximum": number; };
|
|
81
|
+
export type Document = { "schemaVersion": "deepblend.recipe/v1"; "id": string; "version": string; "title": string; "description": string; "author": { "name": string; "url"?: string; }; "license": string; "source": { "url": string; "note"?: string; }; "compatibility": { "sceneSchemaVersion": "deepblend.scene/v1"; "capabilities": Array<"geometry.primitive" | "geometry.lathe" | "geometry.curve" | "geometry.handled_cup" | "geometry.handled_cup.tension" | "geometry.handled_cup.tension.extended" | "geometry.modifiers" | "material.principled" | "material.glass" | "material.emission" | "material.procedural" | "material.procedural.uv" | "material.anisotropy" | "animation.transform" | "animation.material">; }; "input": { "path": "scene-spec.json"; "sha256": string; }; "preview": { "path": "preview.png"; "sha256": string; "mediaType": "image/png"; "alt": string; }; "parameters": Array<(RecipeSchema.ColorParameter) | (RecipeSchema.NumberParameter)>; };
|
|
82
|
+
}
|
|
83
|
+
export type RecipeManifest = RecipeSchema.Document;
|
|
84
|
+
export type Entity = SceneSpecSchema.Entity;
|
|
85
|
+
export type Generator = SceneSpecSchema.Generator;
|
|
86
|
+
export type ModelModifier = SceneSpecSchema.ModelModifier;
|
|
87
|
+
export type Material = SceneSpecSchema.Material;
|
|
88
|
+
export type Asset = SceneSpecSchema.Asset;
|
|
89
|
+
export type Camera = SceneSpecSchema.Camera;
|
|
90
|
+
export type Light = SceneSpecSchema.Light;
|
|
91
|
+
export type Transform = SceneSpecSchema.Transform;
|
|
92
|
+
export type AnimationTrack = SceneSpecSchema.AnimationTrack;
|
|
93
|
+
export type RenderProfile = SceneSpecSchema.RenderProfile;
|
|
94
|
+
export type ReferenceImage = SceneSpecSchema.ReferenceImage;
|
|
95
|
+
export type MaterialBinding = SceneSpecSchema.MaterialBinding;
|
|
96
|
+
export type MaterialTangent = SceneSpecSchema.MaterialTangent;
|
|
97
|
+
export type Vec3 = SceneSpecSchema.Vec3;
|
|
98
|
+
export type ScenePatchOperation = ScenePatchSchema.Operation;
|
|
99
|
+
export type RecipeCapability = RecipeManifest["compatibility"]["capabilities"][number];
|
|
100
|
+
export interface BlenderErrorCodeMap {
|
|
101
|
+
readonly NOT_FOUND: "BLENDER_NOT_FOUND";
|
|
102
|
+
readonly EXECUTABLE_NOT_EXECUTABLE: "BLENDER_EXECUTABLE_NOT_EXECUTABLE";
|
|
103
|
+
readonly EXECUTABLE_OUTSIDE_ALLOWLIST: "BLENDER_EXECUTABLE_OUTSIDE_ALLOWLIST";
|
|
104
|
+
readonly SPAWN_FAILED: "BLENDER_SPAWN_FAILED";
|
|
105
|
+
readonly NONZERO_EXIT: "BLENDER_NONZERO_EXIT";
|
|
106
|
+
readonly TIMEOUT: "BLENDER_TIMEOUT";
|
|
107
|
+
readonly ABORTED: "BLENDER_ABORTED";
|
|
108
|
+
readonly RESULT_MISSING: "BLENDER_RESULT_MISSING";
|
|
109
|
+
readonly RESULT_UNPARSEABLE: "BLENDER_RESULT_UNPARSEABLE";
|
|
110
|
+
readonly SCRIPT_ERROR: "BLENDER_SCRIPT_ERROR";
|
|
111
|
+
readonly PROTOCOL_VERSION_MISMATCH: "BLENDER_PROTOCOL_VERSION_MISMATCH";
|
|
112
|
+
readonly UNSUPPORTED_ACTION: "BLENDER_UNSUPPORTED_ACTION";
|
|
113
|
+
readonly CAPABILITY_PROBE_FAILED: "BLENDER_CAPABILITY_PROBE_FAILED";
|
|
114
|
+
readonly BOOTSTRAP_MISSING: "BLENDER_BOOTSTRAP_MISSING";
|
|
115
|
+
readonly ENGINE_UNAVAILABLE: "BLENDER_ENGINE_UNAVAILABLE";
|
|
116
|
+
readonly RUNTIME_UNAVAILABLE: "BLENDER_RUNTIME_UNAVAILABLE";
|
|
117
|
+
readonly PROJECT_NOT_FOUND: "PROJECT_NOT_FOUND";
|
|
118
|
+
readonly PROJECT_CORRUPT: "PROJECT_CORRUPT";
|
|
119
|
+
readonly PROJECT_EXISTS: "PROJECT_EXISTS";
|
|
120
|
+
readonly PROJECT_ID_INVALID: "PROJECT_ID_INVALID";
|
|
121
|
+
readonly REVISION_NOT_FOUND: "REVISION_NOT_FOUND";
|
|
122
|
+
readonly REVISION_CORRUPT: "REVISION_CORRUPT";
|
|
123
|
+
readonly REVISION_CHECKPOINT_MISSING: "REVISION_CHECKPOINT_MISSING";
|
|
124
|
+
readonly REVISION_CONFLICT: "REVISION_CONFLICT";
|
|
125
|
+
readonly REVISION_ALLOCATION_FAILED: "REVISION_ALLOCATION_FAILED";
|
|
126
|
+
readonly REVISION_ID_INVALID: "REVISION_ID_INVALID";
|
|
127
|
+
readonly SCENE_SPEC_INVALID: "SCENE_SPEC_INVALID";
|
|
128
|
+
readonly SCENE_PATCH_INVALID: "SCENE_PATCH_INVALID";
|
|
129
|
+
readonly SCENE_PATCH_REJECTED: "SCENE_PATCH_REJECTED";
|
|
130
|
+
readonly SCENE_VALIDATION_FAILED: "SCENE_VALIDATION_FAILED";
|
|
131
|
+
readonly SCENE_CAMERA_MISSING: "SCENE_CAMERA_MISSING";
|
|
132
|
+
readonly PATH_OUTSIDE_WORKSPACE: "PATH_OUTSIDE_WORKSPACE";
|
|
133
|
+
readonly PATH_SEGMENT_INVALID: "PATH_SEGMENT_INVALID";
|
|
134
|
+
readonly ASSET_MISSING: "ASSET_MISSING";
|
|
135
|
+
readonly ASSET_FORMAT_UNAVAILABLE: "ASSET_FORMAT_UNAVAILABLE";
|
|
136
|
+
readonly ASSET_HASH_MISMATCH: "ASSET_HASH_MISMATCH";
|
|
137
|
+
readonly ASSET_REQUEST_INVALID: "ASSET_REQUEST_INVALID";
|
|
138
|
+
readonly ASSET_SOURCE_NOT_FOUND: "ASSET_SOURCE_NOT_FOUND";
|
|
139
|
+
readonly ASSET_TOO_LARGE: "ASSET_TOO_LARGE";
|
|
140
|
+
readonly ASSET_FETCH_FAILED: "ASSET_FETCH_FAILED";
|
|
141
|
+
readonly ASSET_CONTENT_MISMATCH: "ASSET_CONTENT_MISMATCH";
|
|
142
|
+
readonly DISK_FULL: "DISK_FULL";
|
|
143
|
+
readonly SCENE_TOO_HEAVY: "SCENE_TOO_HEAVY";
|
|
144
|
+
readonly ASSET_APPROVAL_REQUIRED: "ASSET_APPROVAL_REQUIRED";
|
|
145
|
+
readonly RENDER_PROFILE_MISSING: "RENDER_PROFILE_MISSING";
|
|
146
|
+
readonly VISUAL_REVIEW_MODEL_UNAVAILABLE: "VISUAL_REVIEW_MODEL_UNAVAILABLE";
|
|
147
|
+
readonly RENDER_BUDGET_EXCEEDED: "RENDER_BUDGET_EXCEEDED";
|
|
148
|
+
readonly RENDER_NO_OUTPUT: "RENDER_NO_OUTPUT";
|
|
149
|
+
readonly RENDER_JOB_NOT_FOUND: "RENDER_JOB_NOT_FOUND";
|
|
150
|
+
readonly RENDER_JOB_CONFLICT: "RENDER_JOB_CONFLICT";
|
|
151
|
+
readonly RENDER_JOB_STATE_INVALID: "RENDER_JOB_STATE_INVALID";
|
|
152
|
+
readonly RENDER_RANGE_INVALID: "RENDER_RANGE_INVALID";
|
|
153
|
+
readonly RENDER_FRAMES_INCOMPLETE: "RENDER_FRAMES_INCOMPLETE";
|
|
154
|
+
readonly ENCODER_NOT_FOUND: "ENCODER_NOT_FOUND";
|
|
155
|
+
readonly ENCODE_FAILED: "ENCODE_FAILED";
|
|
156
|
+
readonly ENCODE_VERIFY_FAILED: "ENCODE_VERIFY_FAILED";
|
|
157
|
+
readonly PROBE_FAILED: "PROBE_FAILED";
|
|
158
|
+
readonly DELIVERY_INCOMPLETE: "DELIVERY_INCOMPLETE";
|
|
159
|
+
readonly EXPORT_IN_PROGRESS: "EXPORT_IN_PROGRESS";
|
|
160
|
+
readonly RENDER_APPROVAL_REQUIRED: "RENDER_APPROVAL_REQUIRED";
|
|
161
|
+
readonly UI_ROUTE_NOT_FOUND: "UI_ROUTE_NOT_FOUND";
|
|
162
|
+
readonly ARTIFACT_NOT_FOUND: "ARTIFACT_NOT_FOUND";
|
|
163
|
+
readonly UI_REQUEST_FAILED: "UI_REQUEST_FAILED";
|
|
164
|
+
readonly UI_HOST_API_STALE: "UI_HOST_API_STALE";
|
|
165
|
+
}
|
|
166
|
+
export interface BlenderWarningCodeMap {
|
|
167
|
+
readonly BLENDER_NOT_INSTALLED: "BLENDER_NOT_INSTALLED";
|
|
168
|
+
readonly ENGINE_NOT_IN_STATIC_ENUM: "ENGINE_NOT_IN_STATIC_ENUM";
|
|
169
|
+
readonly ENGINE_UNAVAILABLE: "ENGINE_UNAVAILABLE";
|
|
170
|
+
readonly ENGINE_DOWNGRADED: "ENGINE_DOWNGRADED";
|
|
171
|
+
readonly GPU_UNAVAILABLE: "GPU_UNAVAILABLE";
|
|
172
|
+
readonly FORMAT_UNAVAILABLE: "FORMAT_UNAVAILABLE";
|
|
173
|
+
readonly ADDON_ENABLE_FAILED: "ADDON_ENABLE_FAILED";
|
|
174
|
+
readonly PROBE_WARNING: "PROBE_WARNING";
|
|
175
|
+
readonly STALE_CAPABILITIES: "STALE_CAPABILITIES";
|
|
176
|
+
readonly SCENE_PATCH_NO_CHANGE: "SCENE_PATCH_NO_CHANGE";
|
|
177
|
+
readonly RENDER_ENGINE_DOWNGRADED: "RENDER_ENGINE_DOWNGRADED";
|
|
178
|
+
readonly RENDER_SAMPLES_REDUCED: "RENDER_SAMPLES_REDUCED";
|
|
179
|
+
readonly SCENE_COMPILER_DECISION: "SCENE_COMPILER_DECISION";
|
|
180
|
+
readonly SCENE_ASSET_NOT_INGESTED: "SCENE_ASSET_NOT_INGESTED";
|
|
181
|
+
readonly SCENE_ANIMATION_KEYFRAMES_ADJUSTED: "SCENE_ANIMATION_KEYFRAMES_ADJUSTED";
|
|
182
|
+
readonly JOB_PROJECTION_UNAVAILABLE: "JOB_PROJECTION_UNAVAILABLE";
|
|
183
|
+
readonly DELIVERY_CLAIM_UNAVAILABLE: "DELIVERY_CLAIM_UNAVAILABLE";
|
|
184
|
+
readonly JOURNAL_INCOMPLETE: "JOURNAL_INCOMPLETE";
|
|
185
|
+
}
|