@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/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 !== 'empty' && entity.materialId === undefined) {
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
- * `project.goal` and `project.title` are excluded on purpose: the digest exists
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 `project.title` and `project.goal` so
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
+ }