@deepblend/dsh-blender-tool 0.20.0 → 0.22.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 +82 -3
  2. package/package.json +2 -2
package/lib/tools.js CHANGED
@@ -125,18 +125,82 @@ export function apply(ctx) {
125
125
  ctx.tools.register(sceneGet(ctx))
126
126
  ctx.tools.register(scenePatch(ctx))
127
127
  ctx.tools.register(placeOnSurface(ctx))
128
+ ctx.tools.register(cameraFit(ctx))
128
129
  ctx.tools.register(previewRender(ctx))
129
130
  ctx.tools.register(sceneValidate(ctx))
130
131
  ctx.tools.register(revisionRestore(ctx))
131
132
  ctx.tools.register(assetIngest(ctx))
132
133
  }
133
134
 
135
+ /** Fit complete evaluated meshes through the Host, keeping original photography. */
136
+ function cameraFit(ctx) {
137
+ return defineTool({
138
+ name: 'blender_camera_fit',
139
+ description: 'Save a new static camera that frames all explicitly selected product meshes at one frame, with an editable Blender checkpoint and preview. ' +
140
+ 'Read blender_scene_get first for the current baseRevision, entity IDs and source camera; for a supported complete recipe use every scene.assemblyPlacement.memberEntityIds entry, including the cable. ' +
141
+ 'Pass an unused fittedCameraId. The source camera may be animated: its evaluated viewing direction and roll at frame are preserved in the static copy, together with its measured optics. ' +
142
+ 'Original cameras, camera order, animation tracks, shots, entities, materials and lights are retained. Depth of field focuses the new view at the selected product’s bounds center depth. ' +
143
+ 'The Host measures actual whole evaluated mesh sets and independently checks the compiled candidate’s frame margin and clipping before publication. ' +
144
+ 'Select 1–64 distinct generated entities or whole asset instances, within 128 mesh objects, two million evaluated vertices and two million polygons in total. Supports perspective cameras and matching viewport/render geometry; unsupported or hidden geometry is refused. ' +
145
+ 'margin is a fraction of each image edge (default 0.03). Evidence applies to this frame and the saved checkpoint’s preview render aspect; a different delivery aspect needs its own check. ' +
146
+ 'This proves framing and clipping of the selected vertices, not visibility through other objects, artistic quality, shader displacement, transparency or an animation range. ' +
147
+ 'Always saves one checkpoint; renderPreview defaults to true. Resolve returned relative artifact paths against projectDirectory. ' +
148
+ 'Reuse one idempotencyKey only for an identical request; a successful retry returns its earlier fitted view without creating another camera.',
149
+ parameters: {
150
+ projectId: { type: 'string', required: true, description: 'The existing project.' },
151
+ baseRevision: { type: 'string', required: true, description: 'Current revision from blender_scene_get. A new stale request is refused.' },
152
+ entityIds: { type: 'array', items: { type: 'string' }, required: true, description: '1–64 distinct complete mesh entity or asset-instance IDs. Include every part of the product; do not include an unrelated floor or support.' },
153
+ cameraId: { type: 'string', required: true, description: 'Existing perspective camera whose evaluated viewing direction, roll and optics should be retained.' },
154
+ fittedCameraId: { type: 'string', required: true, description: 'Unused ID for the new static fitted camera. Existing cameras are preserved.' },
155
+ frame: { type: 'integer', required: true, description: 'Explicit integer frame from 0 to 1000000 to evaluate, fit, verify and preview.' },
156
+ margin: { type: 'number', description: 'Fractional margin at each image edge, 0–0.25; default 0.03. The Host validates this range.' },
157
+ renderPreview: { type: 'boolean', description: 'Attach the committed fitted view’s preview, default true. False still saves the checkpoint.' },
158
+ idempotencyKey: { type: 'string', description: 'Unique logical fit request. Reuse only for an identical retry, including its original baseRevision.' },
159
+ },
160
+ output: TOOL_OUTPUT_WITH_IMAGE,
161
+ async execute(args, exec) {
162
+ const resolved = resolveStudio(ctx)
163
+ if (resolved.unavailable !== undefined) return { ok: false, ...resolved.unavailable }
164
+ try {
165
+ if (resolved.studio.cameraFitSupported !== true || typeof resolved.studio.fitCamera !== 'function') {
166
+ throw new BlenderError(BlenderErrorCode.RUNTIME_UNAVAILABLE,
167
+ 'This Host cannot save verified fitted views. Update the DeepBlend Host bundle and restart the profile before retrying.')
168
+ }
169
+ const { projectId, baseRevision, entityIds, cameraId, fittedCameraId, frame, margin, renderPreview, idempotencyKey, ...extra } = args
170
+ const { data, canonicalWarnings } = await canonicalCall(resolved.studio.fitCamera({
171
+ ...extra, projectId, baseRevision, entityIds, cameraId, fittedCameraId, frame,
172
+ ...(margin === undefined ? {} : { margin }),
173
+ ...(renderPreview === undefined ? {} : { renderPreview }),
174
+ ...(idempotencyKey === undefined ? {} : { idempotencyKey }),
175
+ signal: exec?.signal,
176
+ }), warning)
177
+ const value = { ok: true,
178
+ text: renderSuccess(`${data.idempotentReplay === true ? 'Reused' : 'Saved'} fitted view "${data.cameraFitReceipt?.fittedCameraId ?? args.fittedCameraId}" in revision ${data.revision}.`, data,
179
+ { warnings: [...(data.warnings ?? []), ...canonicalWarnings] }), data }
180
+ return args.renderPreview !== false
181
+ ? await attachCommittedPreview(ctx, exec, resolved, value, { projectId: args.projectId, summary: data }) : value
182
+ } catch (cause) {
183
+ const failure = renderFailure(cause, 'CAMERA_FIT_FAILED')
184
+ if (failure.data.errorCode === BlenderErrorCode.REVISION_CONFLICT) {
185
+ failure.text += '\n\nRead blender_scene_get again, then retry blender_camera_fit with the new baseRevision and an unused fittedCameraId.'
186
+ }
187
+ return { ok: false, ...failure }
188
+ }
189
+ },
190
+ presentCall: args => ({ card: 'generic', title: `Save complete view "${args?.fittedCameraId ?? ''}"`,
191
+ kind: 'edit', projectId: args?.projectId }),
192
+ })
193
+ }
194
+
134
195
  /** A measured placement is committed by the Host, not guessed as a transform patch. */
