@deepblend/dsh-blender-provider-local 0.19.0 → 0.21.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
|
@@ -39,6 +39,8 @@ import {
|
|
|
39
39
|
SURFACE_PLACEMENT_LIMITS,
|
|
40
40
|
validateSurfacePlacementSelection,
|
|
41
41
|
verifySurfacePlacementReport,
|
|
42
|
+
validateAssemblyPlacementSelection,
|
|
43
|
+
verifyAssemblyPlacementReport,
|
|
42
44
|
BlenderError,
|
|
43
45
|
BlenderErrorCode,
|
|
44
46
|
CANDIDATE_RENDER_ENGINES,
|
|
@@ -1644,7 +1646,17 @@ export default class LocalBlenderRuntime extends Service {
|
|
|
1644
1646
|
|
|
1645
1647
|
/** Measure both actual meshes in a private batch; candidate verification also pins its baseline. */
|
|
1646
1648
|
async inspectSurfacePlacement(request) {
|
|
1647
|
-
|
|
1649
|
+
return this._inspectSurfacePlacement(request, false)
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
async inspectAssemblyPlacement(request) {
|
|
1653
|
+
return this._inspectSurfacePlacement(request, true)
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
async _inspectSurfacePlacement(request, assembly) {
|
|
1657
|
+
const validate = assembly ? validateAssemblyPlacementSelection : validateSurfacePlacementSelection
|
|
1658
|
+
const verifyReport = assembly ? verifyAssemblyPlacementReport : verifySurfacePlacementReport
|
|
1659
|
+
validate(request)
|
|
1648
1660
|
const aborted = () => {
|
|
1649
1661
|
if (request.signal?.aborted) throw new BlenderError(BlenderErrorCode.ABORTED, 'Surface placement cancelled.')
|
|
1650
1662
|
}
|
|
@@ -1682,8 +1694,11 @@ export default class LocalBlenderRuntime extends Service {
|
|
|
1682
1694
|
const run = await this.runBootstrap({ action: 'inspect_surface_placement', jobId: correlationId,
|
|
1683
1695
|
payload: { subjectEntityId: request.subjectEntityId, supportEntityId: request.supportEntityId,
|
|
1684
1696
|
frame: request.frame, clearanceMm: request.clearanceMm ?? 0, checkpointSha256: request.checkpointSha256,
|
|
1697
|
+
...(assembly ? { scope: 'recipe-assembly', memberEntityIds: request.memberEntityIds, pose: request.pose } : {}),
|
|
1685
1698
|
...(sources.length === 1 ? {} : { baselineCheckpointPath: sources[1].snapshot,
|
|
1686
|
-
baselineCheckpointSha256: request.baselineCheckpointSha256,
|
|
1699
|
+
baselineCheckpointSha256: request.baselineCheckpointSha256,
|
|
1700
|
+
...(assembly ? { expectedWorldMatrixMeters: request.expectedWorldMatrixMeters }
|
|
1701
|
+
: { expectedTranslationMeters: request.expectedTranslationMeters }) }) } },
|
|
1687
1702
|
{ signal: request.signal, session: false, args: ['--blend', sources[0].snapshot] })
|
|
1688
1703
|
aborted()
|
|
1689
1704
|
if (run.exitCode !== 0) throw new BlenderError(BlenderErrorCode.NONZERO_EXIT,
|
|
@@ -1693,7 +1708,7 @@ export default class LocalBlenderRuntime extends Service {
|
|
|
1693
1708
|
throw new BlenderError(BlenderErrorCode.PROTOCOL_VERSION_MISMATCH,
|
|
1694
1709
|
'Native placement inspection did not return a successful response for this request.')
|
|
1695
1710
|
}
|
|
1696
|
-
const report =
|
|
1711
|
+
const report = verifyReport(run.envelope?.result, request)
|
|
1697
1712
|
for (const source of sources) {
|
|
1698
1713
|
await verify(source.path, source.sha256)
|
|
1699
1714
|
await verify(source.snapshot, source.sha256)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deepblend/dsh-blender-provider-local",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.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.
|
|
14
|
+
"@deepblend/dsh-blender-contracts": "0.21.0"
|
|
15
15
|
},
|
|
16
16
|
"license": "MIT",
|
|
17
17
|
"repository": {
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
"""Read-only whole-assembly pose/settle measurement and complete candidate proof.
|
|
2
|
+
|
|
3
|
+
The public Host owns recipe provenance and complete membership. This native layer
|
|
4
|
+
measures the exact supplied union, then independently reopens both pinned files
|
|
5
|
+
and checks every evaluated member against one rigid upright world transform.
|
|
6
|
+
"""
|
|
7
|
+
import math
|
|
8
|
+
import re
|
|
9
|
+
|
|
10
|
+
from deepblend_util import ActionError
|
|
11
|
+
from deepblend_surface_placement import (
|
|
12
|
+
_capture_checkpoint, _verified_checkpoint, _unsupported, _finite_number, _active_animation,
|
|
13
|
+
solve_surface_placement, SurfacePlacementError, MAX_MESH_OBJECTS, MAX_VERTICES,
|
|
14
|
+
MAX_TRIANGLES, MAX_HIERARCHY_OBJECTS, TRANSLATION_TOLERANCE_METERS,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
BASIS_TOLERANCE = 1e-6
|
|
18
|
+
UV_TOLERANCE = 1e-6
|
|
19
|
+
IDENTITY = [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _fail(message):
|
|
23
|
+
raise ActionError('SCENE_VALIDATION_FAILED', message)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _matrix_product(a, b):
|
|
27
|
+
return [[sum(a[i][k] * b[k][j] for k in range(4)) for j in range(4)] for i in range(4)]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _point(matrix, point):
|
|
31
|
+
return tuple(sum(matrix[i][j] * point[j] for j in range(3)) + matrix[i][3] for i in range(3))
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _valid_pose(pose):
|
|
35
|
+
return (isinstance(pose, dict) and set(pose) == {'translationMm', 'yawDegrees', 'pivotMm'}
|
|
36
|
+
and all(isinstance(pose[key], list) and len(pose[key]) == 3
|
|
37
|
+
and all(_finite_number(value) and abs(value) <= 1_000_000 for value in pose[key])
|
|
38
|
+
for key in ('translationMm', 'pivotMm'))
|
|
39
|
+
and _finite_number(pose['yawDegrees']) and abs(pose['yawDegrees']) <= 360)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _pose_matrix(pose):
|
|
43
|
+
angle = pose['yawDegrees'] * math.pi / 180
|
|
44
|
+
c, s = math.cos(angle), math.sin(angle)
|
|
45
|
+
x, y = (value / 1000 for value in pose['pivotMm'][:2])
|
|
46
|
+
dx, dy, dz = (value / 1000 for value in pose['translationMm'])
|
|
47
|
+
return [[c, -s, 0, x-c*x+s*y+dx], [s, c, 0, y-s*x-c*y+dy],
|
|
48
|
+
[0, 0, 1, dz], [0, 0, 0, 1]]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _upright_matrix(matrix):
|
|
52
|
+
if (not isinstance(matrix, list) or len(matrix) != 4
|
|
53
|
+
or any(not isinstance(row, list) or len(row) != 4
|
|
54
|
+
or any(not _finite_number(value) or abs(value) > 1e6 for value in row) for row in matrix)):
|
|
55
|
+
return False
|
|
56
|
+
near = lambda a, b: abs(a-b) <= 1e-9
|
|
57
|
+
return (all(near(matrix[3][i], value) for i, value in enumerate((0, 0, 0, 1)))
|
|
58
|
+
and all(near(matrix[2][i], value) for i, value in enumerate((0, 0, 1)))
|
|
59
|
+
and near(matrix[0][2], 0) and near(matrix[1][2], 0)
|
|
60
|
+
and near(matrix[0][0], matrix[1][1]) and near(matrix[0][1], -matrix[1][0])
|
|
61
|
+
and near(matrix[0][0]**2+matrix[1][0]**2, 1))
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _cycle(values):
|
|
65
|
+
values = tuple(values)
|
|
66
|
+
return min(values[i:] + values[:i] for i in range(len(values)))
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _socket_value(value):
|
|
70
|
+
if isinstance(value, (str, bool, int, float)):
|
|
71
|
+
return value
|
|
72
|
+
if hasattr(value, '__len__') and not hasattr(value, 'bl_rna'):
|
|
73
|
+
return tuple(value)
|
|
74
|
+
if value is None:
|
|
75
|
+
return None
|
|
76
|
+
return (getattr(getattr(value, 'bl_rna', None), 'identifier', type(value).__name__), getattr(value, 'name', None))
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _material_facts(material):
|
|
80
|
+
if material is None:
|
|
81
|
+
return None
|
|
82
|
+
tree = material.node_tree
|
|
83
|
+
if _active_animation(material) or _active_animation(tree):
|
|
84
|
+
_unsupported('animated-assembly-material', 'Complete assembly placement requires static materials.')
|
|
85
|
+
nodes = []
|
|
86
|
+
for node in tree.nodes if tree else []:
|
|
87
|
+
properties = tuple((key, getattr(node, key)) for key in ('operation', 'blend_type', 'noise_dimensions',
|
|
88
|
+
'normalize', 'noise_type', 'distribution', 'subsurface_method', 'gradient_type', 'voronoi_dimensions',
|
|
89
|
+
'feature', 'distance', 'interpolation', 'extension', 'projection', 'vector_type', 'convert_from', 'convert_to')
|
|
90
|
+
if hasattr(node, key))
|
|
91
|
+
nodes.append((node.name, node.bl_idname, properties,
|
|
92
|
+
tuple((socket.identifier, _socket_value(socket.default_value)) for socket in node.inputs
|
|
93
|
+
if hasattr(socket, 'default_value'))))
|
|
94
|
+
links = sorted((link.from_node.name, link.from_socket.identifier, link.to_node.name, link.to_socket.identifier)
|
|
95
|
+
for link in tree.links) if tree else []
|
|
96
|
+
return (material.name, tuple(material.diffuse_color), bool(material.use_nodes), sorted(nodes), links)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _capture(path, expected, frame, member_ids, support_id):
|
|
100
|
+
"""Reuse the static, visibility, source hash and cross-object checks of v1."""
|
|
101
|
+
import bpy
|
|
102
|
+
captures = _capture_checkpoint(path, expected, frame, member_ids, support_id)
|
|
103
|
+
graph = bpy.context.evaluated_depsgraph_get()
|
|
104
|
+
for capture in captures:
|
|
105
|
+
for part in capture['parts']:
|
|
106
|
+
obj = bpy.data.objects[part['identity'][0]]
|
|
107
|
+
evaluated = obj.evaluated_get(graph)
|
|
108
|
+
mesh = evaluated.to_mesh()
|
|
109
|
+
try:
|
|
110
|
+
# Polygon order and cyclic start are not identity. Preserve the
|
|
111
|
+
# complete oriented faces, per-face material and loop UV data.
|
|
112
|
+
faces = []
|
|
113
|
+
for face in mesh.polygons:
|
|
114
|
+
corners = []
|
|
115
|
+
for loop_index in face.loop_indices:
|
|
116
|
+
corners.append((mesh.loops[loop_index].vertex_index,
|
|
117
|
+
tuple(tuple(layer.uv[loop_index].vector) for layer in mesh.uv_layers)))
|
|
118
|
+
faces.append((_cycle(corners), face.material_index, bool(face.use_smooth)))
|
|
119
|
+
part['appearance'] = (tuple(layer.name for layer in mesh.uv_layers), sorted(faces),
|
|
120
|
+
tuple(_material_facts(slot.material) for slot in obj.material_slots))
|
|
121
|
+
finally:
|
|
122
|
+
evaluated.to_mesh_clear()
|
|
123
|
+
total_meshes = sum(item['metadata']['meshObjectCount'] for item in captures[:-1])
|
|
124
|
+
total_vertices = sum(item['metadata']['vertexCount'] for item in captures[:-1])
|
|
125
|
+
total_triangles = sum(item['metadata']['triangleCount'] for item in captures[:-1])
|
|
126
|
+
if (total_meshes > MAX_MESH_OBJECTS or total_vertices > MAX_VERTICES or total_triangles > MAX_TRIANGLES
|
|
127
|
+
or sum(len(item['hierarchyIdentity']) for item in captures[:-1]) > MAX_HIERARCHY_OBJECTS):
|
|
128
|
+
_unsupported('geometry-budget-exceeded', 'The complete subject union exceeds the bounded native geometry budget.')
|
|
129
|
+
_verified_checkpoint(path, expected)
|
|
130
|
+
return captures
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _transformed(capture, matrix):
|
|
134
|
+
triangles = [tuple(_point(matrix, point) for point in triangle) for triangle in capture['triangles']]
|
|
135
|
+
parts = [{**part, 'vertices': [_point(matrix, point) for point in part['vertices']]} for part in capture['parts']]
|
|
136
|
+
points = [point for part in parts for point in part['vertices']]
|
|
137
|
+
metadata = {**capture['metadata'], 'rootWorldMatrix': _matrix_product(matrix, capture['metadata']['rootWorldMatrix']),
|
|
138
|
+
'worldBoundsMeters': {'min': [min(point[a] for point in points) for a in range(3)],
|
|
139
|
+
'max': [max(point[a] for point in points) for a in range(3)]}}
|
|
140
|
+
return {**capture, 'parts': parts, 'triangles': triangles, 'metadata': metadata}
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _compare_appearance(old, new, label):
|
|
144
|
+
if old[0] != new[0] or old[2] != new[2] or len(old[1]) != len(new[1]):
|
|
145
|
+
_fail(label + ' native UV layers, materials or mesh faces changed.')
|
|
146
|
+
maximum_uv_error = 0.0
|
|
147
|
+
for a, b in zip(old[1], new[1]):
|
|
148
|
+
if a[1:] != b[1:] or len(a[0]) != len(b[0]):
|
|
149
|
+
_fail(label + ' native face identity, material indices or smooth flags changed.')
|
|
150
|
+
for ac, bc in zip(a[0], b[0]):
|
|
151
|
+
if ac[0] != bc[0] or len(ac[1]) != len(bc[1]):
|
|
152
|
+
_fail(label + ' native oriented topology or loop UV identity changed.')
|
|
153
|
+
for auv, buv in zip(ac[1], bc[1]):
|
|
154
|
+
maximum_uv_error = max(maximum_uv_error, max(abs(a-b) for a, b in zip(auv, buv)))
|
|
155
|
+
if maximum_uv_error > UV_TOLERANCE:
|
|
156
|
+
_fail(label + ' evaluated UV coordinates changed beyond float32 reconstruction tolerance.')
|
|
157
|
+
return maximum_uv_error
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _compare_rigid(baseline, current, matrix, label):
|
|
161
|
+
if baseline['hierarchyIdentity'] != current['hierarchyIdentity'] or len(baseline['parts']) != len(current['parts']):
|
|
162
|
+
_fail(label + ' hierarchy identity changed.')
|
|
163
|
+
maximum, maximum_uv_error = 0.0, 0.0
|
|
164
|
+
for old, new in zip(baseline['parts'], current['parts']):
|
|
165
|
+
if old['identity'] != new['identity'] or len(old['vertices']) != len(new['vertices']):
|
|
166
|
+
_fail(label + ' mesh topology or identity changed.')
|
|
167
|
+
maximum_uv_error = max(maximum_uv_error, _compare_appearance(old['appearance'], new['appearance'], label))
|
|
168
|
+
for before, after in zip(old['vertices'], new['vertices']):
|
|
169
|
+
maximum = max(maximum, math.dist(_point(matrix, before), after))
|
|
170
|
+
expected = _matrix_product(matrix, baseline['metadata']['rootWorldMatrix'])
|
|
171
|
+
actual = current['metadata']['rootWorldMatrix']
|
|
172
|
+
position_error = max(abs(expected[i][3]-actual[i][3]) for i in range(3))
|
|
173
|
+
basis_error = max(abs(expected[i][j]-actual[i][j]) for i in range(4) for j in range(4) if j != 3)
|
|
174
|
+
if maximum > TRANSLATION_TOLERANCE_METERS or position_error > TRANSLATION_TOLERANCE_METERS or basis_error > BASIS_TOLERANCE:
|
|
175
|
+
_fail(label + ' did not follow the complete rigid world transform.')
|
|
176
|
+
return {'rigidTransform': True, 'maxVertexErrorMeters': maximum,
|
|
177
|
+
'maxRootPositionErrorMeters': position_error, 'maxRootBasisError': basis_error, 'maxUvError': maximum_uv_error}
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def inspect_assembly_placement(request, options):
|
|
181
|
+
import bpy
|
|
182
|
+
payload = request.get('payload')
|
|
183
|
+
allowed = {'scope', 'subjectEntityId', 'memberEntityIds', 'supportEntityId', 'frame', 'clearanceMm', 'pose',
|
|
184
|
+
'checkpointSha256', 'baselineCheckpointPath', 'baselineCheckpointSha256', 'expectedWorldMatrixMeters'}
|
|
185
|
+
valid_id = lambda value: isinstance(value, str) and re.fullmatch(r'[A-Za-z][A-Za-z0-9._-]{0,127}', value) is not None
|
|
186
|
+
valid_hash = lambda value: isinstance(value, str) and re.fullmatch('[a-f0-9]{64}', value) is not None
|
|
187
|
+
if not isinstance(payload, dict) or set(payload) - allowed or payload.get('scope') != 'recipe-assembly':
|
|
188
|
+
_fail('Unsupported complete assembly measurement payload.')
|
|
189
|
+
ids, subject, support = payload.get('memberEntityIds'), payload.get('subjectEntityId'), payload.get('supportEntityId')
|
|
190
|
+
frame, expected, pose, clearance = payload.get('frame'), payload.get('checkpointSha256'), payload.get('pose'), payload.get('clearanceMm', 0)
|
|
191
|
+
if (not isinstance(ids, list) or not 1 <= len(ids) <= MAX_MESH_OBJECTS or not all(valid_id(value) for value in ids)
|
|
192
|
+
or ids != sorted(set(ids)) or not valid_id(subject) or subject not in ids or not valid_id(support) or support in ids
|
|
193
|
+
or type(frame) is not int or not 0 <= frame <= 1_000_000 or not valid_hash(expected)
|
|
194
|
+
or not _valid_pose(pose) or not _finite_number(clearance) or not 0 <= clearance <= 1000):
|
|
195
|
+
_fail('Complete assembly selection, pose, frame or checkpoint binding is invalid.')
|
|
196
|
+
baseline_fields = {'baselineCheckpointPath', 'baselineCheckpointSha256', 'expectedWorldMatrixMeters'}
|
|
197
|
+
has_baseline = bool(baseline_fields & set(payload))
|
|
198
|
+
baseline = None
|
|
199
|
+
if has_baseline:
|
|
200
|
+
if (not baseline_fields <= set(payload) or not valid_hash(payload['baselineCheckpointSha256'])
|
|
201
|
+
or not isinstance(payload['baselineCheckpointPath'], str) or not _upright_matrix(payload['expectedWorldMatrixMeters'])):
|
|
202
|
+
_fail('Complete candidate verification requires a pinned baseline and a rigid upright matrix.')
|
|
203
|
+
baseline = _capture(payload['baselineCheckpointPath'], payload['baselineCheckpointSha256'], frame, ids, support)
|
|
204
|
+
checkpoint = options.get('blend')
|
|
205
|
+
current = _capture(checkpoint, expected, frame, ids, support)
|
|
206
|
+
verification = None
|
|
207
|
+
if has_baseline:
|
|
208
|
+
members = [{'entityId': entity_id, **_compare_rigid(baseline[index], current[index], payload['expectedWorldMatrixMeters'], entity_id)}
|
|
209
|
+
for index, entity_id in enumerate(ids)]
|
|
210
|
+
support_proof = _compare_rigid(baseline[-1], current[-1], IDENTITY, support)
|
|
211
|
+
_verified_checkpoint(payload['baselineCheckpointPath'], payload['baselineCheckpointSha256'])
|
|
212
|
+
verification = {'checkpointSha256': payload['baselineCheckpointSha256'], 'checkpointUnchanged': True,
|
|
213
|
+
'worldMatrixMeters': payload['expectedWorldMatrixMeters'], 'members': members,
|
|
214
|
+
'supportUnchanged': True, 'supportMaxErrorMeters': support_proof['maxVertexErrorMeters'],
|
|
215
|
+
'toleranceMeters': TRANSLATION_TOLERANCE_METERS, 'basisTolerance': BASIS_TOLERANCE, 'uvTolerance': UV_TOLERANCE}
|
|
216
|
+
matrix = _pose_matrix(pose)
|
|
217
|
+
transformed = [_transformed(item, matrix) for item in current[:-1]]
|
|
218
|
+
triangles = [triangle for item in transformed for triangle in item['triangles']]
|
|
219
|
+
support_capture = current[-1]
|
|
220
|
+
try:
|
|
221
|
+
solution = solve_surface_placement(triangles, support_capture['triangles'], clearance / 1000)
|
|
222
|
+
except SurfacePlacementError as exc:
|
|
223
|
+
_unsupported(exc.reason, str(exc))
|
|
224
|
+
result_matrix = [list(row) for row in matrix]; result_matrix[2][3] += solution['deltaZ']
|
|
225
|
+
members = [{'entityId': entity_id, 'geometry': transformed[index]['metadata']} for index, entity_id in enumerate(ids)]
|
|
226
|
+
offset, contact_member = 0, None
|
|
227
|
+
for member in members:
|
|
228
|
+
count = member['geometry']['triangleCount']
|
|
229
|
+
if offset <= solution['subjectTriangleIndex'] < offset + count:
|
|
230
|
+
contact_member = member['entityId']
|
|
231
|
+
offset += count
|
|
232
|
+
union = {key: sum(member['geometry'][key] for member in members) for key in ('meshObjectCount', 'vertexCount', 'triangleCount')}
|
|
233
|
+
union['rootWorldMatrix'] = next(member['geometry']['rootWorldMatrix'] for member in members if member['entityId'] == subject)
|
|
234
|
+
union['worldBoundsMeters'] = {'min': [min(member['geometry']['worldBoundsMeters']['min'][a] for member in members) for a in range(3)],
|
|
235
|
+
'max': [max(member['geometry']['worldBoundsMeters']['max'][a] for member in members) for a in range(3)]}
|
|
236
|
+
report = {'schemaVersion': 'deepblend.assembly-placement/v1', 'axis': 'world-z', 'checkpointSha256': expected,
|
|
237
|
+
'checkpointUnchanged': True, 'frame': frame, 'blenderVersion': bpy.app.version_string,
|
|
238
|
+
'subjectEntityId': subject, 'memberEntityIds': ids, 'members': members, 'supportEntityId': support,
|
|
239
|
+
'pose': pose, 'poseMatrixMeters': matrix, 'resultWorldMatrixMeters': result_matrix,
|
|
240
|
+
'clearanceMm': clearance, 'translationMeters': [0, 0, solution['deltaZ']],
|
|
241
|
+
'subjectContactEntityId': contact_member, 'subjectContactMeters': solution['subjectContactMeters'],
|
|
242
|
+
'supportContactMeters': solution['supportContactMeters'], 'placedSubjectContactMeters': solution['placedSubjectContactMeters'],
|
|
243
|
+
'subjectTriangleIndex': solution['subjectTriangleIndex'], 'supportTriangleIndex': solution['supportTriangleIndex'],
|
|
244
|
+
'statistics': {'candidatePairs': solution['statistics']['candidatePairs']},
|
|
245
|
+
'subject': union, 'support': support_capture['metadata']}
|
|
246
|
+
if verification is not None:
|
|
247
|
+
report['baselineVerification'] = verification
|
|
248
|
+
_verified_checkpoint(checkpoint, expected)
|
|
249
|
+
return report, [], []
|
|
@@ -520,7 +520,8 @@ def _capture_checkpoint(path, expected, frame, subject_id, support_id):
|
|
|
520
520
|
render_visible.update(item.as_pointer() for item in collection.objects)
|
|
521
521
|
pending_collections.extend(collection.children)
|
|
522
522
|
result = []
|
|
523
|
-
|
|
523
|
+
entity_ids = [*subject_id, support_id] if isinstance(subject_id, list) else [subject_id, support_id]
|
|
524
|
+
for entity_id in entity_ids:
|
|
524
525
|
obj = bpy.data.objects.get('db_entity__' + entity_id)
|
|
525
526
|
if obj is None:
|
|
526
527
|
raise ActionError('SCENE_SPEC_INVALID', 'The selected native entity does not exist: ' + entity_id)
|
|
@@ -559,6 +560,9 @@ def inspect_surface_placement(request, options):
|
|
|
559
560
|
"""Inspect a hash-bound checkpoint, optionally proving a saved rigid move."""
|
|
560
561
|
import bpy
|
|
561
562
|
payload = request.get('payload')
|
|
563
|
+
if isinstance(payload, dict) and payload.get('scope') == 'recipe-assembly':
|
|
564
|
+
from deepblend_assembly_placement import inspect_assembly_placement
|
|
565
|
+
return inspect_assembly_placement(request, options)
|
|
562
566
|
allowed = {'subjectEntityId', 'supportEntityId', 'frame', 'clearanceMm', 'checkpointSha256',
|
|
563
567
|
'baselineCheckpointPath', 'baselineCheckpointSha256', 'expectedTranslationMeters'}
|
|
564
568
|
if not isinstance(payload, dict) or set(payload) - allowed:
|