@deepblend/dsh-blender-host 0.12.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,39 @@
1
+ /** Continue one recognized default hero using an actual Blender vertex/frustum measurement. */
2
+ import { BlenderError } from '@deepblend/dsh-blender-contracts'
3
+ const vector = value => Array.isArray(value) && value.length === 3 && value.every(Number.isFinite)
4
+ const near = (a, b) => vector(a) && vector(b) && a.every((value, axis) => Math.abs(value-b[axis]) <= 1e-6)
5
+ const refuse = message => { throw new BlenderError('PROTOCOL_VERSION_MISMATCH', `Cannot verify recipe hero framing: ${message}`) }
6
+
7
+ export function fitCupHeroFromNative(spec, report, selector) {
8
+ if (!selector) return null
9
+ const camera = spec.cameras.find(item => item.id === selector.cameraId)
10
+ const native = report?.cameraFraming
11
+ if (selector.cameraId !== 'hero' || selector.entityId !== 'cup' || selector.margin !== 0.03
12
+ || !camera || !vector(camera.targetPoint) || !vector(camera.transform.location)
13
+ || native?.schemaVersion !== 'deepblend.camera-framing/v1'
14
+ || native.entityId !== selector.entityId || native.cameraId !== selector.cameraId
15
+ || native.margin !== selector.margin || native.cameraType !== 'PERSP'
16
+ || !near(native.cameraLocation, camera.transform.location)
17
+ || !Number.isFinite(native.lens) || Math.abs(native.lens-camera.lens) > 1e-6
18
+ || !Number.isFinite(native.sensorWidth) || Math.abs(native.sensorWidth-(camera.sensorWidth??36)) > 1e-6
19
+ || !Number.isFinite(native.backoffM) || native.backoffM < 0
20
+ || !Number.isInteger(native.vertices) || native.vertices < 1 || native.vertices > 2000000
21
+ || native.scope !== 'evaluated-cup-vertices-at-reported-frame-and-render-aspect'
22
+ || !Number.isInteger(native.frame) || native.frame !== report.renderConfig?.frameStart
23
+ || native.resolutionPercentage !== 100 || !Array.isArray(native.pixelAspect)
24
+ || native.pixelAspect.length !== 2 || !native.pixelAspect.every(value => Number.isFinite(value) && Math.abs(value-1) <= 1e-9)
25
+ || !Number.isFinite(native.nearestDepthM) || native.nearestDepthM <= 0
26
+ || !Number.isFinite(native.farthestDepthM) || native.farthestDepthM < native.nearestDepthM
27
+ || native.nearestDepthM <= (camera.clipping?.[0] ?? 0.1)
28
+ || native.farthestDepthM >= (camera.clipping?.[1] ?? 1000)
29
+ || !Array.isArray(native.resolution) || native.resolution.length !== 2
30
+ || !native.resolution.every((value, axis) => Number.isInteger(value) && value > 0 && value === report.renderConfig?.resolution?.[axis])) refuse('missing or inconsistent measured facts')
31
+ if (spec.animationTracks.some(track => track.targetKind === 'camera' && track.targetEntityId === camera.id)) refuse('animated camera')
32
+ const away = camera.transform.location.map((value, axis) => value-camera.targetPoint[axis])
33
+ const distance = Math.hypot(...away)
34
+ if (!(distance > 0)) refuse('invalid target')
35
+ const location = camera.transform.location.map((value, axis) => value+away[axis]/distance*native.backoffM)
36
+ if (!vector(location)) refuse('non-finite fitted camera')
37
+ return { ...structuredClone(native), location,
38
+ source: 'native-evaluated-vertices-and-blender-camera-frame' }
39
+ }
@@ -66,17 +66,20 @@ export async function inspectRevisionGeometry(host, projectId, revision, selecti
66
66
  if (selection.surfaceSamples !== undefined && result?.report?.schemaVersion === 'deepblend.geometry-inspection/v1') {
67
67
  refuse(BlenderErrorCode.RUNTIME_UNAVAILABLE, 'the runtime does not support local surface samples; upgrade DeepBlend. Ordinary native geometry reads remain available.')
68
68
  }
69
+ if (selection.objectAxes === true && result?.report?.schemaVersion !== 'deepblend.geometry-inspection/v3') refuse(BlenderErrorCode.RUNTIME_UNAVAILABLE, 'The runtime does not support object-local native bounds; upgrade DeepBlend. Ordinary reads remain available.')
69
70
  const native = verifyGeometryReport(result?.report, request)
70
71
  verifySource()
71
72
  const hasSamples = selection.surfaceSamples !== undefined
72
- return { schemaVersion: hasSamples ? 'deepblend.scene-geometry/v2' : 'deepblend.scene-geometry/v1', projectId, sourceRevision: revision,
73
+ return { schemaVersion: selection.objectAxes === true ? 'deepblend.scene-geometry/v3' : hasSamples ? 'deepblend.scene-geometry/v2' : 'deepblend.scene-geometry/v1', projectId, sourceRevision: revision,
73
74
  sourceDigest: digest, sourceSpecHash: manifest.specHashAfter, sourceCheckpointSha256: expected,
75
+ ...(selection.objectAxes === true ? { objectAxisBasis: native.objectAxisBasis } : {}),
74
76
  sourceUnchanged: true, frame: native.frame, objects: native.objects, blenderVersion: native.blenderVersion,
75
77
  ...(hasSamples ? { surfaceSamples: native.surfaceSamples } : {}),
76
78
  excludedMeasurements: hasSamples ? ['globalWallThickness', 'assemblyClearance', 'minimumClearance', 'collision', 'selfIntersection'] :
77
79
  ['wallThickness', 'shellToLinerClearance', 'minimumClearance', 'collision'],
78
80
  execution: { transport: 'batch' }, limitations: [
79
- 'World-axis spans include rotations, handles and protrusions; they are not intrinsic body dimensions.',
81
+ 'World-axis spans include rotations, handles and protrusions; they are not intrinsic body dimensions. Never invert a world-axis bounding box to recover local bounds.',
82
+ ...(selection.objectAxes === true ? ['objectAxisBoundsMeters and objectAxisExtentMm bound actual evaluated mesh vertices in physical orthogonal axes of the selected object. They include evaluated modifiers, protrusions and native object/world scale, exclude placement rotation/translation, and are not semantic body dimensions or assembly extents. objectAxisWorldMatrix supplies the normalized axes and native origin. Sheared or degenerate object-axis bases have explicit unavailable objectAxisStatus while world bounds remain measured; these dimensions are not inferred from world bounds. Native scale can include generator construction transforms; it is not itself proof of a manual scaling edit. These extents already include that scale; do not apply it again.'] : []),
80
83
  'This evaluates saved native mesh geometry, not shader displacement or rendered pixels.',
81
84
  ...(hasSamples ? [
82
85
  'Surface samples are finite local chords along evaluated triangle-face normals, not shader or smoothed shading normals.',
package/lib/index.js CHANGED
@@ -712,7 +712,7 @@ export default class BlenderStudio extends Service {
712
712
  * whole spec on every turn spends its context on fields it is not changing.
713
713
  *
714
714
  * @param {string} projectId
715
- * @param {{ revision?: string, full?: boolean, geometry?: { entityIds: string[], frame: number, surfaceSamples?: object[] }, signal?: AbortSignal }} [options]
715
+ * @param {{ revision?: string, full?: boolean, geometry?: { entityIds: string[], frame: number, objectAxes?: boolean, surfaceSamples?: object[] }, signal?: AbortSignal }} [options]
716
716
  * @returns {Promise<Record<string, unknown>>}
717
717
  */
718
718
  async getScene(projectId, options = {}) {
@@ -833,6 +833,7 @@ export default class BlenderStudio extends Service {
833
833
  return { ...toCanonicalRevisionSummary({ manifest: outcome.revision, checkpointPath: outcome.revision.checkpoint }),
834
834
  unchanged: outcome.unchanged === true, idempotentReplay: outcome.idempotentReplay === true,
835
835
  parameterState: outcome.parameterState, preservedCameraIds: outcome.preservedCameraIds, placementChanges,
836
+ ...(outcome.revision.cameraFraming ? { cameraFraming: structuredClone(outcome.revision.cameraFraming) } : {}),
836
837
  parameters: await this.getRecipeParameters(input.projectId, { revision: outcome.revision.revision }),
837
838
  ...(outcome.job ? { job: toCanonicalJobRecord(outcome.job) } : {}) }
838
839
  }
@@ -42,6 +42,7 @@
42
42
  */
43
43
 
44
44
  import { assertRuntimeSceneFeatures } from './runtime-scene-features.js'
45
+ import { fitCupHeroFromNative } from './cup-hero-framing.js'
45
46
  import { existsSync, mkdirSync, readdirSync, renameSync, statSync } from 'node:fs'
46
47
  import { randomUUID } from 'node:crypto'
47
48
  import { join } from 'node:path'
@@ -325,7 +326,7 @@ export class RevisionTransaction {
325
326
  const checked = validateScenePatch(patch)
326
327
  if (!checked.ok) throw new BlenderError(BlenderErrorCode.SCENE_PATCH_INVALID, checked.summary)
327
328
  const state = { ...planned.parameterState, originRevision: context.originRevision }
328
- const outcome = await this._applyScenePatch(patch, { ...options, recipeParameters: state, verifyNativeSource })
329
+ const outcome = await this._applyScenePatch(patch, { ...options, recipeParameters: state, verifyNativeSource, cameraFraming: planned.cameraFraming })
329
330
  const committed = readRecipeContext(this.store, request.projectId, outcome.revision.revision)
330
331
  if (!committed?.state) throw new BlenderError('RECIPE_LOCK_INVALID', 'The recorded outcome has no recipe parameter state')
331
332
  return { ...outcome, parameterState: committed.state, preservedCameraIds: planned.preservedCameraIds }
@@ -481,6 +482,7 @@ export class RevisionTransaction {
481
482
  kind: 'scene_patch',
482
483
  recipeParameters: options.recipeParameters ?? null,
483
484
  verifyNativeSource: options.verifyNativeSource,
485
+ cameraFraming: options.cameraFraming,
484
486
  summary,
485
487
  actor: patch.actor ?? null,
486
488
  stage: patch.stage ?? null,
@@ -611,21 +613,51 @@ export class RevisionTransaction {
611
613
  try {
612
614
  const featureCheck = assertRuntimeSceneFeatures(this.runtime, plan.spec, { signal: plan.signal })
613
615
  if (featureCheck) await featureCheck
614
- const run = await this.runtime.compileScene({
615
- sceneSpecPath: specPath,
616
- profile: plan.spec.renderProfiles?.preview === undefined ? undefined : 'preview',
617
- projectRoot: this.store.projectDirectory(projectId),
618
- jobId,
619
- signal: plan.signal,
620
- onWorkingDirectory: info => {
621
- const produced = join(info.directory, 'result.blend')
622
- if (isFile(produced)) {
623
- // Same filesystem (both are inside the project's staging tree), so
624
- // this rename is atomic and cannot half-copy a large file.
625
- renameSync(produced, checkpointStaging)
626
- }
627
- },
628
- })
616
+ const compile = async () => {
617
+ writeFileAtomic(specPath, sceneSpecCanonicalText(plan.spec))
618
+ return this.runtime.compileScene({
619
+ sceneSpecPath: specPath,
620
+ cameraFraming: plan.cameraFraming,
621
+ // Automatic framing compiles in isolation, preserving any live editor scene.
622
+ session: plan.cameraFraming ? false : undefined,
623
+ profile: plan.spec.renderProfiles?.preview === undefined ? undefined : 'preview',
624
+ projectRoot: this.store.projectDirectory(projectId),
625
+ jobId,
626
+ signal: plan.signal,
627
+ onWorkingDirectory: info => {
628
+ const produced = join(info.directory, 'result.blend')
629
+ if (isFile(produced)) {
630
+ // Same filesystem (both are inside the project's staging tree), so
631
+ // this rename is atomic and cannot half-copy a large file.
632
+ renameSync(produced, checkpointStaging)
633
+ }
634
+ },
635
+ })
636
+ }
637
+ let run = await compile()
638
+ if (plan.cameraFraming) {
639
+ const initialFit = fitCupHeroFromNative(plan.spec, run.report, plan.cameraFraming)
640
+ if (initialFit.backoffM > 0) {
641
+ for (const entry of run.envelope.warnings ?? []) warnings.push(entry)
642
+ for (const entry of run.envelope.notices ?? []) warnings.push(warning(BlenderWarningCode.SCENE_COMPILER_DECISION, entry.message, { code: entry.code, framingStage: 'before-correction' }))
643
+ const correction = { op: 'camera.update', cameraId: initialFit.cameraId, transform: { location: initialFit.location } }
644
+ const corrected = applyPatchToSpec(plan.spec, { projectId, baseRevision: plan.baseRevision, operations: [correction] })
645
+ plan.spec = compileSceneSpec(corrected.spec).spec
646
+ const validation = validateSceneSpec(plan.spec)
647
+ if (!validation.ok) throw new BlenderError(BlenderErrorCode.SCENE_SPEC_INVALID, validation.summary)
648
+ plan.operations = [...plan.operations, correction]
649
+ plan.operationRecords = [...plan.operationRecords, ...corrected.operations]
650
+ plan.digestAfter = sceneSpecDigest(plan.spec)
651
+ plan.specHashAfter = specHash(plan.spec)
652
+ run = await compile()
653
+ }
654
+ const finalFit = fitCupHeroFromNative(plan.spec, run.report, plan.cameraFraming)
655
+ if (finalFit.backoffM !== 0) throw new BlenderError(BlenderErrorCode.SCENE_VALIDATION_FAILED, 'The recompiled hero still needs a framing correction; no revision was published')
656
+ plan.cameraFramingReceipt = { ...finalFit, appliedBackoffM: initialFit.backoffM, beforeCameraLocation: initialFit.cameraLocation }
657
+ plan.recipeParameters.cupCameraRig = { schemaVersion: 'deepblend.cup-camera-rig/v1',
658
+ anchor: structuredClone(plan.spec.entities.find(entity => entity.id === 'cup').transform),
659
+ camera: structuredClone(plan.spec.cameras.find(camera => camera.id === 'hero')) }
660
+ }
629
661
  compileReport = run.report
630
662
  technical = run.report?.validation ?? null
631
663
  for (const entry of run.envelope.warnings ?? []) warnings.push(entry)
@@ -809,6 +841,7 @@ export class RevisionTransaction {
809
841
  animatedObjects: technical.animatedObjects ?? null,
810
842
  cameraParameters: technical.cameraParameters ?? null,
811
843
  },
844
+ ...(plan.cameraFramingReceipt ? { cameraFraming: plan.cameraFramingReceipt } : {}),
812
845
  renderConfig: compileReport?.renderConfig ?? null,
813
846
  sceneFingerprint: compileReport?.sceneFingerprint ?? null,
814
847
  assetParts: (compileReport?.objects ?? [])
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deepblend/dsh-blender-host",
3
- "version": "0.12.0",
3
+ "version": "0.14.0",
4
4
  "type": "module",
5
5
  "description": "DeepBlend Host service: the blenderStudio business facade over the BlenderRuntime seam.",
6
6
  "main": "lib/index.js",
@@ -20,7 +20,7 @@
20
20
  "./package.json": "./package.json"
21
21
  },
22
22
  "dependencies": {
23
- "@deepblend/dsh-blender-contracts": "0.12.0",
23
+ "@deepblend/dsh-blender-contracts": "0.14.0",
24
24
  "sharp": "0.35.5"
25
25
  },
26
26
  "license": "MIT",