@deepblend/dsh-blender-provider-local 0.11.0 → 0.13.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/index.js CHANGED
@@ -1512,12 +1512,21 @@ export default class LocalBlenderRuntime extends Service {
1512
1512
  )
1513
1513
  }
1514
1514
 
1515
+ if (request.cameraFraming !== undefined) {
1516
+ const selection = request.cameraFraming
1517
+ if (!selection || typeof selection !== 'object' || Array.isArray(selection)
1518
+ || Object.keys(selection).length !== 3 || selection.entityId !== 'cup'
1519
+ || selection.cameraId !== 'hero' || selection.margin !== 0.03) {
1520
+ throw new BlenderError(BlenderErrorCode.SCENE_SPEC_INVALID, 'Expected the bounded cup/hero framing selection')
1521
+ }
1522
+ }
1515
1523
  const args = ['--scene-spec', request.sceneSpecPath, '--output-blend', outputBlend]
1516
1524
  if (request.profile !== undefined) args.push('--profile', String(request.profile))
1517
1525
  if (request.projectRoot !== undefined) args.push('--project-root', request.projectRoot)
1518
1526
 
1519
1527
  const run = await this.runBootstrap(
1520
- { action: 'compile_scene', jobId: request.jobId },
1528
+ { action: 'compile_scene', jobId: request.jobId,
1529
+ ...(request.cameraFraming === undefined ? {} : { payload: { cameraFraming: request.cameraFraming } }) },
1521
1530
  {
1522
1531
  signal: request.signal,
1523
1532
  session: request.session,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deepblend/dsh-blender-provider-local",
3
- "version": "0.11.0",
3
+ "version": "0.13.0",
4
4
  "type": "module",
5
5
  "description": "Local Blender Provider: the BlenderRuntime Service over ctx.subprocess (batch execution).",
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.11.0"
14
+ "@deepblend/dsh-blender-contracts": "0.13.0"
15
15
  },
16
16
  "license": "MIT",
17
17
  "repository": {
@@ -216,6 +216,11 @@ def action_compile_scene(request, options):
216
216
  "project_root": options.get("project_root"),
217
217
  }
218
218
  report = build_scene(spec, build_options, guard)
219
+ framing_selection = (request.get('payload') or {}).get('cameraFraming')
220
+ framing = None
221
+ if framing_selection is not None:
222
+ from deepblend_camera_framing import measure_cup_hero_framing
223
+ framing = measure_cup_hero_framing(spec, framing_selection)
219
224
 
220
225
  report_progress("validate", 92)
221
226
  validation = validate_scene(bpy.context.scene, spec, guard)
@@ -268,6 +273,9 @@ def action_compile_scene(request, options):
268
273
  "sceneFingerprint": scene_fingerprint(spec, objects),
269
274
  }
270
275
 
276
+ if framing is not None:
277
+ payload['cameraFraming'] = framing
278
+
271
279
  # The checkpoint is written even when validation reported errors: the caller
272
280
  # decides whether to publish, and a diagnostic blend is far more useful than
273
281
  # no blend at all when a compile went wrong. Nothing is published by THIS
