@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.
@@ -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
- return { schemaVersion: 'deepblend.scene-geometry/v1', projectId, sourceRevision: revision,
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
- excludedMeasurements: ['wallThickness', 'shellToLinerClearance', 'minimumClearance', 'collision'],
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
- 'No wall thickness, clearance, collision, topology or artistic approval is established.',
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, this.store.readRevisionSpec(projectId, revision), context.state ?? undefined, {}) }
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.9.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.9.0",
23
+ "@deepblend/dsh-blender-contracts": "0.11.0",
24
24
  "sharp": "0.35.5"
25
25
  },
26
26
  "license": "MIT",