135
196
  function placeOnSurface(ctx) {
136
197
  return defineTool({
137
198
  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. ' +
199
+ description: 'Place a saved entity or a complete saved lamp above a chosen support using actual evaluated meshes, then save one editable Blender revision and preview. ' +
200
+ 'Default entity scope moves only the selected subject’s world Z and keeps its XY, rotation and scale; supports generated meshes and whole imported asset instances. ' +
201
+ 'For the complete pinned metal-lamp2.2 assembly, use scope="recipe-assembly" and the subjectEntityId returned in scene.assemblyPlacement. ' +
202
+ 'This moves every lamp member, including the cable: optional pose applies relative world translation in mm and upright yaw in degrees, then the complete native mesh union settles vertically onto the selected support. ' +
203
+ 'pose.pivotMm defaults to the saved base origin; translation is applied after rotation. All camera/light settings are retained; after a large move use blender_camera_fit to save a complete fitted view or explicitly edit photography. ' +
140
204
  'Read blender_scene_get first and pass its current baseRevision, both entity IDs and an explicit frame. ' +
141
205
  'clearanceMm is a vertical gap, not distance along a surface normal. Requires static geometry without related cross-entity geometry dependencies. ' +
142
206
  'The receipt describes this selected pair at that frame; it does not certify physical stability, collisions with other objects or the movement path. ' +
@@ -146,6 +210,13 @@ function placeOnSurface(ctx) {
146
210
  projectId: { type: 'string', required: true, description: 'The existing project.' },
147
211
  baseRevision: { type: 'string', required: true, description: 'Current revision returned by blender_scene_get. Stale revisions are refused.' },
148
212
  subjectEntityId: { type: 'string', required: true, description: 'Entity to move vertically; use the whole entity ID for an imported asset instance.' },
213
+ scope: { type: 'string', enum: ['entity', 'recipe-assembly'], description: 'Default entity preserves existing single-entity semantics. recipe-assembly explicitly selects every member of the supported saved complete metal lamp2.2.' },
214
+ pose: { type: 'object', additionalProperties: false, description: 'For recipe-assembly only. Apply a relative world pose first, then settle the complete lamp in world Z; no scale or tilt.',
215
+ properties: {
216
+ translationMm: { type: 'array', items: { type: 'number' }, description: 'Relative world [dx,dy,dz] in mm, exactly3 finite numbers within ±1000000. Default[0,0,0]. The provisional Z is adjusted by surface settling.' },
217
+ yawDegrees: { type: 'number', description: 'Relative rotation about world Z in degrees, ±360; default0. Positive is counter-clockwise viewed from above.' },
218
+ pivotMm: { type: 'array', items: { type: 'number' }, description: 'World [x,y,z] rotation pivot in mm, exactly3 finite numbers within ±1000000. Default is the current saved base origin. Rotate first, then translate.' },
219
+ } },
149
220
  supportEntityId: { type: 'string', required: true, description: 'Different entity whose evaluated surface should support the subject.' },
150
221
  frame: { type: 'integer', required: true, description: 'Explicit frame to measure and verify, from 0 to 1000000. The Host enforces this range.' },
151
222
  clearanceMm: { type: 'number', description: 'Required vertical gap in millimetres, 0–1000; defaults to 0. The Host enforces this range.' },
@@ -162,10 +233,16 @@ function placeOnSurface(ctx) {
162
233
  throw new BlenderError(BlenderErrorCode.RUNTIME_UNAVAILABLE,
163
234
  'This Host cannot perform measured surface placement. Update the DeepBlend Host bundle and restart the profile before retrying.')
164
235
  }
236
+ if (args.scope === 'recipe-assembly' && resolved.studio.assemblyPlacementSupported !== true) {
237
+ throw new BlenderError(BlenderErrorCode.RUNTIME_UNAVAILABLE,
238
+ 'This Host cannot place the complete lamp assembly. Update the DeepBlend Host bundle and restart the profile before retrying.')
239
+ }
165
240
  const { data, canonicalWarnings } = await canonicalCall(resolved.studio.placeOnSurface({
166
241
  // Preserve unknown fields so the Host can reject them rather than silently changing the intent.
167
242
  ...args,
168
243
  projectId: args.projectId, baseRevision: args.baseRevision,
244
+ ...(args.scope === undefined ? {} : { scope: args.scope }),
245
+ ...(args.pose === undefined ? {} : { pose: args.pose }),
169
246
  subjectEntityId: args.subjectEntityId, supportEntityId: args.supportEntityId, frame: args.frame,
170
247
  clearanceMm: args.clearanceMm ?? 0, renderPreview: args.renderPreview ?? true,
171
248
  cameraId: args.cameraId, idempotencyKey: args.idempotencyKey, signal: exec?.signal,
@@ -186,7 +263,9 @@ function placeOnSurface(ctx) {
186
263
  return { ok: false, ...failure }
187
264
  }
188
265
  },
189
- presentCall: args => ({ card: 'generic', title: `Place "${args?.subjectEntityId ?? ''}" on "${args?.supportEntityId ?? ''}"`,
266
+ presentCall: args => ({ card: 'generic', title: args?.scope === 'recipe-assembly'
267
+ ? `Place the complete lamp on "${args?.supportEntityId ?? ''}"`
268
+ : `Place "${args?.subjectEntityId ?? ''}" on "${args?.supportEntityId ?? ''}"`,
190
269
  kind: 'move', projectId: args?.projectId }),
191
270
  })
192
271
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deepblend/dsh-blender-tool",
3
- "version": "0.20.0",
3
+ "version": "0.22.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.20.0"
14
+ "@deepblend/dsh-blender-contracts": "0.22.0"
15
15
  },
16
16
  "license": "MIT",
17
17
  "repository": {