@deepblend/dsh-blender-tool 0.21.0 → 0.23.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 +63 -1
  2. package/package.json +2 -2
package/lib/tools.js CHANGED
@@ -125,12 +125,73 @@ 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({
@@ -139,7 +200,7 @@ function placeOnSurface(ctx) {
139
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. ' +
140
201
  'For the complete pinned metal-lamp2.2 assembly, use scope="recipe-assembly" and the subjectEntityId returned in scene.assemblyPlacement. ' +
141
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. ' +
142
- 'pose.pivotMm defaults to the saved base origin; translation is applied after rotation. All camera/light settings are retained, so inspect framing after a large move and explicitly edit photography if needed. ' +
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. ' +
143
204
  'Read blender_scene_get first and pass its current baseRevision, both entity IDs and an explicit frame. ' +
144
205
  'clearanceMm is a vertical gap, not distance along a surface normal. Requires static geometry without related cross-entity geometry dependencies. ' +
145
206
  'The receipt describes this selected pair at that frame; it does not certify physical stability, collisions with other objects or the movement path. ' +
@@ -721,6 +782,7 @@ const OPERATION_SUMMARY = [
721
782
  'material.tangent.set {materialId, tangent} — {mode:uv,uvMap:UVMap} uses named mesh UVs; {mode:radial,axis:x|y|z} uses object-local cylindrical tangent; null clears after disabling anisotropy and rotation/animation',
722
783
  'project.brief.set {goal, referenceImages} — replace written goal and up to four reference bindings; each {id,assetId,sha256,label,purposes,notes?}, purposes geometry/materials/lighting/goalFit; bind immutable ingested PNG/JPEG assets; empty goal and [] clear',
723
784
  'project.reviewSubject.set {entityId} — save the main review entity; null restores automatic selection; entity must exist and be non-empty; does not move any camera',
785
+ 'entity.uv.project {entityId, quarterTurns?, scale?} — replace only mesh faceUvs for repeating textures. U follows each planar face\'s longest local boundary edge; V is perpendicular within that face. quarterTurns 0–3 rotates projected UV coordinates counterclockwise in 90-degree steps; default 0 keeps the long edge along U. One mesh-wide local extent divisor preserves face proportions and relative density; scale 0.0001–10000 is repetitions across the longest control-mesh extent, default 1. Projection happens before modifiers and object transform, so rotated copies can use the same options. Geometry, modifiers, materials and cameras stay unchanged. Refuses nonplanar faces; islands overlap, no atlas packing or seamless-unwrap guarantee',
724
786
  ].join('\n ')
725
787
 
726
788
  function scenePatch(ctx) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deepblend/dsh-blender-tool",
3
- "version": "0.21.0",
3
+ "version": "0.23.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.21.0"
14
+ "@deepblend/dsh-blender-contracts": "0.23.0"
15
15
  },
16
16
  "license": "MIT",
17
17
  "repository": {