@deepblend/dsh-blender-host 0.12.0 → 0.13.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.
- package/lib/cup-hero-framing.js +39 -0
- package/lib/index.js +1 -0
- package/lib/revision-transaction.js +49 -16
- package/package.json +2 -2
|
@@ -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
|
+
}
|
package/lib/index.js
CHANGED
|
@@ -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
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
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.
|
|
3
|
+
"version": "0.13.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.
|
|
23
|
+
"@deepblend/dsh-blender-contracts": "0.13.0",
|
|
24
24
|
"sharp": "0.35.5"
|
|
25
25
|
},
|
|
26
26
|
"license": "MIT",
|