@@ -0,0 +1,64 @@
1
+ """Bounded, read-only framing measurements of one evaluated cup and hero camera."""
2
+ import math
3
+ import bpy
4
+ from deepblend_util import ActionError
5
+
6
+
7
+ def measure_cup_hero_framing(spec, selection):
8
+ if not isinstance(selection, dict) or set(selection) != {'entityId', 'cameraId', 'margin'} or selection != {'entityId': 'cup', 'cameraId': 'hero', 'margin': 0.03}:
9
+ raise ActionError('SCENE_VALIDATION_FAILED', 'Expected one bounded cup/hero framing selection')
10
+ entity = next((e for e in spec.get('entities', []) if e.get('id') == 'cup'), None)
11
+ if not entity or entity.get('generator', {}).get('shape') != 'handled_cup':
12
+ raise ActionError('SCENE_VALIDATION_FAILED', 'Cup framing needs the declared handled-cup entity')
13
+ camera = bpy.data.objects.get('db_camera__hero')
14
+ obj = bpy.data.objects.get('db_entity__cup')
15
+ if camera is None or obj is None or camera.type != 'CAMERA' or obj.type != 'MESH' or camera.data.type != 'PERSP':
16
+ raise ActionError('SCENE_VALIDATION_FAILED', 'Native cup and perspective hero are required for framing')
17
+ scene = bpy.context.scene
18
+ if not obj.visible_get(view_layer=bpy.context.view_layer) or obj.hide_render:
19
+ raise ActionError('SCENE_VALIDATION_FAILED', 'Cannot frame an unavailable cup surface')
20
+ if any(t.get('targetKind') == 'camera' and t.get('targetEntityId') == 'hero' for t in spec.get('animationTracks', [])):
21
+ raise ActionError('SCENE_VALIDATION_FAILED', 'An authored camera animation cannot be reframed')
22
+ depsgraph = bpy.context.evaluated_depsgraph_get()
23
+ measured_camera = camera.evaluated_get(depsgraph)
24
+ inverse = measured_camera.matrix_world.inverted()
25
+ frame = measured_camera.data.view_frame(scene=scene)
26
+ slopes = [(p.x / -p.z, p.y / -p.z) for p in frame]
27
+ left, right = min(p[0] for p in slopes), max(p[0] for p in slopes)
28
+ bottom, top = min(p[1] for p in slopes), max(p[1] for p in slopes)
29
+ margin = selection['margin']
30
+ left, right = left + (right-left)*margin, right - (right-left)*margin
31
+ bottom, top = bottom + (top-bottom)*margin, top - (top-bottom)*margin
32
+ if not left < 0 < right or not bottom < 0 < top:
33
+ raise ActionError('SCENE_VALIDATION_FAILED', 'The hero optical axis is outside the usable frame')
34
+ evaluated = obj.evaluated_get(depsgraph)
35
+ mesh = evaluated.to_mesh(preserve_all_data_layers=True, depsgraph=depsgraph)
36
+ try:
37
+ if mesh is None or not 1 <= len(mesh.vertices) <= 2000000:
38
+ raise ActionError('SCENE_TOO_HEAVY', 'Cup framing requires 1 to 2000000 evaluated vertices')
39
+ backoff, nearest, farthest = 0.0, math.inf, -math.inf
40
+ for vertex in mesh.vertices:
41
+ point = inverse @ (evaluated.matrix_world @ vertex.co)
42
+ depth = -point.z
43
+ if not all(math.isfinite(v) for v in [point.x, point.y, depth]):
44
+ raise ActionError('SCENE_VALIDATION_FAILED', 'Non-finite evaluated framing vertex')
45
+ nearest, farthest = min(nearest, depth), max(farthest, depth)
46
+ backoff = max(backoff, point.x/right-depth, point.x/left-depth,
47
+ point.y/top-depth, point.y/bottom-depth,
48
+ camera.data.clip_start+1e-5-depth)
49
+ # Avoid a second corrective compile due to single-precision matrix noise.
50
+ backoff = backoff + 1e-5 if backoff > 1e-6 else 0.0
51
+ if not math.isfinite(backoff) or farthest+backoff >= camera.data.clip_end-1e-5:
52
+ raise ActionError('SCENE_VALIDATION_FAILED', 'Cup framing exceeds the hero far clip')
53
+ return {'schemaVersion': 'deepblend.camera-framing/v1', **selection,
54
+ 'frame': int(scene.frame_current), 'resolution': [scene.render.resolution_x, scene.render.resolution_y],
55
+ 'resolutionPercentage': scene.render.resolution_percentage,
56
+ 'pixelAspect': [scene.render.pixel_aspect_x, scene.render.pixel_aspect_y],
57
+ 'cameraLocation': list(measured_camera.matrix_world.translation),
58
+ 'cameraType': camera.data.type, 'lens': camera.data.lens, 'sensorWidth': camera.data.sensor_width,
59
+ 'backoffM': backoff, 'vertices': len(mesh.vertices),
60
+ 'nearestDepthM': nearest+backoff, 'farthestDepthM': farthest+backoff,
61
+ 'scope': 'evaluated-cup-vertices-at-reported-frame-and-render-aspect'}
62
+ finally:
63
+ if mesh is not None:
64
+ evaluated.to_mesh_clear()