@deepblend/dsh-blender-tool 0.18.0 → 0.19.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.
Files changed (2) hide show
  1. package/lib/tools.js +64 -2
  2. package/package.json +2 -2
package/lib/tools.js CHANGED
@@ -124,12 +124,73 @@ export function apply(ctx) {
124
124
  ctx.tools.register(projectGet(ctx))
125
125
  ctx.tools.register(sceneGet(ctx))
126
126
  ctx.tools.register(scenePatch(ctx))
127
+ ctx.tools.register(placeOnSurface(ctx))
127
128
  ctx.tools.register(previewRender(ctx))
128
129
  ctx.tools.register(sceneValidate(ctx))
129
130
  ctx.tools.register(revisionRestore(ctx))
130
131
  ctx.tools.register(assetIngest(ctx))
131
132
  }
132
133
 
134
+ /** A measured placement is committed by the Host, not guessed as a transform patch. */
135
+ function placeOnSurface(ctx) {
136
+ return defineTool({
137
+ name: 'blender_place_on_surface',
138
+ description: 'Place a saved entity above a chosen support using their actual evaluated meshes, then save an editable Blender revision and preview. ' +
139
+ 'Moves only the subject’s world Z; keeps its XY, rotation and scale. Supports generated meshes and whole imported asset instances, including sloped supports. ' +
140
+ 'Read blender_scene_get first and pass its current baseRevision, both entity IDs and an explicit frame. ' +
141
+ 'clearanceMm is a vertical gap, not distance along a surface normal. Requires static geometry without related cross-entity geometry dependencies. ' +
142
+ 'The receipt describes this selected pair at that frame; it does not certify physical stability, collisions with other objects or the movement path. ' +
143
+ 'For file delivery, resolve returned checkpoint and preview paths against the returned absolute projectDirectory. ' +
144
+ 'Always saves a checkpoint; renderPreview defaults to true. Use one idempotencyKey for an intent and reuse it only for an identical retry.',
145
+ parameters: {
146
+ projectId: { type: 'string', required: true, description: 'The existing project.' },
147
+ baseRevision: { type: 'string', required: true, description: 'Current revision returned by blender_scene_get. Stale revisions are refused.' },
148
+ subjectEntityId: { type: 'string', required: true, description: 'Entity to move vertically; use the whole entity ID for an imported asset instance.' },
149
+ supportEntityId: { type: 'string', required: true, description: 'Different entity whose evaluated surface should support the subject.' },
150
+ frame: { type: 'integer', required: true, description: 'Explicit frame to measure and verify, from 0 to 1000000. The Host enforces this range.' },
151
+ clearanceMm: { type: 'number', description: 'Required vertical gap in millimetres, 0–1000; defaults to 0. The Host enforces this range.' },
152
+ renderPreview: { type: 'boolean', description: 'Render and attach the saved revision’s preview (default true). False still saves the Blender checkpoint.' },
153
+ cameraId: { type: 'string', description: 'Optional saved camera for the preview; omitted uses the project’s default camera.' },
154
+ idempotencyKey: { type: 'string', description: 'Unique placement intent; reuse only when retrying this exact request.' },
155
+ },
156
+ output: TOOL_OUTPUT_WITH_IMAGE,
157
+ async execute(args, exec) {
158
+ const resolved = resolveStudio(ctx)
159
+ if (resolved.unavailable !== undefined) return { ok: false, ...resolved.unavailable }
160
+ try {
161
+ if (resolved.studio.surfacePlacementSupported !== true || typeof resolved.studio.placeOnSurface !== 'function') {
162
+ throw new BlenderError(BlenderErrorCode.RUNTIME_UNAVAILABLE,
163
+ 'This Host cannot perform measured surface placement. Update the DeepBlend Host bundle and restart the profile before retrying.')
164
+ }
165
+ const { data, canonicalWarnings } = await canonicalCall(resolved.studio.placeOnSurface({
166
+ // Preserve unknown fields so the Host can reject them rather than silently changing the intent.
167
+ ...args,
168
+ projectId: args.projectId, baseRevision: args.baseRevision,
169
+ subjectEntityId: args.subjectEntityId, supportEntityId: args.supportEntityId, frame: args.frame,
170
+ clearanceMm: args.clearanceMm ?? 0, renderPreview: args.renderPreview ?? true,
171
+ cameraId: args.cameraId, idempotencyKey: args.idempotencyKey, signal: exec?.signal,
172
+ }), warning)
173
+ const value = { ok: true,
174
+ text: renderSuccess(`${data.idempotentReplay === true ? 'Reused' : 'Saved'} surface placement in revision ${data.revision}.`, data,
175
+ { warnings: [...(data.warnings ?? []), ...canonicalWarnings] }), data }
176
+ return args.renderPreview !== false
177
+ ? await attachCommittedPreview(ctx, exec, resolved, value, { projectId: args.projectId, summary: data }) : value
178
+ } catch (cause) {
179
+ const failure = renderFailure(cause, 'SURFACE_PLACEMENT_FAILED')
180
+ if (failure.data.errorCode === BlenderErrorCode.REVISION_CONFLICT) {
181
+ failure.text = ['DeepBlend placement refused.', `errorCode: ${failure.data.errorCode}`,
182
+ `message: ${failure.data.message}`,
183
+ ...(failure.data.detail == null ? [] : [`detail: ${JSON.stringify(failure.data.detail)}`]),
184
+ '', 'Read blender_scene_get again, then retry blender_place_on_surface with the new baseRevision.'].join('\n')
185
+ }
186
+ return { ok: false, ...failure }
187
+ }
188
+ },
189
+ presentCall: args => ({ card: 'generic', title: `Place "${args?.subjectEntityId ?? ''}" on "${args?.supportEntityId ?? ''}"`,
190
+ kind: 'move', projectId: args?.projectId }),
191
+ })
192
+ }
193
+
133
194
  /** Read verified local package metadata through the Host; never load a package in the tool plane. */
134
195
  function recipeParameters(ctx) {
135
196
  return defineTool({
@@ -648,8 +709,9 @@ function scenePatch(ctx) {
648
709
  return defineTool({
649
710
  name: 'blender_scene_patch',
650
711
  description:
651
- 'Modify a scene and commit the result as one new immutable revision. This is the ONLY way to change a ' +
652
- 'scene — there is no free-form edit. Read the scene first with blender_scene_get and pass the revision ' +
712
+ 'Modify a scene with explicit operations and commit the result as one new immutable revision. ' +
713
+ 'For measured vertical placement use blender_place_on_surface; for saved assembly dimensions use blender_recipe_parameters. ' +
714
+ 'Read the scene first with blender_scene_get and pass the revision ' +
653
715
  'it returned as `baseRevision`; if the project has moved on since, the patch is REFUSED rather than ' +
654
716
  'merged, and you should re-read and re-apply. Operations apply in order and all-or-nothing: if any one ' +
655
717
  'fails, no revision is created and the current revision is untouched, so a failed patch is always safe ' +
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deepblend/dsh-blender-tool",
3
- "version": "0.18.0",
3
+ "version": "0.19.0",
4
4
  "type": "module",
5
5
  "description": "Agent-preset tool consumer: registers the model-visible DeepBlend Blender tools. Publishes no service.",
6
6
  "main": "lib/index.js",
@@ -11,7 +11,7 @@
11
11
  "./package.json": "./package.json"
12
12
  },
13
13
  "dependencies": {
14
- "@deepblend/dsh-blender-contracts": "0.18.0"
14
+ "@deepblend/dsh-blender-contracts": "0.19.0"
15
15
  },
16
16
  "license": "MIT",
17
17
  "repository": {