@deepblend/dsh-blender-host 0.9.0 → 0.11.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/assembly-changes.js +21 -0
- package/lib/geometry-inspection.js +16 -3
- package/lib/index.js +17 -5
- package/package.json +2 -2
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** Describe saved object-origin moves and changed generators, not surface clearances. */
|
|
2
|
+
import { canonicalStringify } from '@deepblend/dsh-blender-contracts'
|
|
3
|
+
export function assemblyPlacementChanges(before, after, fromRevision, toRevision) {
|
|
4
|
+
const previous = new Map(before.entities.map(entity => [entity.id, entity]))
|
|
5
|
+
const groups = new Map()
|
|
6
|
+
const generatorChangedEntityIds = []
|
|
7
|
+
for (const entity of after.entities) {
|
|
8
|
+
const original = previous.get(entity.id)
|
|
9
|
+
if (!original) continue
|
|
10
|
+
if (canonicalStringify(entity.generator) !== canonicalStringify(original.generator)) generatorChangedEntityIds.push(entity.id)
|
|
11
|
+
const from = original.transform?.location ?? [0, 0, 0]
|
|
12
|
+
const to = entity.transform?.location ?? [0, 0, 0]
|
|
13
|
+
const deltaMm = to.map((value, axis) => Number(((value - from[axis]) * 1000).toFixed(9)))
|
|
14
|
+
if (deltaMm.every(value => value === 0)) continue
|
|
15
|
+
const key = JSON.stringify(deltaMm)
|
|
16
|
+
if (!groups.has(key)) groups.set(key, { deltaMm, entityIds: [] })
|
|
17
|
+
groups.get(key).entityIds.push(entity.id)
|
|
18
|
+
}
|
|
19
|
+
return { source: 'saved-scene-spec', coordinateField: 'entity.transform.location',
|
|
20
|
+
fromRevision, toRevision, groups: [...groups.values()], generatorChangedEntityIds }
|
|
21
|
+
}
|
|
@@ -63,16 +63,29 @@ export async function inspectRevisionGeometry(host, projectId, revision, selecti
|
|
|
63
63
|
if (fileSha256(snapshot) !== expected) refuse(BlenderErrorCode.SCENE_VALIDATION_FAILED, 'the staged native input differs from the saved source.')
|
|
64
64
|
const result = await host.runtime.inspectGeometry(request)
|
|
65
65
|
cancelled(signal)
|
|
66
|
+
if (selection.surfaceSamples !== undefined && result?.report?.schemaVersion === 'deepblend.geometry-inspection/v1') {
|
|
67
|
+
refuse(BlenderErrorCode.RUNTIME_UNAVAILABLE, 'the runtime does not support local surface samples; upgrade DeepBlend. Ordinary native geometry reads remain available.')
|
|
68
|
+
}
|
|
66
69
|
const native = verifyGeometryReport(result?.report, request)
|
|
67
70
|
verifySource()
|
|
68
|
-
|
|
71
|
+
const hasSamples = selection.surfaceSamples !== undefined
|
|
72
|
+
return { schemaVersion: hasSamples ? 'deepblend.scene-geometry/v2' : 'deepblend.scene-geometry/v1', projectId, sourceRevision: revision,
|
|
69
73
|
sourceDigest: digest, sourceSpecHash: manifest.specHashAfter, sourceCheckpointSha256: expected,
|
|
70
74
|
sourceUnchanged: true, frame: native.frame, objects: native.objects, blenderVersion: native.blenderVersion,
|
|
71
|
-
|
|
75
|
+
...(hasSamples ? { surfaceSamples: native.surfaceSamples } : {}),
|
|
76
|
+
excludedMeasurements: hasSamples ? ['globalWallThickness', 'assemblyClearance', 'minimumClearance', 'collision', 'selfIntersection'] :
|
|
77
|
+
['wallThickness', 'shellToLinerClearance', 'minimumClearance', 'collision'],
|
|
72
78
|
execution: { transport: 'batch' }, limitations: [
|
|
73
79
|
'World-axis spans include rotations, handles and protrusions; they are not intrinsic body dimensions.',
|
|
74
80
|
'This evaluates saved native mesh geometry, not shader displacement or rendered pixels.',
|
|
75
|
-
|
|
81
|
+
...(hasSamples ? [
|
|
82
|
+
'Surface samples are finite local chords along evaluated triangle-face normals, not shader or smoothed shading normals.',
|
|
83
|
+
'The normalSurfaceChordMm runs from source entry to source exit. Target normalRaySeparationMm runs from source EXIT to target entry along that inward normal and excludes the source chord. Other scene objects are not checked; this is not full assembly free clearance.',
|
|
84
|
+
'Closed connected consistently wound positive-volume geometry is required, but absence of self-intersections is not certified.',
|
|
85
|
+
'No global minimum wall thickness, minimum clearance, collision absence, manufacturing safety or artistic approval is established.',
|
|
86
|
+
'Unmeasured, missed, overlapping, occluded or unsupported samples must remain unavailable, never be replaced by zero or inferred from bounds.',
|
|
87
|
+
'Search distance is explicitly requested (0.01–500 mm); chord and target search limits are each 100 mm. Epsilon is 0.0001 mm; distances at or below twice epsilon are unavailable.',
|
|
88
|
+
] : ['No wall thickness, clearance, collision, topology or artistic approval is established.']),
|
|
76
89
|
'Do not subtract shell and liner extents or radii to report a gap: both bounds describe outer surfaces, and their difference includes wall material and depends on curvature and orientation.',
|
|
77
90
|
'Unchanged height, position or scale does not prove that wall thickness or clearance was preserved. Report those conditions as unmeasured unless a separate surface measurement verifies them.',
|
|
78
91
|
] }
|
package/lib/index.js
CHANGED
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
|
|
28
28
|
import { assertRuntimeSceneFeatures } from './runtime-scene-features.js'
|
|
29
29
|
import { readRecipeContext } from './recipe-state.js'
|
|
30
|
+
import { assemblyPlacementChanges } from './assembly-changes.js'
|
|
30
31
|
import { instantiateRecipeLock, planRecipeGeometryEdit, recipeGeometryParameters } from '@deepblend/dsh-blender-contracts'
|
|
31
32
|
import { Service } from '@deepseek-ai/cordis'
|
|
32
33
|
import z from '@deepseek-ai/schemastery'
|
|
@@ -671,7 +672,7 @@ export default class BlenderStudio extends Service {
|
|
|
671
672
|
const digest = sceneSpecDigest(spec)
|
|
672
673
|
return {
|
|
673
674
|
...toCanonicalProjectSummary({
|
|
674
|
-
record: { ...record, goal: spec.project.goal ?? null },
|
|
675
|
+
record: { ...record, projectsRoot: this.projectsRoot, goal: spec.project.goal ?? null },
|
|
675
676
|
revisionCount: record.revisionCount ?? this.store.listRevisions(projectId).length,
|
|
676
677
|
sceneSummary: summarizeSceneSpec(spec, {
|
|
677
678
|
revision,
|
|
@@ -679,6 +680,7 @@ export default class BlenderStudio extends Service {
|
|
|
679
680
|
digest,
|
|
680
681
|
}),
|
|
681
682
|
}),
|
|
683
|
+
projectDirectory: this.store.projectDirectory(projectId),
|
|
682
684
|
revisions: this.store.listRevisions(projectId).map(id => {
|
|
683
685
|
const manifest = this.store.readRevisionManifest(projectId, id)
|
|
684
686
|
return {
|
|
@@ -710,7 +712,7 @@ export default class BlenderStudio extends Service {
|
|
|
710
712
|
* whole spec on every turn spends its context on fields it is not changing.
|
|
711
713
|
*
|
|
712
714
|
* @param {string} projectId
|
|
713
|
-
* @param {{ revision?: string, full?: boolean, geometry?: { entityIds: string[], frame: number }, signal?: AbortSignal }} [options]
|
|
715
|
+
* @param {{ revision?: string, full?: boolean, geometry?: { entityIds: string[], frame: number, surfaceSamples?: object[] }, signal?: AbortSignal }} [options]
|
|
714
716
|
* @returns {Promise<Record<string, unknown>>}
|
|
715
717
|
*/
|
|
716
718
|
async getScene(projectId, options = {}) {
|
|
@@ -792,8 +794,14 @@ export default class BlenderStudio extends Service {
|
|
|
792
794
|
const locked = context.state ? { ...context.lock, values: context.state.values,
|
|
793
795
|
valuesSha256: context.state.valuesSha256 } : context.lock
|
|
794
796
|
const resolved = instantiateRecipeLock(locked)
|
|
797
|
+
const currentSpec = this.store.readRevisionSpec(projectId, revision)
|
|
798
|
+
const animatedBindings = definitions.flatMap(parameter => parameter.bindings).filter(binding => binding.offsetAnimation === true)
|
|
799
|
+
const linkedAnimationTracks = (currentSpec.animationTracks ?? []).filter(track => (track.targetKind ?? 'entity') === 'entity'
|
|
800
|
+
&& animatedBindings.some(binding => binding.entityId === track.targetEntityId && track.property === `location.${binding.axis}`))
|
|
801
|
+
.map(track => ({ id: track.id, targetEntityId: track.targetEntityId, property: track.property,
|
|
802
|
+
keyframes: structuredClone(track.keyframes) }))
|
|
795
803
|
let conflicts = []
|
|
796
|
-
try { planRecipeGeometryEdit(context.lock,
|
|
804
|
+
try { planRecipeGeometryEdit(context.lock, currentSpec, context.state ?? undefined, {}) }
|
|
797
805
|
catch (error) {
|
|
798
806
|
if (error.code !== 'RECIPE_PARAMETER_CONFLICT') throw error
|
|
799
807
|
conflicts = error.details.conflicts
|
|
@@ -802,7 +810,7 @@ export default class BlenderStudio extends Service {
|
|
|
802
810
|
recipe: { id: context.lock.id, version: context.lock.version }, valuesSha256: resolved.recipe.valuesSha256,
|
|
803
811
|
parameters: definitions.map(parameter => ({ id: parameter.id, title: parameter.title, type: parameter.type,
|
|
804
812
|
minimum: parameter.minimum, maximum: parameter.maximum, default: parameter.default, value: resolved.values[parameter.id] })),
|
|
805
|
-
conflicts }
|
|
813
|
+
conflicts, linkedAnimationTracks, animationSource: 'saved-scene-spec' }
|
|
806
814
|
}
|
|
807
815
|
|
|
808
816
|
async updateRecipeParameters(request) {
|
|
@@ -818,9 +826,13 @@ export default class BlenderStudio extends Service {
|
|
|
818
826
|
throw new BlenderError('REVISION_CONFLICT', 'The active project store changed; read the selected work again')
|
|
819
827
|
}
|
|
820
828
|
const outcome = await this.transactions.applyRecipeParameters(input, { signal })
|
|
829
|
+
const placementChanges = assemblyPlacementChanges(
|
|
830
|
+
this.store.readRevisionSpec(input.projectId, input.baseRevision),
|
|
831
|
+
this.store.readRevisionSpec(input.projectId, outcome.revision.revision),
|
|
832
|
+
input.baseRevision, outcome.revision.revision)
|
|
821
833
|
return { ...toCanonicalRevisionSummary({ manifest: outcome.revision, checkpointPath: outcome.revision.checkpoint }),
|
|
822
834
|
unchanged: outcome.unchanged === true, idempotentReplay: outcome.idempotentReplay === true,
|
|
823
|
-
parameterState: outcome.parameterState, preservedCameraIds: outcome.preservedCameraIds,
|
|
835
|
+
parameterState: outcome.parameterState, preservedCameraIds: outcome.preservedCameraIds, placementChanges,
|
|
824
836
|
parameters: await this.getRecipeParameters(input.projectId, { revision: outcome.revision.revision }),
|
|
825
837
|
...(outcome.job ? { job: toCanonicalJobRecord(outcome.job) } : {}) }
|
|
826
838
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deepblend/dsh-blender-host",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.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.11.0",
|
|
24
24
|
"sharp": "0.35.5"
|
|
25
25
|
},
|
|
26
26
|
"license": "MIT",
|