@deepblend/dsh-blender-tool 0.11.0 → 0.12.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/assembly-origins.js +87 -0
- package/lib/tools.js +18 -6
- package/package.json +2 -2
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/** Endpoint origins from verified native reports; these are not surface clearances. */
|
|
2
|
+
import { BlenderError, BlenderErrorCode } from '@deepblend/dsh-blender-contracts'
|
|
3
|
+
|
|
4
|
+
const refuse = message => { throw new BlenderError(BlenderErrorCode.SCRIPT_ERROR, `Native assembly origins: ${message}`) }
|
|
5
|
+
const finite = values => {
|
|
6
|
+
if (!values.every(Number.isFinite)) refuse('the derived origin/span values exceed the finite numeric range.')
|
|
7
|
+
return values
|
|
8
|
+
}
|
|
9
|
+
const subtract = (a, b) => finite(a.map((value, axis) => value - b[axis]))
|
|
10
|
+
const subtractMm = (a, b) => finite(subtract(a, b).map(value => value * 1000))
|
|
11
|
+
const identity = report => ({ projectId: report.projectId, revision: report.sourceRevision,
|
|
12
|
+
checkpointSha256: report.sourceCheckpointSha256, frame: report.frame })
|
|
13
|
+
const limitations = [
|
|
14
|
+
'Offsets join evaluated native OBJECT ORIGINS in world XYZ axes, not surface points. They are not protrusion, contact, seating depth or clearance.',
|
|
15
|
+
'World-axis offsets and spans change under rotation; they are not intrinsic dimensions or anchor-local coordinates.',
|
|
16
|
+
'Only the selected parts, ordered pairs and explicit frame are measured. Unavailable parts have no numeric values, never zero.',
|
|
17
|
+
'No whole-assembly attachment, collision absence, minimum clearance, manufacturing tolerance or artistic approval is established.',
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
export function validateAssemblyPairs(pairs, entityIds) {
|
|
21
|
+
if (!Array.isArray(entityIds) || !Array.isArray(pairs) || pairs.length < 1 || pairs.length > 8) refuse('select 1–8 explicit ordered pairs within geometryEntityIds.')
|
|
22
|
+
const ids = new Set()
|
|
23
|
+
for (const pair of pairs) {
|
|
24
|
+
if (!pair || typeof pair !== 'object' || Array.isArray(pair) ||
|
|
25
|
+
Object.keys(pair).some(key => !['pairId', 'entityId', 'anchorEntityId'].includes(key)) ||
|
|
26
|
+
typeof pair.pairId !== 'string' || !/^[a-zA-Z0-9_-]{1,64}$/.test(pair.pairId) || ids.has(pair.pairId) ||
|
|
27
|
+
!entityIds.includes(pair.entityId) || !entityIds.includes(pair.anchorEntityId) || pair.entityId === pair.anchorEntityId) {
|
|
28
|
+
refuse('each pair requires a distinct pairId and two different selected entity/anchor IDs; arbitrary fields are refused.')
|
|
29
|
+
}
|
|
30
|
+
ids.add(pair.pairId)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function origin(object) {
|
|
35
|
+
const matrix = object.worldMatrix
|
|
36
|
+
if (!Array.isArray(matrix) || matrix.length !== 4 || matrix.some(row => !Array.isArray(row) || row.length !== 4 || row.some(value => !Number.isFinite(value))) ||
|
|
37
|
+
matrix[3].some((value, axis) => value !== (axis === 3 ? 1 : 0))) refuse('the native world matrix is not finite affine geometry.')
|
|
38
|
+
return matrix.slice(0, 3).map(row => row[3])
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function readAssemblyOrigins(report, pairs) {
|
|
42
|
+
validateAssemblyPairs(pairs, report.objects.map(object => object.entityId))
|
|
43
|
+
const objects = new Map(report.objects.map(object => [object.entityId, object]))
|
|
44
|
+
return { schemaVersion: 'deepblend.assembly-origins/v1', source: identity(report), coordinateSystem: 'world',
|
|
45
|
+
pairs: pairs.map(pair => {
|
|
46
|
+
const entity = objects.get(pair.entityId), anchor = objects.get(pair.anchorEntityId)
|
|
47
|
+
if (!entity.available || !anchor.available) return { ...pair, status: 'unavailable',
|
|
48
|
+
unavailableParts: [entity, anchor].filter(object => !object.available).map(object => ({ entityId: object.entityId, reason: object.unavailableReason })) }
|
|
49
|
+
const entityOriginMeters = origin(entity), anchorOriginMeters = origin(anchor)
|
|
50
|
+
return { ...pair, status: 'measured', entityOriginMeters, anchorOriginMeters,
|
|
51
|
+
worldOriginOffsetMm: subtractMm(entityOriginMeters, anchorOriginMeters) }
|
|
52
|
+
}), limitations }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function compareAssemblyOrigins(before, after) {
|
|
56
|
+
if (before.source.projectId !== after.source.projectId || before.source.frame !== after.source.frame || before.pairs.length !== after.pairs.length) refuse('comparison requires the same project, frame and ordered pairs.')
|
|
57
|
+
const pairs = after.pairs.map((current, index) => {
|
|
58
|
+
const reference = before.pairs[index]
|
|
59
|
+
if (['pairId', 'entityId', 'anchorEntityId'].some(key => reference[key] !== current[key])) refuse('comparison pair identities differ.')
|
|
60
|
+
return { pairId: current.pairId, entityId: current.entityId, anchorEntityId: current.anchorEntityId,
|
|
61
|
+
before: reference, after: current,
|
|
62
|
+
...(reference.status === 'measured' && current.status === 'measured' ? { worldOriginOffsetChangeMm: subtract(current.worldOriginOffsetMm, reference.worldOriginOffsetMm) } : {}) }
|
|
63
|
+
})
|
|
64
|
+
return { schemaVersion: 'deepblend.assembly-origin-comparison/v1', referenceSource: before.source, currentSource: after.source,
|
|
65
|
+
pairs, comparedPairs: pairs.filter(pair => pair.worldOriginOffsetChangeMm !== undefined).length,
|
|
66
|
+
limitations: ['Changes are current minus reference; offset preservation does not establish surface contact or attachment correctness.', ...limitations] }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Inputs have already passed source/selection/frame and native report verification. */
|
|
70
|
+
export function compareGeometryReports(reference, current) {
|
|
71
|
+
if (reference.projectId !== current.projectId || reference.frame !== current.frame || reference.objects.length !== current.objects.length) refuse('comparison requires the same project, frame and ordered entities.')
|
|
72
|
+
const objects = current.objects.map((after, index) => {
|
|
73
|
+
const before = reference.objects[index]
|
|
74
|
+
if (before.entityId !== after.entityId) refuse('comparison entity identities differ.')
|
|
75
|
+
const values = object => object.available ? { status: 'measured', worldAxisExtentMm: object.worldAxisExtentMm, worldOriginMeters: origin(object) } :
|
|
76
|
+
{ status: 'unavailable', reason: object.unavailableReason }
|
|
77
|
+
const row = { entityId: after.entityId, before: values(before), after: values(after) }
|
|
78
|
+
if (before.available && after.available) {
|
|
79
|
+
row.worldAxisExtentChangeMm = subtract(after.worldAxisExtentMm, before.worldAxisExtentMm)
|
|
80
|
+
row.worldOriginChangeMm = subtractMm(row.after.worldOriginMeters, row.before.worldOriginMeters)
|
|
81
|
+
}
|
|
82
|
+
return row
|
|
83
|
+
})
|
|
84
|
+
return { schemaVersion: 'deepblend.geometry-comparison/v1', referenceInspection: reference, currentSource: identity(current), objects,
|
|
85
|
+
summary: { requestedEntities: objects.length, comparedEntities: objects.filter(object => object.worldAxisExtentChangeMm !== undefined).length },
|
|
86
|
+
limitations: ['Changes are current minus reference for the same selected native parts and frame; their cause is not established.', ...limitations] }
|
|
87
|
+
}
|
package/lib/tools.js
CHANGED
|
@@ -54,6 +54,7 @@ import {
|
|
|
54
54
|
resolveStudio,
|
|
55
55
|
} from './shared.js'
|
|
56
56
|
import { compareSurfaceReports } from './geometry-comparison.js'
|
|
57
|
+
import { validateAssemblyPairs, readAssemblyOrigins, compareAssemblyOrigins, compareGeometryReports } from './assembly-origins.js'
|
|
57
58
|
|
|
58
59
|
/** A short parameter description of a SceneSpec engine key. */
|
|
59
60
|
const ENGINE_KEY_DESCRIPTION =
|
|
@@ -398,7 +399,7 @@ function sceneGet(ctx) {
|
|
|
398
399
|
'Optional geometrySurfaceSamples measures 1–8 explicit local surface chords and pair separations on the same saved source. ' +
|
|
399
400
|
'The chord runs from source entry to exit; separation runs from source EXIT to target entry and excludes that chord. ' +
|
|
400
401
|
'Use geometryOnly:true for a compact verified measurement receipt without the scene catalogue. ' +
|
|
401
|
-
'
|
|
402
|
+
'Add geometryAssemblyPairs to measure explicit ordered object-origin offsets in world XYZ, not surface clearances. When comparing revisions, geometryCompareRevision computes current-minus-reference native spans and origin moves; if surface samples are supplied, their existing local comparison is used. ' +
|
|
402
403
|
'Choose outside world seeds and directions from the actual scene/bounds; missed or unsupported samples remain unavailable. ' +
|
|
403
404
|
'Local samples do not prove full-surface minimum wall thickness, whole-assembly clearance, no collision or quality approval. ' +
|
|
404
405
|
'ALWAYS use the revision this returns as `baseRevision` for your next blender_scene_patch.',
|
|
@@ -410,7 +411,13 @@ function sceneGet(ctx) {
|
|
|
410
411
|
geometryFrame: { type: 'integer',
|
|
411
412
|
description: 'Explicit integer frame from 0 to 1000000 for native geometry measurement. Use the frame of the preview being checked. Requires geometryEntityIds.' },
|
|
412
413
|
geometryOnly: { type: 'boolean', description: 'Return only source identity and verified geometry evidence, without the scene catalogue. Requires geometryEntityIds and geometryFrame; cannot be combined with full:true. Ordinary reads keep their existing shape.' },
|
|
413
|
-
geometryCompareRevision: { type: 'string', description: 'Optional explicit reference revision (e.g. r0004). Requires
|
|
414
|
+
geometryCompareRevision: { type: 'string', description: 'Optional explicit reference revision (e.g. r0004). Requires geometryEntityIds and geometryFrame. Re-measures identical selected parts/frame and computes current-minus-reference native world-axis spans and origin moves. If geometrySurfaceSamples is supplied, compares those same local samples instead. Requested geometryAssemblyPairs also return origin-offset changes. Unavailable evidence stays unavailable; no clearance, global minima or change cause is established.' },
|
|
415
|
+
geometryAssemblyPairs: { type: 'array', description: 'Optional 1–8 ordered native object-origin pairs. Requires geometryEntityIds containing both ends and geometryFrame. Offsets use world XYZ in millimeters; they are not surface protrusion, contact, clearance or whole-assembly certification.',
|
|
416
|
+
items: { type: 'object', additionalProperties: false, properties: {
|
|
417
|
+
pairId: { type: 'string', required: true, description: 'Distinct letters/numbers/underscore/hyphen identifier (1–64 characters).' },
|
|
418
|
+
entityId: { type: 'string', required: true, description: 'Selected native mesh whose object origin is measured.' },
|
|
419
|
+
anchorEntityId: { type: 'string', required: true, description: 'Different selected mesh origin used as anchor; offset is entity minus anchor in world XYZ.' },
|
|
420
|
+
} } },
|
|
414
421
|
geometrySurfaceSamples: { type: 'array', description: 'Optional 1–8 local surface samples. Requires geometryEntityIds including every source/target and geometryFrame. Numeric results are local evidence only.',
|
|
415
422
|
items: { type: 'object', additionalProperties: false, properties: {
|
|
416
423
|
sampleId: { type: 'string', required: true, description: 'Distinct short letters/numbers/underscore/hyphen identifier (1–64 characters).' },
|
|
@@ -444,9 +451,10 @@ function sceneGet(ctx) {
|
|
|
444
451
|
throw new BlenderError(BlenderErrorCode.SCRIPT_ERROR, 'Surface samples require geometryEntityIds and geometryFrame.')
|
|
445
452
|
}
|
|
446
453
|
if (args.geometryCompareRevision !== undefined && (typeof args.geometryCompareRevision !== 'string' ||
|
|
447
|
-
!/^r\d{4,}$/.test(args.geometryCompareRevision) || args.
|
|
448
|
-
throw new BlenderError(BlenderErrorCode.SCRIPT_ERROR, 'geometryCompareRevision requires an explicit saved revision and
|
|
454
|
+
!/^r\d{4,}$/.test(args.geometryCompareRevision) || args.geometryEntityIds === undefined)) {
|
|
455
|
+
throw new BlenderError(BlenderErrorCode.SCRIPT_ERROR, 'geometryCompareRevision requires an explicit saved revision, geometryEntityIds and geometryFrame.')
|
|
449
456
|
}
|
|
457
|
+
if (args.geometryAssemblyPairs !== undefined) validateAssemblyPairs(args.geometryAssemblyPairs, args.geometryEntityIds)
|
|
450
458
|
if (args.geometryEntityIds !== undefined) validateGeometrySelection({ entityIds: args.geometryEntityIds,
|
|
451
459
|
frame: args.geometryFrame, checkpointSha256: '0'.repeat(64), ...(args.geometrySurfaceSamples === undefined ? {} : { surfaceSamples: args.geometrySurfaceSamples }) })
|
|
452
460
|
const { data, canonicalWarnings } = await canonicalCall(resolved.studio.getScene(args.projectId, {
|
|
@@ -474,16 +482,20 @@ function sceneGet(ctx) {
|
|
|
474
482
|
checkpointSha256: data.geometryInspection.sourceCheckpointSha256, ...(hasSamples ? { surfaceSamples: args.geometrySurfaceSamples } : {}) })
|
|
475
483
|
}
|
|
476
484
|
verifyScene(data, args.revision)
|
|
485
|
+
if (args.geometryAssemblyPairs !== undefined) data.geometryAssemblyOrigins = readAssemblyOrigins(data.geometryInspection, args.geometryAssemblyPairs)
|
|
477
486
|
if (args.geometryCompareRevision !== undefined) {
|
|
478
487
|
const reference = await canonicalCall(resolved.studio.getScene(args.projectId, { revision: args.geometryCompareRevision,
|
|
479
|
-
full: false, geometry: { entityIds: args.geometryEntityIds, frame: args.geometryFrame, surfaceSamples: args.geometrySurfaceSamples }, signal: exec?.signal }), warning)
|
|
488
|
+
full: false, geometry: { entityIds: args.geometryEntityIds, frame: args.geometryFrame, ...(hasSamples ? { surfaceSamples: args.geometrySurfaceSamples } : {}) }, signal: exec?.signal }), warning)
|
|
480
489
|
verifyScene(reference.data, args.geometryCompareRevision)
|
|
481
490
|
canonicalWarnings.push(...reference.canonicalWarnings)
|
|
482
|
-
data.geometryComparison = compareSurfaceReports(reference.data.geometryInspection, data.geometryInspection)
|
|
491
|
+
data.geometryComparison = hasSamples ? compareSurfaceReports(reference.data.geometryInspection, data.geometryInspection) : compareGeometryReports(reference.data.geometryInspection, data.geometryInspection)
|
|
492
|
+
if (args.geometryAssemblyPairs !== undefined) data.geometryComparison.assemblyOriginComparison = compareAssemblyOrigins(
|
|
493
|
+
readAssemblyOrigins(reference.data.geometryInspection, args.geometryAssemblyPairs), data.geometryAssemblyOrigins)
|
|
483
494
|
}
|
|
484
495
|
if (args.geometryOnly === true) {
|
|
485
496
|
const receipt = { projectId: args.projectId, revision: data.revision, digest: data.digest,
|
|
486
497
|
isCurrent: data.isCurrent, geometryInspection: data.geometryInspection,
|
|
498
|
+
...(data.geometryAssemblyOrigins ? { geometryAssemblyOrigins: data.geometryAssemblyOrigins } : {}),
|
|
487
499
|
...(data.geometryComparison ? { geometryComparison: data.geometryComparison } : {}) }
|
|
488
500
|
return { ok: true, text: renderSuccess(`Saved native measurement receipt for "${args.projectId}" at ${data.revision}.`, receipt,
|
|
489
501
|
{ warnings: canonicalWarnings, compact: true }), data: receipt }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deepblend/dsh-blender-tool",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.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.
|
|
14
|
+
"@deepblend/dsh-blender-contracts": "0.12.0"
|
|
15
15
|
},
|
|
16
16
|
"license": "MIT",
|
|
17
17
|
"repository": {
|