abelien-harness 0.4.0 → 0.6.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/.env.example +4 -0
- package/README.md +11 -18
- package/package.json +2 -3
- package/skills/abelien-appearance/SKILL.md +1 -0
- package/skills/abelien-clarify/SKILL.md +12 -2
- package/skills/abelien-clarify/references/examples.md +22 -0
- package/skills/abelien-clarify/scripts/check_result.py +38 -3
- package/skills/abelien-clarify/scripts/test_check_result.py +32 -1
- package/skills/abelien-guide/SKILL.md +1 -1
- package/skills/abelien-hardware/SKILL.md +10 -5
- package/skills/abelien-hardware/domain.json +3 -3
- package/skills/abelien-hardware/references/search-index-format.md +13 -1
- package/skills/abelien-hardware/references/search-index-release.md +4 -2
- package/skills/abelien-hardware/references/web-evidence-contract.md +3 -3
- package/skills/abelien-hardware/scripts/dimensions.mjs +23 -8
- package/skills/abelien-hardware/scripts/electrical.mjs +168 -0
- package/skills/abelien-hardware/scripts/finalize-selection.mjs +4 -2
- package/skills/abelien-hardware/scripts/provider-chain-contract.mjs +27 -6
- package/skills/abelien-hardware/scripts/public-page.mjs +11 -121
- package/skills/abelien-hardware/scripts/response-redaction.mjs +1 -130
- package/skills/abelien-hardware/scripts/search-index-release.mjs +30 -17
- package/skills/abelien-hardware/scripts/selection-request.mjs +6 -5
- package/skills/abelien-hardware/scripts/tos-wiki-search.mjs +13 -8
- package/skills/abelien-hardware/scripts/volc-web-search.mjs +10 -27
- package/skills/abelien-hardware/scripts/web-common.mjs +8 -11
- package/skills/abelien-hardware/scripts/web-integrity.mjs +1 -1
- package/skills/abelien-hardware/scripts/web-provider-chain.mjs +10 -3
- package/skills/abelien-hardware/scripts/web-receipt.mjs +4 -2
- package/skills/abelien-hardware/scripts/web-search-plan.mjs +21 -2
- package/skills/abelien-hardware/scripts/web-search.mjs +5 -2
- package/skills/abelien-hardware-selection/SKILL.md +1 -1
- package/skills/abelien-hardware-selection/domain.json +2 -2
- package/skills/abelien-hardware-selection/references/selection-service-contract.md +30 -3
- package/skills/abelien-hardware-selection/scripts/select.mjs +35 -8
- package/skills/abelien-harness/scripts/ab.mjs +24 -2
- package/skills/abelien-harness/scripts/web/common.mjs +20 -0
- package/skills/abelien-harness/scripts/web/public-read.mjs +168 -0
- package/skills/abelien-harness/scripts/web/redact.mjs +136 -0
- package/skills/abelien-harness/scripts/web/volc.mjs +49 -0
- package/skills/abelien-reference/SKILL.md +51 -0
- package/skills/abelien-reference/domain.json +92 -0
- package/skills/abelien-reference/fixtures/create.final.json +102 -0
- package/skills/abelien-reference/fixtures/restore.collected.json +189 -0
- package/skills/abelien-reference/fixtures/restore.final.json +267 -0
- package/skills/abelien-reference/references/research-contract.md +74 -0
- package/skills/abelien-reference/schemas/reference-evidence.v1.json +140 -0
- package/skills/abelien-reference/scripts/evidence.mjs +60 -0
- package/skills/abelien-reference/scripts/finalize.mjs +116 -0
- package/skills/abelien-reference/scripts/measure.mjs +62 -0
- package/skills/abelien-reference/scripts/png.mjs +86 -0
- package/skills/abelien-reference/scripts/research.mjs +179 -0
- package/skills/abelien-structure/SKILL.md +2 -2
- package/skills/abelien-structure/domain.json +64 -0
- package/skills/abelien-structure/references/build123d-execution.md +1 -1
- package/skills/abelien-structure/references/handoff.md +1 -1
- package/skills/abelien-structure/worker/abelien_cad.py +585 -0
- package/skills/abelien-structure/worker/assembly-validation.mjs +106 -0
- package/skills/abelien-structure/worker/cad.py +1623 -0
- package/skills/abelien-structure/worker/cli-adapter.mjs +207 -0
- package/skills/abelien-structure/worker/component-fit-glb.mjs +173 -0
- package/skills/abelien-structure/worker/component-fit.d.mts +82 -0
- package/skills/abelien-structure/worker/component-fit.mjs +167 -0
- package/skills/abelien-structure/worker/component-target-image.d.mts +23 -0
- package/skills/abelien-structure/worker/component-target-image.mjs +107 -0
- package/skills/abelien-structure/worker/component-target-render.py +58 -0
- package/skills/abelien-structure/worker/config.mjs +6 -0
- package/skills/abelien-structure/worker/engineering-fit.d.mts +48 -0
- package/skills/abelien-structure/worker/engineering-fit.mjs +118 -0
- package/skills/abelien-structure/worker/engineering-policy.json +16 -0
- package/skills/abelien-structure/worker/engineering-repair.py +147 -0
- package/skills/abelien-structure/worker/engineering.py +324 -0
- package/skills/abelien-structure/worker/identity.mjs +98 -0
- package/skills/abelien-structure/worker/inspection-geometry.mjs +71 -0
- package/skills/abelien-structure/worker/jobs.mjs +451 -0
- package/skills/abelien-structure/worker/library-runtime.mjs +15 -0
- package/skills/abelien-structure/worker/main.mjs +102 -0
- package/skills/abelien-structure/worker/manifest-digest.mjs +11 -0
- package/skills/abelien-structure/worker/materials.mjs +52 -0
- package/skills/abelien-structure/worker/native-launcher.py +13 -0
- package/skills/abelien-structure/worker/native.mjs +157 -0
- package/skills/abelien-structure/worker/representation.mjs +47 -0
- package/skills/abelien-structure/worker/sandbox.mjs +41 -0
- package/skills/abelien-structure/worker/source-contract.mjs +143 -0
- package/skills/abelien-structure/worker/source_call_stats.py +59 -0
- package/skills/abelien-structure/worker/supervisor.mjs +38 -0
- package/skills/abelien-workspace/SKILL.md +11 -0
- package/skills/abelien-workspace/assets/journey-stage-plan.json +155 -0
- package/skills/abelien-workspace/domain.json +25 -2
- package/skills/abelien-workspace/references/hardware-journey-orchestration.md +1 -1
- package/skills/abelien-workspace/references/harness-binding.md +2 -2
- package/skills/abelien-workspace/references/user-journey.md +10 -0
- package/skills/abelien-workspace/scripts/journey-next.mjs +38 -0
- package/skills/abelien-workspace/workflows/definitions/0852e4949f5a6246880c1a26bf5ccaa42f8cda06b960e7c7ce234a0a176878f9.json +229 -0
- package/skills/abelien-workspace/workflows/definitions/399ef372790b7330d0ef88ce5096e7f69943d4793de97b2ed6c25165a332612f.json +230 -0
- package/skills/abelien-workspace/workflows/definitions/591a7f21e0b0ad586e340163f7533ac910356091ba8d2a26e250c0acbc80cd8b.json +176 -0
- package/skills/abelien-workspace/workflows/definitions/7c69ea0d51099b1ae947f08b056ea67f5cad6b40ae8bcb00efb496fa5b9d5ed3.json +149 -0
- package/skills/abelien-workspace/workflows/definitions/9b5c65ad03fdecc0148931a7677cfa684a3fb04ab2738d6f7a1086a2c79ac86c.json +149 -0
- package/skills/abelien-workspace/workflows/definitions/c1b5c6c653f0c4761d51efee923aafbbeea05512e7f35c60f7f7d36ec49fc1d1.json +229 -0
- package/skills/abelien-workspace/workflows/definitions/d9f8c6cdf0b2c41cdeb1c72ac5d1e37d95a7caac3272d434a3c7e8483fc565de.json +150 -0
- package/skills/abelien-workspace/workflows/definitions/e391e14c14cb39408a0a9e226ed54ffab7a36badea305ae4a3eafbac8124fc02.json +175 -0
- package/skills/abelien-workspace/workflows/definitions/fd8ce7074e2044985778446e93153864940bb9465471c1c71c41328cefff70bf.json +175 -0
- package/skills/abelien-workspace/workflows/user-journey-hardware-algorithm.json +5 -4
- package/skills/abelien-workspace/workflows/user-journey-hardware-candidates.json +5 -4
- package/skills/abelien-workspace/workflows/user-journey-hardware.json +5 -4
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/** Host-only CLI adapter for `ab capability run structure.*`. Files stay under the host IO root; Worker credentials come only from host env. */
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import {createHash} from 'node:crypto';
|
|
5
|
+
import {ENGINEERING_VERSION,ENGINEERING_CHECKS,ENGINEERING_NOT_CHECKED,ENGINEERING_ARTIFACT_TYPES,ENGINEERING_MAX_BODY_BYTES,ENGINEERING_POLICY,
|
|
6
|
+
normalizeEngineeringFitRequest,validateAttachmentBaseline,validateEngineeringArtifacts,validateEngineeringStoredArtifacts} from './engineering-fit.mjs';
|
|
7
|
+
import {fitAppearanceComponent,verifyAppearanceComponentFit,deriveTargetLocalPlacement} from './component-fit.mjs';
|
|
8
|
+
import {MAX_GLB_BYTES} from './component-fit-glb.mjs';
|
|
9
|
+
|
|
10
|
+
const MAX_STEP_BYTES=24*1024*1024,MAX_JSON_BYTES=128*1024,MAX_PNG_BYTES=4*1024*1024,ERROR_BODY_BYTES=4096,TARGET_ERROR_BODY_BYTES=128*1024;
|
|
11
|
+
const hex=/^[a-f0-9]{64}$/,reasonPattern=/^[a-z][a-z0-9_]{0,63}$/;
|
|
12
|
+
const sha256=bytes=>createHash('sha256').update(bytes).digest('hex');
|
|
13
|
+
const fail=(code,message,details)=>Object.assign(new Error(message),{code},details===undefined?{}:{details});
|
|
14
|
+
const object=(value,name)=>{if(!value||typeof value!=='object'||Array.isArray(value))throw fail('STRUCTURE_INPUT_INVALID',`${name} must be an object`);return value;};
|
|
15
|
+
const reason=value=>typeof value==='string'&&reasonPattern.test(value)?value:'invalid';
|
|
16
|
+
|
|
17
|
+
/** Worker validators throw plain Errors; keep their exact code/message so the host maps them as before. */
|
|
18
|
+
async function guarded(run){
|
|
19
|
+
try{return await run();}
|
|
20
|
+
catch(error){
|
|
21
|
+
if(/^STRUCTURE_[A-Z_]+$/.test(error?.code??''))throw error;
|
|
22
|
+
throw fail('STRUCTURE_REJECTED','structure input rejected',{code:reason(error?.code??error?.message),message:reason(error?.message)});
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function ioRoot(env){
|
|
27
|
+
const root=env.ABELIEN_STRUCTURE_IO_ROOT;
|
|
28
|
+
if(typeof root!=='string'||!path.isAbsolute(root))throw fail('STRUCTURE_HOST_ENV_MISSING','ABELIEN_STRUCTURE_IO_ROOT must be an absolute directory set by the host');
|
|
29
|
+
let real;try{real=fs.realpathSync(root);}catch{real=null;}
|
|
30
|
+
if(!real||!fs.statSync(real).isDirectory())throw fail('STRUCTURE_HOST_ENV_MISSING','ABELIEN_STRUCTURE_IO_ROOT must be an existing directory');
|
|
31
|
+
return real;
|
|
32
|
+
}
|
|
33
|
+
const within=(root,real)=>real.startsWith(root+path.sep);
|
|
34
|
+
|
|
35
|
+
function readInput(root,file,maximum,name){
|
|
36
|
+
if(typeof file!=='string'||!path.isAbsolute(file))throw fail('STRUCTURE_PATH_INVALID',`${name} must be an absolute path`);
|
|
37
|
+
const stat=fs.lstatSync(file,{throwIfNoEntry:false});
|
|
38
|
+
if(!stat?.isFile())throw fail('STRUCTURE_PATH_INVALID',`${name} must be a regular file, not a link`,{name});
|
|
39
|
+
const real=fs.realpathSync(file);
|
|
40
|
+
if(!within(root,real))throw fail('STRUCTURE_PATH_INVALID',`${name} is outside the host IO root`,{name});
|
|
41
|
+
if(stat.size>maximum)throw fail('STRUCTURE_PATH_INVALID',`${name} exceeds ${maximum} bytes`,{name,maximum});
|
|
42
|
+
return fs.readFileSync(real);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function writeOutput(root,directory,name,bytes){
|
|
46
|
+
if(typeof directory!=='string'||!path.isAbsolute(directory))throw fail('STRUCTURE_PATH_INVALID','outDir must be an absolute path');
|
|
47
|
+
const stat=fs.lstatSync(directory,{throwIfNoEntry:false});
|
|
48
|
+
if(!stat?.isDirectory())throw fail('STRUCTURE_PATH_INVALID','outDir must be an existing directory, not a link');
|
|
49
|
+
const real=fs.realpathSync(directory);
|
|
50
|
+
if(real!==root&&!within(root,real))throw fail('STRUCTURE_PATH_INVALID','outDir is outside the host IO root');
|
|
51
|
+
const file=path.join(real,name);
|
|
52
|
+
try{fs.writeFileSync(file,bytes,{flag:'wx',mode:0o600});}
|
|
53
|
+
catch{throw fail('STRUCTURE_PATH_INVALID','output file already exists or cannot be written',{name});}
|
|
54
|
+
return {path:file,sha256:sha256(bytes),bytes:bytes.length};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const readFiles=(root,files)=>new Map(Object.entries(object(files,'files')).map(([name,file])=>{
|
|
58
|
+
if(!Object.hasOwn(ENGINEERING_ARTIFACT_TYPES,name))throw fail('STRUCTURE_INPUT_INVALID','files contains an unknown artifact name',{name:reason(name)});
|
|
59
|
+
return [name,readInput(root,file,name.endsWith('.png')?MAX_PNG_BYTES:name.endsWith('.json')?MAX_JSON_BYTES:MAX_STEP_BYTES,`files.${name}`)];
|
|
60
|
+
}));
|
|
61
|
+
|
|
62
|
+
export function engineeringContract(){
|
|
63
|
+
return {version:ENGINEERING_VERSION,checks:[...ENGINEERING_CHECKS],notChecked:[...ENGINEERING_NOT_CHECKED],
|
|
64
|
+
artifactTypes:{...ENGINEERING_ARTIFACT_TYPES},maxBodyBytes:ENGINEERING_MAX_BODY_BYTES,policy:structuredClone(ENGINEERING_POLICY)};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function engineeringRequestCheck({input}){
|
|
68
|
+
return guarded(()=>{const normalized=normalizeEngineeringFitRequest(object(input,'input').request);
|
|
69
|
+
return {request:normalized.input,triangleCount:normalized.triangles.length};});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function attachmentBaselineCheck({input,env}){
|
|
73
|
+
const root=ioRoot(env),value=object(input,'input');
|
|
74
|
+
return guarded(()=>({attachment:validateAttachmentBaseline(value.attachment,value.assembly,readInput(root,value.stepPath,MAX_STEP_BYTES,'stepPath'))}));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function engineeringArtifactsCheck({input,env}){
|
|
78
|
+
const root=ioRoot(env),value=object(input,'input'),expected=object(value.expected,'expected');
|
|
79
|
+
if(typeof value.stored!=='boolean')throw fail('STRUCTURE_INPUT_INVALID','stored must be a boolean');
|
|
80
|
+
return guarded(()=>{const files=readFiles(root,value.files);
|
|
81
|
+
return value.stored?validateEngineeringStoredArtifacts(files,expected):validateEngineeringArtifacts(files,expected);});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function componentFit({input,env}){
|
|
85
|
+
const root=ioRoot(env),value=object(input,'input');
|
|
86
|
+
return guarded(()=>{
|
|
87
|
+
const fit=fitAppearanceComponent({request:value.request,trusted:value.trusted,
|
|
88
|
+
baselineGlb:readInput(root,value.baselineGlbPath,MAX_GLB_BYTES,'baselineGlbPath'),candidateGlb:readInput(root,value.candidateGlbPath,MAX_GLB_BYTES,'candidateGlbPath')});
|
|
89
|
+
return {report:fit.report,assemblyGlb:writeOutput(root,value.outDir,'assembly.glb',fit.assemblyGlb)};
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function componentFitVerify({input,env}){
|
|
94
|
+
const root=ioRoot(env),value=object(input,'input');
|
|
95
|
+
return guarded(()=>({verified:verifyAppearanceComponentFit({request:value.request,trusted:value.trusted,report:value.report,
|
|
96
|
+
baselineGlb:readInput(root,value.baselineGlbPath,MAX_GLB_BYTES,'baselineGlbPath'),candidateGlb:readInput(root,value.candidateGlbPath,MAX_GLB_BYTES,'candidateGlbPath'),
|
|
97
|
+
assemblyGlb:readInput(root,value.assemblyGlbPath,MAX_GLB_BYTES,'assemblyGlbPath')})}));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function targetPlacement({input,env}){
|
|
101
|
+
const root=ioRoot(env),value=object(input,'input');
|
|
102
|
+
return guarded(()=>({candidateToTarget:deriveTargetLocalPlacement({baseline:value.baseline,baselineGlb:readInput(root,value.baselineGlbPath,MAX_GLB_BYTES,'baselineGlbPath')})}));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function workerConfig(env,input){
|
|
106
|
+
const origin=env.AGENT_STRUCTURE_ORIGIN,token=env.AGENT_STRUCTURE_TOKEN;
|
|
107
|
+
let url=null;try{url=new URL(origin);}catch{/* reported below */}
|
|
108
|
+
const loopback=['127.0.0.1','localhost','[::1]'].includes(url?.hostname);
|
|
109
|
+
if(!url||url.origin!==origin||!(url.protocol==='https:'||(url.protocol==='http:'&&loopback)))
|
|
110
|
+
throw fail('STRUCTURE_HOST_ENV_MISSING','AGENT_STRUCTURE_ORIGIN must be an https origin (http only on loopback) set by the host');
|
|
111
|
+
if(typeof token!=='string'||token.length<32)throw fail('STRUCTURE_HOST_ENV_MISSING','AGENT_STRUCTURE_TOKEN must be set by the host');
|
|
112
|
+
const timeoutMs=input.timeoutMs??30000;
|
|
113
|
+
if(!Number.isSafeInteger(timeoutMs)||timeoutMs<1000||timeoutMs>300000)throw fail('STRUCTURE_INPUT_INVALID','timeoutMs must be an integer from 1000 to 300000');
|
|
114
|
+
return {origin,token,timeoutMs};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** One mapping from Worker HTTP outcomes to host reasons; noEffect is true only where the Worker provably did nothing. */
|
|
118
|
+
const workerFailure=(why,noEffect,diagnostic)=>fail('STRUCTURE_WORKER',why,{reason:why,noEffect,...(diagnostic===undefined?{}:{diagnostic})});
|
|
119
|
+
|
|
120
|
+
async function bounded(response,maximum){
|
|
121
|
+
if(!response.body||response.redirected)throw workerFailure('worker_unavailable',false);
|
|
122
|
+
const reader=response.body.getReader(),chunks=[];let total=0;
|
|
123
|
+
try{
|
|
124
|
+
for(;;){const {done,value}=await reader.read();if(done)break;
|
|
125
|
+
total+=value.byteLength;if(total>maximum)throw workerFailure('worker_unavailable',false);chunks.push(value);}
|
|
126
|
+
}finally{void reader.cancel().catch(()=>undefined);reader.releaseLock();}
|
|
127
|
+
return Buffer.concat(chunks,total);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function call(worker,fetchImpl,{route,scope='0'.repeat(64),body,errorBytes=ERROR_BODY_BYTES}){
|
|
131
|
+
let response;
|
|
132
|
+
try{
|
|
133
|
+
response=await fetchImpl(`${worker.origin}${route}`,{method:body===undefined?'GET':'POST',redirect:'error',cache:'no-store',
|
|
134
|
+
headers:{authorization:`Bearer ${worker.token}`,'x-structure-scope':scope,...(body===undefined?{}:{'content-type':'application/json'})},
|
|
135
|
+
...(body===undefined?{}:{body:JSON.stringify(body)}),signal:AbortSignal.timeout(worker.timeoutMs)});
|
|
136
|
+
}catch{throw workerFailure('worker_result_unknown',false);}
|
|
137
|
+
if(response.status===404)throw workerFailure('not_found',true);
|
|
138
|
+
if([400,429,503].includes(response.status)){
|
|
139
|
+
let why=response.status===429?'queue_full':response.status===503?'worker_unavailable':'invalid_input',diagnostic;
|
|
140
|
+
try{
|
|
141
|
+
const value=JSON.parse((await bounded(response,errorBytes)).toString('utf8'));
|
|
142
|
+
if(typeof value.error==='string'&&/^[a-z][a-z0-9_]{0,47}$/.test(value.error))why=value.error;
|
|
143
|
+
if(value.diagnostic!==undefined)diagnostic=value.diagnostic;
|
|
144
|
+
}catch{/* status remains authoritative */}
|
|
145
|
+
throw workerFailure(why,true,diagnostic);
|
|
146
|
+
}
|
|
147
|
+
if(!response.ok)throw workerFailure(response.status===409?'upstream_key_conflict':'worker_unavailable',false);
|
|
148
|
+
return response;
|
|
149
|
+
}
|
|
150
|
+
const json=async response=>JSON.parse((await bounded(response,262_144)).toString('utf8'));
|
|
151
|
+
const scopeOf=value=>{if(!hex.test(value??''))throw fail('STRUCTURE_INPUT_INVALID','scope must be 64 lowercase hex characters');return value;};
|
|
152
|
+
const jobIdOf=value=>{if(typeof value!=='string'||!/^[a-f0-9-]{1,64}$/.test(value))throw fail('STRUCTURE_INPUT_INVALID','jobId is invalid');return value;};
|
|
153
|
+
|
|
154
|
+
export async function workerHealth({input,env,fetchImpl}){
|
|
155
|
+
return json(await call(workerConfig(env,object(input,'input')),fetchImpl,{route:'/health'}));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export async function sourceContract({input,env,fetchImpl}){
|
|
159
|
+
return json(await call(workerConfig(env,object(input,'input')),fetchImpl,{route:'/source-contract'}));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export async function jobSubmit({input,env,fetchImpl}){
|
|
163
|
+
const value=object(input,'input'),worker=workerConfig(env,value);
|
|
164
|
+
return json(await call(worker,fetchImpl,{route:'/jobs',scope:scopeOf(value.scope),body:object(value.command,'command')}));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export async function jobLookup({input,env,fetchImpl}){
|
|
168
|
+
const value=object(input,'input'),worker=workerConfig(env,value);
|
|
169
|
+
if(!hex.test(value.upstreamKey??''))throw fail('STRUCTURE_INPUT_INVALID','upstreamKey must be 64 lowercase hex characters');
|
|
170
|
+
return json(await call(worker,fetchImpl,{route:`/jobs/by-upstream-key/${value.upstreamKey}`,scope:scopeOf(value.scope)}));
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export async function jobCancel({input,env,fetchImpl}){
|
|
174
|
+
const value=object(input,'input'),worker=workerConfig(env,value);
|
|
175
|
+
return json(await call(worker,fetchImpl,{route:`/jobs/${jobIdOf(value.jobId)}/cancel`,scope:scopeOf(value.scope),body:{}}));
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Header identity and body bytes must both match the caller's expected artifact row before anything is written. */
|
|
179
|
+
export async function artifactRead({input,env,fetchImpl}){
|
|
180
|
+
const value=object(input,'input'),worker=workerConfig(env,value),root=ioRoot(env),expected=object(value.expected,'expected');
|
|
181
|
+
if(typeof value.name!=='string'||!/^[a-z][a-z0-9-]{0,62}\.(step|glb|png|json|py)$/.test(value.name))throw fail('STRUCTURE_INPUT_INVALID','name is invalid');
|
|
182
|
+
if(typeof expected.mediaType!=='string'||!hex.test(expected.sha256??'')||(expected.bytes!==undefined&&!Number.isSafeInteger(expected.bytes)))
|
|
183
|
+
throw fail('STRUCTURE_INPUT_INVALID','expected needs mediaType, sha256 and optional integer bytes');
|
|
184
|
+
const response=await call(worker,fetchImpl,{route:`/jobs/${jobIdOf(value.jobId)}/artifacts/${value.name}`,scope:scopeOf(value.scope)});
|
|
185
|
+
if(response.headers.get('content-type')?.split(';')[0]!==expected.mediaType||response.headers.get('x-content-sha256')!==expected.sha256)
|
|
186
|
+
throw workerFailure('artifact_changed',false);
|
|
187
|
+
const bytes=await bounded(response,value.name.endsWith('.png')?MAX_PNG_BYTES:value.name.endsWith('.json')?MAX_JSON_BYTES:MAX_STEP_BYTES);
|
|
188
|
+
if(sha256(bytes)!==expected.sha256||(expected.bytes!==undefined&&bytes.length!==expected.bytes))throw workerFailure('artifact_changed',false);
|
|
189
|
+
return {...writeOutput(root,value.outDir,value.name,bytes),mediaType:expected.mediaType};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Rendering runs inside the Worker's runtime (v3 /target-image); the host only moves bytes and keeps the private diagnostic. */
|
|
193
|
+
export async function targetImage({input,env,fetchImpl}){
|
|
194
|
+
const value=object(input,'input'),worker=workerConfig(env,value),root=ioRoot(env);
|
|
195
|
+
const baselineGlb=readInput(root,value.baselineGlbPath,MAX_GLB_BYTES,'baselineGlbPath');
|
|
196
|
+
const response=await call(worker,fetchImpl,{route:'/target-image',errorBytes:TARGET_ERROR_BODY_BYTES,
|
|
197
|
+
body:{baseline:object(value.baseline,'baseline'),baselineGlb:{sha256:sha256(baselineGlb),base64:baselineGlb.toString('base64')}}});
|
|
198
|
+
const result=JSON.parse((await bounded(response,Math.ceil((MAX_GLB_BYTES+MAX_PNG_BYTES)/3)*4+TARGET_ERROR_BODY_BYTES*2)).toString('utf8'));
|
|
199
|
+
const decode=(field,maximum)=>{
|
|
200
|
+
const item=result?.[field],bytes=Buffer.from(typeof item?.base64==='string'?item.base64:'','base64');
|
|
201
|
+
if(!bytes.length||bytes.length>maximum||sha256(bytes)!==item.sha256)throw workerFailure('worker_unavailable',false);
|
|
202
|
+
return bytes;
|
|
203
|
+
};
|
|
204
|
+
const png=decode('png',MAX_PNG_BYTES),targetGlb=decode('targetGlb',MAX_GLB_BYTES);
|
|
205
|
+
return {provenance:result.provenance,diagnostic:result.diagnostic??null,
|
|
206
|
+
png:writeOutput(root,value.outDir,'target.png',png),targetGlb:writeOutput(root,value.outDir,'target.glb',targetGlb)};
|
|
207
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/** Bounded static glTF 2.0 decoding and lossless resource relocation for component previews. */
|
|
2
|
+
import {createHash} from 'node:crypto';
|
|
3
|
+
|
|
4
|
+
export const MAX_GLB_BYTES=64*1024*1024;
|
|
5
|
+
const MAX_NODES=4096, MAX_ELEMENTS=2_000_000;
|
|
6
|
+
export const identity=[1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1];
|
|
7
|
+
export const sha256=bytes=>createHash('sha256').update(bytes).digest('hex');
|
|
8
|
+
export const fail=code=>{throw Object.assign(new Error(code),{code});};
|
|
9
|
+
const integer=(value,max)=>Number.isSafeInteger(value)&&value>=0&&value<max;
|
|
10
|
+
const finiteArray=(value,n)=>Array.isArray(value)&&value.length===n&&value.every(Number.isFinite);
|
|
11
|
+
const near=(a,b)=>Math.abs(a-b)<1e-8;
|
|
12
|
+
const components={SCALAR:1,VEC2:2,VEC3:3,VEC4:4,MAT2:4,MAT3:9,MAT4:16};
|
|
13
|
+
const componentBytes={5120:1,5121:1,5122:2,5123:2,5125:4,5126:4};
|
|
14
|
+
|
|
15
|
+
export function rigid(matrix){
|
|
16
|
+
if(!finiteArray(matrix,16)||![3,7,11].every(index=>near(matrix[index],0))||!near(matrix[15],1))return false;
|
|
17
|
+
for(let a=0;a<3;a++)for(let b=a;b<3;b++){
|
|
18
|
+
const dot=[0,1,2].reduce((sum,row)=>sum+matrix[a*4+row]*matrix[b*4+row],0);
|
|
19
|
+
if(!near(dot,a===b?1:0))return false;
|
|
20
|
+
}
|
|
21
|
+
const determinant=matrix[0]*(matrix[5]*matrix[10]-matrix[9]*matrix[6])-matrix[4]*(matrix[1]*matrix[10]-matrix[9]*matrix[2])+matrix[8]*(matrix[1]*matrix[6]-matrix[5]*matrix[2]);
|
|
22
|
+
return near(determinant,1);
|
|
23
|
+
}
|
|
24
|
+
export function multiply(a,b){
|
|
25
|
+
const output=Array(16).fill(0);
|
|
26
|
+
for(let col=0;col<4;col++)for(let row=0;row<4;row++)for(let k=0;k<4;k++)output[col*4+row]+=a[k*4+row]*b[col*4+k];
|
|
27
|
+
return output;
|
|
28
|
+
}
|
|
29
|
+
export const point=(matrix,p)=>[0,1,2].map(row=>matrix[row]*p[0]+matrix[4+row]*p[1]+matrix[8+row]*p[2]+matrix[12+row]);
|
|
30
|
+
export function nodeMatrix(node,{allowUniformScale=false}={}){
|
|
31
|
+
if(node.matrix!==undefined){
|
|
32
|
+
const matrix=node.matrix;
|
|
33
|
+
let valid=rigid(matrix);
|
|
34
|
+
if(!valid&&allowUniformScale&&finiteArray(matrix,16)){
|
|
35
|
+
const scale=Math.hypot(...matrix.slice(0,3));
|
|
36
|
+
if(scale>0&&Number.isFinite(scale)){const normalized=[...matrix];for(const i of [0,1,2,4,5,6,8,9,10])normalized[i]/=scale;valid=rigid(normalized);}
|
|
37
|
+
}
|
|
38
|
+
if(node.translation!==undefined||node.rotation!==undefined||node.scale!==undefined||!valid)fail('unsupported_node_transform');
|
|
39
|
+
return node.matrix;
|
|
40
|
+
}
|
|
41
|
+
const translation=node.translation??[0,0,0],rotation=node.rotation??[0,0,0,1],scale=node.scale??[1,1,1];
|
|
42
|
+
if(!finiteArray(translation,3)||!finiteArray(rotation,4)||!finiteArray(scale,3)||!scale.every(value=>near(value,1))||!near(rotation.reduce((sum,v)=>sum+v*v,0),1))fail('unsupported_node_transform');
|
|
43
|
+
const [x,y,z,w]=rotation;
|
|
44
|
+
return [1-2*(y*y+z*z),2*(x*y+z*w),2*(x*z-y*w),0,
|
|
45
|
+
2*(x*y-z*w),1-2*(x*x+z*z),2*(y*z+x*w),0,
|
|
46
|
+
2*(x*z+y*w),2*(y*z-x*w),1-2*(x*x+y*y),0,...translation,1];
|
|
47
|
+
}
|
|
48
|
+
function rejectUnsupported(value,depth=0){
|
|
49
|
+
if(depth>128)fail('unsupported_gltf_depth');
|
|
50
|
+
if(!value||typeof value!=='object')return;
|
|
51
|
+
if(value.extensions&&Object.keys(value.extensions).some(key=>!['KHR_materials_unlit','KHR_texture_transform'].includes(key)))fail('unsupported_gltf_extension');
|
|
52
|
+
for(const child of Object.values(value))rejectUnsupported(child,depth+1);
|
|
53
|
+
}
|
|
54
|
+
export function parseGlb(bytes,{allowedUniformScaleNodeIndices=[]}={}){
|
|
55
|
+
if(!(bytes instanceof Uint8Array)||bytes.byteLength<28||bytes.byteLength>MAX_GLB_BYTES)fail('invalid_glb');
|
|
56
|
+
const raw=Buffer.from(bytes);
|
|
57
|
+
if(raw.readUInt32LE(0)!==0x46546c67||raw.readUInt32LE(4)!==2||raw.readUInt32LE(8)!==raw.length)fail('invalid_glb');
|
|
58
|
+
const jsonLength=raw.readUInt32LE(12);
|
|
59
|
+
if(jsonLength>8*1024*1024||jsonLength%4||20+jsonLength+8>raw.length||raw.readUInt32LE(16)!==0x4e4f534a)fail('invalid_glb');
|
|
60
|
+
let doc;try{doc=JSON.parse(raw.subarray(20,20+jsonLength).toString('utf8'));}catch{fail('invalid_glb_json');}
|
|
61
|
+
const binHeader=20+jsonLength,binLength=raw.readUInt32LE(binHeader);
|
|
62
|
+
if(raw.readUInt32LE(binHeader+4)!==0x004e4942||binLength%4||binHeader+8+binLength!==raw.length)fail('invalid_glb');
|
|
63
|
+
const bin=raw.subarray(binHeader+8);
|
|
64
|
+
if(doc.asset?.version!=='2.0'||!Array.isArray(doc.buffers)||doc.buffers.length!==1||doc.buffers[0].uri!==undefined||!Number.isSafeInteger(doc.buffers[0].byteLength)||doc.buffers[0].byteLength<=0||doc.buffers[0].byteLength>bin.length||bin.length-doc.buffers[0].byteLength>3)fail('unsupported_gltf_buffer');
|
|
65
|
+
if(doc.animations?.length||doc.skins?.length||doc.cameras?.length)fail('unsupported_dynamic_gltf');
|
|
66
|
+
if((doc.extensionsRequired??[]).some(key=>!['KHR_materials_unlit','KHR_texture_transform'].includes(key)))fail('unsupported_gltf_extension');
|
|
67
|
+
rejectUnsupported(doc);
|
|
68
|
+
for(const field of ['nodes','meshes','accessors','bufferViews'])if(!Array.isArray(doc[field])||!doc[field].length||doc[field].length>MAX_NODES*8)fail('invalid_gltf_resources');
|
|
69
|
+
if(doc.nodes.length>MAX_NODES||!Array.isArray(doc.scenes)||doc.scenes.length!==1||(doc.scene??0)!==0||!Array.isArray(doc.scenes[0].nodes)||!doc.scenes[0].nodes.length)fail('unsupported_gltf_scene');
|
|
70
|
+
for(const view of doc.bufferViews){
|
|
71
|
+
if(view.buffer!==0||!Number.isSafeInteger(view.byteOffset??0)||(view.byteOffset??0)<0||!Number.isSafeInteger(view.byteLength)||view.byteLength<=0||(view.byteOffset??0)+view.byteLength>doc.buffers[0].byteLength||
|
|
72
|
+
(view.byteStride!==undefined&&(!Number.isSafeInteger(view.byteStride)||view.byteStride<4||view.byteStride>252||view.byteStride%4)))fail('invalid_gltf_buffer_view');
|
|
73
|
+
}
|
|
74
|
+
for(const accessor of doc.accessors){
|
|
75
|
+
const count=components[accessor.type],size=componentBytes[accessor.componentType];
|
|
76
|
+
if(!integer(accessor.bufferView,doc.bufferViews.length)||accessor.sparse||!count||!size||!Number.isSafeInteger(accessor.count)||accessor.count<=0||accessor.count>MAX_ELEMENTS)fail('unsupported_gltf_accessor');
|
|
77
|
+
const view=doc.bufferViews[accessor.bufferView],stride=view.byteStride??count*size,offset=accessor.byteOffset??0;
|
|
78
|
+
if(!Number.isSafeInteger(offset)||offset<0||stride<count*size||offset%size||((view.byteOffset??0)+offset)%size||offset+(accessor.count-1)*stride+count*size>view.byteLength)fail('invalid_gltf_accessor_bounds');
|
|
79
|
+
}
|
|
80
|
+
for(const mesh of doc.meshes){
|
|
81
|
+
if(mesh.weights||!Array.isArray(mesh.primitives)||!mesh.primitives.length||mesh.primitives.length>MAX_NODES)fail('unsupported_gltf_mesh');
|
|
82
|
+
for(const primitive of mesh.primitives){
|
|
83
|
+
if((primitive.mode??4)!==4||primitive.targets||!primitive.attributes||!integer(primitive.attributes.POSITION,doc.accessors.length)||Object.values(primitive.attributes).some(index=>!integer(index,doc.accessors.length))||
|
|
84
|
+
(primitive.indices!==undefined&&!integer(primitive.indices,doc.accessors.length))||(primitive.material!==undefined&&!integer(primitive.material,doc.materials?.length??0)))fail('unsupported_gltf_primitive');
|
|
85
|
+
const pos=doc.accessors[primitive.attributes.POSITION];
|
|
86
|
+
if(pos.componentType!==5126||pos.type!=='VEC3'||pos.normalized)fail('unsupported_gltf_positions');
|
|
87
|
+
if(Object.values(primitive.attributes).some(index=>doc.accessors[index].count!==pos.count))fail('invalid_gltf_attribute_count');
|
|
88
|
+
if(primitive.indices!==undefined){const index=doc.accessors[primitive.indices];if(index.type!=='SCALAR'||![5121,5123,5125].includes(index.componentType)||index.normalized)fail('unsupported_gltf_indices');}
|
|
89
|
+
if((primitive.indices===undefined?pos.count:doc.accessors[primitive.indices].count)%3)fail('invalid_gltf_triangles');
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
for(const image of doc.images??[])if(image.uri!==undefined||!integer(image.bufferView,doc.bufferViews.length)||!['image/png','image/jpeg'].includes(image.mimeType))fail('unsupported_gltf_image');
|
|
93
|
+
for(const texture of doc.textures??[])if(!integer(texture.source,doc.images?.length??0)||(texture.sampler!==undefined&&!integer(texture.sampler,doc.samplers?.length??0)))fail('invalid_gltf_texture');
|
|
94
|
+
for(const material of doc.materials??[]){
|
|
95
|
+
for(const texture of [material.pbrMetallicRoughness?.baseColorTexture,material.pbrMetallicRoughness?.metallicRoughnessTexture,material.normalTexture,material.occlusionTexture,material.emissiveTexture])if(texture&&!integer(texture.index,doc.textures?.length??0))fail('invalid_gltf_material');
|
|
96
|
+
}
|
|
97
|
+
const parents=new Map(),reachable=new Set(),world=new Map();
|
|
98
|
+
for(let index=0;index<doc.nodes.length;index++){
|
|
99
|
+
const node=doc.nodes[index];nodeMatrix(node,{allowUniformScale:allowedUniformScaleNodeIndices.includes(index)});
|
|
100
|
+
if(node.skin!==undefined||node.camera!==undefined||node.weights||(node.mesh!==undefined&&!integer(node.mesh,doc.meshes.length))||(node.children!==undefined&&!Array.isArray(node.children)))fail('unsupported_gltf_node');
|
|
101
|
+
for(const child of node.children??[]){if(!integer(child,doc.nodes.length)||parents.has(child))fail('invalid_gltf_hierarchy');parents.set(child,index);}
|
|
102
|
+
}
|
|
103
|
+
const visit=(index,matrix,depth)=>{
|
|
104
|
+
if(!integer(index,doc.nodes.length)||reachable.has(index)||depth>128)fail('invalid_gltf_hierarchy');
|
|
105
|
+
reachable.add(index);const transform=multiply(matrix,nodeMatrix(doc.nodes[index],{allowUniformScale:allowedUniformScaleNodeIndices.includes(index)}));world.set(index,transform);
|
|
106
|
+
for(const child of doc.nodes[index].children??[])visit(child,transform,depth+1);
|
|
107
|
+
};
|
|
108
|
+
for(const root of doc.scenes[0].nodes){if(parents.has(root))fail('invalid_gltf_hierarchy');visit(root,identity,0);}
|
|
109
|
+
if(reachable.size!==doc.nodes.length)fail('unsupported_unreachable_nodes');
|
|
110
|
+
return {doc,bin,parents,world};
|
|
111
|
+
}
|
|
112
|
+
export function encodeGlb(doc,bin){
|
|
113
|
+
const jsonRaw=Buffer.from(JSON.stringify(doc)),json=Buffer.alloc(Math.ceil(jsonRaw.length/4)*4,0x20);jsonRaw.copy(json);
|
|
114
|
+
const padded=Buffer.alloc(Math.ceil(bin.length/4)*4);bin.copy(padded);
|
|
115
|
+
const result=Buffer.alloc(28+json.length+padded.length);
|
|
116
|
+
result.writeUInt32LE(0x46546c67,0);result.writeUInt32LE(2,4);result.writeUInt32LE(result.length,8);
|
|
117
|
+
result.writeUInt32LE(json.length,12);result.writeUInt32LE(0x4e4f534a,16);json.copy(result,20);
|
|
118
|
+
result.writeUInt32LE(padded.length,20+json.length);result.writeUInt32LE(0x004e4942,24+json.length);padded.copy(result,28+json.length);
|
|
119
|
+
if(result.length>MAX_GLB_BYTES)fail('assembly_glb_too_large');
|
|
120
|
+
return result;
|
|
121
|
+
}
|
|
122
|
+
function readAccessor(parsed,index){
|
|
123
|
+
const accessor=parsed.doc.accessors[index],view=parsed.doc.bufferViews[accessor.bufferView],n=components[accessor.type],size=componentBytes[accessor.componentType];
|
|
124
|
+
const offset=(view.byteOffset??0)+(accessor.byteOffset??0),stride=view.byteStride??n*size;
|
|
125
|
+
const read={5121:'readUInt8',5123:'readUInt16LE',5125:'readUInt32LE',5126:'readFloatLE'}[accessor.componentType];
|
|
126
|
+
if(!read)fail('unsupported_geometry_accessor');
|
|
127
|
+
return {count:accessor.count,get:i=>Array.from({length:n},(_,axis)=>parsed.bin[read](offset+i*stride+axis*size))};
|
|
128
|
+
}
|
|
129
|
+
export function* triangles(parsed,nodeIndex,matrix){
|
|
130
|
+
const node=parsed.doc.nodes[nodeIndex];if(node.mesh===undefined)return;
|
|
131
|
+
for(const primitive of parsed.doc.meshes[node.mesh].primitives){
|
|
132
|
+
const positions=readAccessor(parsed,primitive.attributes.POSITION),indices=primitive.indices===undefined?undefined:readAccessor(parsed,primitive.indices),count=indices?.count??positions.count;
|
|
133
|
+
for(let index=0;index<count;index+=3){
|
|
134
|
+
const triangle=[];
|
|
135
|
+
for(let vertex=0;vertex<3;vertex++){
|
|
136
|
+
const positionIndex=indices?indices.get(index+vertex)[0]:index+vertex;
|
|
137
|
+
if(!integer(positionIndex,positions.count))fail('invalid_gltf_index');
|
|
138
|
+
const xyz=positions.get(positionIndex);if(!xyz.every(Number.isFinite))fail('invalid_gltf_position');
|
|
139
|
+
const transformed=point(matrix,xyz);if(!transformed.every(Number.isFinite)||transformed.some(value=>Math.abs(value)>1e6))fail('invalid_geometry_range');triangle.push(transformed);
|
|
140
|
+
}
|
|
141
|
+
yield triangle;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
/** Appends resources; baseline buffer/accessor/mesh/material indices and bytes stay unchanged. */
|
|
146
|
+
export function appendCandidate(baseline,candidate,prefix){
|
|
147
|
+
const doc=structuredClone(baseline.doc),source=candidate.doc,counts=Object.fromEntries(['nodes','meshes','accessors','bufferViews','materials','textures','samplers','images'].map(key=>[key,doc[key]?.length??0]));
|
|
148
|
+
const bin=Buffer.concat([baseline.bin,candidate.bin]),byteOffset=baseline.bin.length;
|
|
149
|
+
doc.buffers=[{...doc.buffers[0],byteLength:bin.length}];
|
|
150
|
+
for(const key of Object.keys(counts)){
|
|
151
|
+
if(!source[key]?.length)continue;
|
|
152
|
+
const rows=structuredClone(source[key]);
|
|
153
|
+
for(let index=0;index<rows.length;index++){
|
|
154
|
+
const row=rows[index];
|
|
155
|
+
if(key==='nodes'){
|
|
156
|
+
row.name=`${prefix}-${index}`;
|
|
157
|
+
if(row.mesh!==undefined)row.mesh+=counts.meshes;
|
|
158
|
+
if(row.children)row.children=row.children.map(child=>child+counts.nodes);
|
|
159
|
+
}else if(key==='meshes')for(const primitive of row.primitives){
|
|
160
|
+
for(const attribute of Object.keys(primitive.attributes))primitive.attributes[attribute]+=counts.accessors;
|
|
161
|
+
if(primitive.indices!==undefined)primitive.indices+=counts.accessors;
|
|
162
|
+
if(primitive.material!==undefined)primitive.material+=counts.materials;
|
|
163
|
+
}else if(key==='accessors')row.bufferView+=counts.bufferViews;
|
|
164
|
+
else if(key==='bufferViews'){row.buffer=0;row.byteOffset=(row.byteOffset??0)+byteOffset;}
|
|
165
|
+
else if(key==='images')row.bufferView+=counts.bufferViews;
|
|
166
|
+
else if(key==='textures'){row.source+=counts.images;if(row.sampler!==undefined)row.sampler+=counts.samplers;}
|
|
167
|
+
else if(key==='materials')for(const texture of [row.pbrMetallicRoughness?.baseColorTexture,row.pbrMetallicRoughness?.metallicRoughnessTexture,row.normalTexture,row.occlusionTexture,row.emissiveTexture])if(texture)texture.index+=counts.textures;
|
|
168
|
+
}
|
|
169
|
+
doc[key]=[...(doc[key]??[]),...rows];
|
|
170
|
+
}
|
|
171
|
+
for(const key of ['extensionsUsed','extensionsRequired'])if(source[key]?.length)doc[key]=[...new Set([...(doc[key]??[]),...source[key]])];
|
|
172
|
+
return {doc,bin,roots:source.scenes[0].nodes.map(index=>index+counts.nodes)};
|
|
173
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/** Server-only immutable inputs. Never populate `trusted` from a model or browser request. */
|
|
2
|
+
export interface ComponentFitBinding {
|
|
3
|
+
draftId: string;
|
|
4
|
+
baselineId: string;
|
|
5
|
+
baselineRevision: number;
|
|
6
|
+
baselineGlbSha256: string;
|
|
7
|
+
targetOccurrenceId: string;
|
|
8
|
+
}
|
|
9
|
+
export interface ComponentFitRequest {
|
|
10
|
+
schema: 'abelien.appearance-component-fit-request.v1';
|
|
11
|
+
baseline: ComponentFitBinding;
|
|
12
|
+
candidateGlbSha256: string;
|
|
13
|
+
}
|
|
14
|
+
export interface ComponentFitBox {
|
|
15
|
+
id: string;
|
|
16
|
+
/** Axis aligned box in the target occurrence's local frame, in metres. */
|
|
17
|
+
min: [number, number, number];
|
|
18
|
+
max: [number, number, number];
|
|
19
|
+
}
|
|
20
|
+
export interface TrustedComponentFitBaseline {
|
|
21
|
+
schema: 'abelien.appearance-component-fit-baseline.v1';
|
|
22
|
+
baseline: ComponentFitBinding;
|
|
23
|
+
/** glTF positions use metres; unknown physical units must stay preview-only upstream. */
|
|
24
|
+
units: 'm';
|
|
25
|
+
/** Unknown by default. Unknown requires explicit visualNormalization; never infer from glTF convention. */
|
|
26
|
+
candidatePhysicalUnits?: 'verified_m' | 'unknown';
|
|
27
|
+
/** Display-only scale/centering of generated wrapper; original geometry is never resized. */
|
|
28
|
+
visualNormalization?: 'fit_target_bounds';
|
|
29
|
+
/** Column-major rigid transform. No automatic sizing or non-unit scale is allowed. */
|
|
30
|
+
candidateToTarget: number[];
|
|
31
|
+
/** Omitted: replace all visual meshes under target, protected checks unknown. */
|
|
32
|
+
constraints?: {
|
|
33
|
+
/** Unique mesh-bearing node names strictly beneath, or equal to, target; children are preserved. */
|
|
34
|
+
replaceNodeNames: string[];
|
|
35
|
+
/** Exact original mesh-bearing nodes retained under target, disjoint from replacements. */
|
|
36
|
+
protectedNodeNames: string[];
|
|
37
|
+
protectedRegions: ComponentFitBox[];
|
|
38
|
+
keepouts: ComponentFitBox[];
|
|
39
|
+
allowedEnvelope?: ComponentFitBox;
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
export interface ComponentFitCheck {
|
|
43
|
+
id: string;
|
|
44
|
+
status: 'pass' | 'conflict' | 'unknown';
|
|
45
|
+
reason: string;
|
|
46
|
+
}
|
|
47
|
+
export interface ComponentFitReport {
|
|
48
|
+
schema: 'abelien.appearance-component-fit-report.v1';
|
|
49
|
+
checkerVersion: 'mesh-assembly-preview.v1';
|
|
50
|
+
baseline: ComponentFitBinding;
|
|
51
|
+
candidateGlbSha256: string;
|
|
52
|
+
assemblyGlbSha256: string;
|
|
53
|
+
trustedInputSha256: string;
|
|
54
|
+
representation: 'mesh_assembly_preview';
|
|
55
|
+
fitStatus: 'conflict' | 'unknown';
|
|
56
|
+
previewEligible: boolean;
|
|
57
|
+
adoptionEligible: false;
|
|
58
|
+
checks: ComponentFitCheck[];
|
|
59
|
+
preservedNodeCount: number;
|
|
60
|
+
replacedNodeNames: string[];
|
|
61
|
+
candidateTriangleCount: number;
|
|
62
|
+
candidateBounds: {min: number[]; max: number[]};
|
|
63
|
+
visualNormalization?: {method: 'fit_target_bounds'; scale: number; translationM: number[]; sourceBounds: {min: number[]; max: number[]}; targetBounds: {min: number[]; max: number[]}};
|
|
64
|
+
}
|
|
65
|
+
export function fitAppearanceComponent(input: {
|
|
66
|
+
request: ComponentFitRequest;
|
|
67
|
+
trusted: TrustedComponentFitBaseline;
|
|
68
|
+
baselineGlb: Uint8Array;
|
|
69
|
+
candidateGlb: Uint8Array;
|
|
70
|
+
}): {assemblyGlb: Buffer; report: ComponentFitReport};
|
|
71
|
+
/** Recomputes geometry and compares the complete report and output bytes; supplied pass fields are never trusted. */
|
|
72
|
+
export function verifyAppearanceComponentFit(input: {
|
|
73
|
+
request: ComponentFitRequest;
|
|
74
|
+
trusted: TrustedComponentFitBaseline;
|
|
75
|
+
baselineGlb: Uint8Array;
|
|
76
|
+
candidateGlb: Uint8Array;
|
|
77
|
+
assemblyGlb: Uint8Array;
|
|
78
|
+
report: ComponentFitReport;
|
|
79
|
+
}): boolean;
|
|
80
|
+
|
|
81
|
+
/** Inverse target world rotation from trusted baseline; zero translation, centering belongs to normalization. */
|
|
82
|
+
export function deriveTargetLocalPlacement(input: {baseline: ComponentFitBinding; baselineGlb: Uint8Array}): number[];
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-only deterministic static-mesh component replacement.
|
|
3
|
+
* The caller resolves ownership, latest revision and immutable assets before calling.
|
|
4
|
+
* Geometric preview evidence never authorizes CAD adoption or manufacturing export.
|
|
5
|
+
*/
|
|
6
|
+
import {MAX_GLB_BYTES,appendCandidate,encodeGlb,fail,identity,multiply,nodeMatrix,parseGlb,rigid,sha256,triangles} from './component-fit-glb.mjs';
|
|
7
|
+
|
|
8
|
+
const HEX=/^[a-f0-9]{64}$/,MAX_TRIANGLES=500_000;
|
|
9
|
+
const isObject=value=>value&&typeof value==='object'&&!Array.isArray(value);
|
|
10
|
+
const keys=(value,required,optional=[])=>isObject(value)&&required.every(key=>Object.hasOwn(value,key))&&Object.keys(value).every(key=>required.includes(key)||optional.includes(key));
|
|
11
|
+
const text=value=>typeof value==='string'&&value.length>0&&value.length<=200&&!/[\u0000-\u001f\u007f]/.test(value);
|
|
12
|
+
const canonical=value=>JSON.stringify(canonicalValue(value));
|
|
13
|
+
function canonicalValue(value){
|
|
14
|
+
if(Array.isArray(value))return value.map(canonicalValue);
|
|
15
|
+
if(isObject(value))return Object.fromEntries(Object.keys(value).sort().map(key=>[key,canonicalValue(value[key])]));
|
|
16
|
+
return value;
|
|
17
|
+
}
|
|
18
|
+
function validBinding(value){
|
|
19
|
+
return keys(value,['draftId','baselineId','baselineRevision','baselineGlbSha256','targetOccurrenceId'])&&['draftId','baselineId','targetOccurrenceId'].every(key=>text(value[key]))&&Number.isSafeInteger(value.baselineRevision)&&value.baselineRevision>=0&&HEX.test(value.baselineGlbSha256);
|
|
20
|
+
}
|
|
21
|
+
function validBox(value){return keys(value,['id','min','max'])&&text(value.id)&&['min','max'].every(key=>Array.isArray(value[key])&&value[key].length===3&&value[key].every(Number.isFinite))&&value.min.every((n,i)=>n<value.max[i]);}
|
|
22
|
+
function validateInputs(request,trusted,baselineGlb,candidateGlb){
|
|
23
|
+
if(!keys(request,['schema','baseline','candidateGlbSha256'])||request.schema!=='abelien.appearance-component-fit-request.v1'||!validBinding(request.baseline)||!HEX.test(request.candidateGlbSha256))fail('invalid_fit_request');
|
|
24
|
+
if(!keys(trusted,['schema','baseline','units','candidateToTarget'],['constraints','visualNormalization','candidatePhysicalUnits'])||trusted.schema!=='abelien.appearance-component-fit-baseline.v1'||!validBinding(trusted.baseline)||trusted.units!=='m')fail('invalid_trusted_fit_baseline');
|
|
25
|
+
if(canonical(request.baseline)!==canonical(trusted.baseline))fail('stale_or_mismatched_fit_baseline');
|
|
26
|
+
if(!rigid(trusted.candidateToTarget))fail('non_rigid_fit_transform');
|
|
27
|
+
if(trusted.visualNormalization!==undefined&&trusted.visualNormalization!=='fit_target_bounds')fail('invalid_visual_normalization');
|
|
28
|
+
if(trusted.candidatePhysicalUnits!==undefined&&!['verified_m','unknown'].includes(trusted.candidatePhysicalUnits))fail('invalid_candidate_physical_units');
|
|
29
|
+
if(trusted.visualNormalization===undefined&&trusted.candidatePhysicalUnits!=='verified_m')fail('candidate_physical_units_unknown');
|
|
30
|
+
if(!(baselineGlb instanceof Uint8Array)||!(candidateGlb instanceof Uint8Array)||baselineGlb.byteLength>MAX_GLB_BYTES||candidateGlb.byteLength>MAX_GLB_BYTES||sha256(baselineGlb)!==trusted.baseline.baselineGlbSha256||sha256(candidateGlb)!==request.candidateGlbSha256)fail('fit_source_hash_mismatch');
|
|
31
|
+
if(trusted.constraints!==undefined){
|
|
32
|
+
const c=trusted.constraints;
|
|
33
|
+
if(!keys(c,['replaceNodeNames','protectedNodeNames','protectedRegions','keepouts'],['allowedEnvelope'])||!Array.isArray(c.replaceNodeNames)||!c.replaceNodeNames.length||c.replaceNodeNames.length>4096||!Array.isArray(c.protectedNodeNames)||c.protectedNodeNames.length>4096||
|
|
34
|
+
[...c.replaceNodeNames,...c.protectedNodeNames].some(name=>!text(name))||new Set([...c.replaceNodeNames,...c.protectedNodeNames]).size!==c.replaceNodeNames.length+c.protectedNodeNames.length||
|
|
35
|
+
!Array.isArray(c.protectedRegions)||!Array.isArray(c.keepouts)||c.protectedRegions.length+c.keepouts.length>128||[...c.protectedRegions,...c.keepouts].some(box=>!validBox(box))||
|
|
36
|
+
(c.allowedEnvelope!==undefined&&!validBox(c.allowedEnvelope)))fail('invalid_fit_constraints');
|
|
37
|
+
const regionIds=[...c.protectedRegions,...c.keepouts,...(c.allowedEnvelope?[c.allowedEnvelope]:[])].map(box=>box.id);
|
|
38
|
+
if(new Set(regionIds).size!==regionIds.length)fail('invalid_fit_constraints');
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function bounds(triangle){return {min:[0,1,2].map(axis=>Math.min(...triangle.map(p=>p[axis]))),max:[0,1,2].map(axis=>Math.max(...triangle.map(p=>p[axis])))};}
|
|
42
|
+
// Conservative exclusion: an overlapping triangle bounding box is a conflict even
|
|
43
|
+
// when the exact triangle would miss the box. This never grants a false clear.
|
|
44
|
+
const touches=(triangle,box)=>triangle.min.every((n,axis)=>n<=box.max[axis]&&triangle.max[axis]>=box.min[axis]);
|
|
45
|
+
const inside=(triangle,box)=>triangle.min.every((n,axis)=>n>=box.min[axis]&&triangle.max[axis]<=box.max[axis]);
|
|
46
|
+
const check=(id,status,reason)=>({id,status,reason});
|
|
47
|
+
const uniqueNode=(parsed,name)=>{
|
|
48
|
+
const matching=parsed.doc.nodes.flatMap((node,index)=>node.name===name?[index]:[]);
|
|
49
|
+
if(matching.length!==1)fail('missing_or_ambiguous_fit_target');
|
|
50
|
+
return matching[0];
|
|
51
|
+
};
|
|
52
|
+
function relativeTransforms(parsed,target){
|
|
53
|
+
const relative=new Map();
|
|
54
|
+
const visit=(index,matrix)=>{relative.set(index,matrix);for(const child of parsed.doc.nodes[index].children??[])visit(child,multiply(matrix,nodeMatrix(parsed.doc.nodes[child])));};
|
|
55
|
+
visit(target,identity);return relative;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Map a generated glTF world-oriented component into the existing target local basis.
|
|
59
|
+
* Translation remains zero: preview normalization owns centering. Only trusted
|
|
60
|
+
* baseline bytes/name determine this transform; never a model-provided mount frame.
|
|
61
|
+
*/
|
|
62
|
+
export function deriveTargetLocalPlacement({baseline:binding,baselineGlb}){
|
|
63
|
+
if(!validBinding(binding)||!(baselineGlb instanceof Uint8Array)||baselineGlb.byteLength>MAX_GLB_BYTES||sha256(baselineGlb)!==binding.baselineGlbSha256)fail('invalid_fit_placement_baseline');
|
|
64
|
+
const parsed=parseGlb(baselineGlb),target=uniqueNode(parsed,binding.targetOccurrenceId),world=parsed.world.get(target);
|
|
65
|
+
if(!rigid(world))fail('non_rigid_fit_target_basis');
|
|
66
|
+
return [world[0],world[4],world[8],0,world[1],world[5],world[9],0,world[2],world[6],world[10],0,0,0,0,1];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function fitAppearanceComponent({request,trusted,baselineGlb,candidateGlb}){
|
|
70
|
+
validateInputs(request,trusted,baselineGlb,candidateGlb);
|
|
71
|
+
const baseline=parseGlb(baselineGlb),candidate=parseGlb(candidateGlb),target=uniqueNode(baseline,trusted.baseline.targetOccurrenceId);
|
|
72
|
+
const relative=relativeTransforms(baseline,target),constraints=trusted.constraints;
|
|
73
|
+
const meshNodes=[...relative.keys()].filter(index=>baseline.doc.nodes[index].mesh!==undefined);
|
|
74
|
+
if(!meshNodes.length)fail('fit_target_has_no_geometry');
|
|
75
|
+
const resolveNames=names=>names.map(name=>{
|
|
76
|
+
const index=uniqueNode(baseline,name);if(!relative.has(index)||baseline.doc.nodes[index].mesh===undefined)fail('fit_constraint_outside_target');return index;
|
|
77
|
+
});
|
|
78
|
+
const replacements=constraints?resolveNames(constraints.replaceNodeNames):meshNodes;
|
|
79
|
+
const protectedNodes=constraints?resolveNames(constraints.protectedNodeNames):[];
|
|
80
|
+
const replaced=new Set(replacements),protectedSet=new Set(protectedNodes);
|
|
81
|
+
const checks=[check('baseline_identity','pass','server_binding_and_requested_revision_match'),check('source_integrity','pass','baseline_and_candidate_bytes_match_sha256'),check('rigid_placement',trusted.visualNormalization?'unknown':'pass',trusted.visualNormalization?'visual_normalization_not_physical_calibration':'verified_metre_frame_rigid_transform_without_resizing')];
|
|
82
|
+
// Validate all reachable baseline geometry, including unrelated components.
|
|
83
|
+
let baselineTriangles=0,removedProtectedSurface=false,unassignedProtectedSurface=false;
|
|
84
|
+
const witnessedRegions=new Set(),targetBounds={min:[Infinity,Infinity,Infinity],max:[-Infinity,-Infinity,-Infinity]};
|
|
85
|
+
for(let index=0;index<baseline.doc.nodes.length;index++){
|
|
86
|
+
const local=relative.get(index);
|
|
87
|
+
for(const triangle of triangles(baseline,index,local??baseline.world.get(index))){
|
|
88
|
+
if(++baselineTriangles>MAX_TRIANGLES)fail('fit_geometry_too_large');
|
|
89
|
+
if(replaced.has(index)){const box=bounds(triangle);for(let axis=0;axis<3;axis++){targetBounds.min[axis]=Math.min(targetBounds.min[axis],box.min[axis]);targetBounds.max[axis]=Math.max(targetBounds.max[axis],box.max[axis]);}}
|
|
90
|
+
if(local&&constraints)for(const region of constraints.protectedRegions)if(touches(bounds(triangle),region)){
|
|
91
|
+
if(replaced.has(index))removedProtectedSurface=true;
|
|
92
|
+
else if(protectedSet.has(index))witnessedRegions.add(region.id);
|
|
93
|
+
else unassignedProtectedSurface=true;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
let placement=[...trusted.candidateToTarget],visualNormalization;
|
|
98
|
+
if(trusted.visualNormalization){
|
|
99
|
+
const sourceBounds={min:[Infinity,Infinity,Infinity],max:[-Infinity,-Infinity,-Infinity]};let count=0;
|
|
100
|
+
for(let index=0;index<candidate.doc.nodes.length;index++)for(const triangle of triangles(candidate,index,multiply(placement,candidate.world.get(index)))){
|
|
101
|
+
if(++count>MAX_TRIANGLES)fail('fit_geometry_too_large');const box=bounds(triangle);
|
|
102
|
+
for(let axis=0;axis<3;axis++){sourceBounds.min[axis]=Math.min(sourceBounds.min[axis],box.min[axis]);sourceBounds.max[axis]=Math.max(sourceBounds.max[axis],box.max[axis]);}
|
|
103
|
+
}
|
|
104
|
+
if(!count)fail('fit_candidate_has_no_geometry');
|
|
105
|
+
const ratios=[0,1,2].filter(axis=>sourceBounds.max[axis]>sourceBounds.min[axis]).map(axis=>(targetBounds.max[axis]-targetBounds.min[axis])/(sourceBounds.max[axis]-sourceBounds.min[axis]));
|
|
106
|
+
const scale=Math.min(...ratios);if(!Number.isFinite(scale)||scale<=0||scale>1e9||scale<1e-9)fail('unsupported_visual_normalization');
|
|
107
|
+
const translationM=[0,1,2].map(axis=>(targetBounds.min[axis]+targetBounds.max[axis])/2-scale*(sourceBounds.min[axis]+sourceBounds.max[axis])/2);
|
|
108
|
+
const normalization=[scale,0,0,0,0,scale,0,0,0,0,scale,0,...translationM,1];placement=multiply(normalization,placement);
|
|
109
|
+
visualNormalization={method:'fit_target_bounds',scale,translationM,sourceBounds,targetBounds};
|
|
110
|
+
}
|
|
111
|
+
let candidateTriangleCount=0,protectedIntrusion=false,keepoutIntrusion=false,outsideEnvelope=false;
|
|
112
|
+
const candidateBounds={min:[Infinity,Infinity,Infinity],max:[-Infinity,-Infinity,-Infinity]};
|
|
113
|
+
for(let index=0;index<candidate.doc.nodes.length;index++){
|
|
114
|
+
const matrix=multiply(placement,candidate.world.get(index));
|
|
115
|
+
for(const triangle of triangles(candidate,index,matrix)){
|
|
116
|
+
if(++candidateTriangleCount>MAX_TRIANGLES)fail('fit_geometry_too_large');
|
|
117
|
+
const box=bounds(triangle);
|
|
118
|
+
for(let axis=0;axis<3;axis++){candidateBounds.min[axis]=Math.min(candidateBounds.min[axis],box.min[axis]);candidateBounds.max[axis]=Math.max(candidateBounds.max[axis],box.max[axis]);}
|
|
119
|
+
if(constraints){
|
|
120
|
+
if(constraints.protectedRegions.some(region=>touches(box,region)))protectedIntrusion=true;
|
|
121
|
+
if(constraints.keepouts.some(region=>touches(box,region)))keepoutIntrusion=true;
|
|
122
|
+
if(constraints.allowedEnvelope&&!inside(box,constraints.allowedEnvelope))outsideEnvelope=true;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
if(!candidateTriangleCount)fail('fit_candidate_has_no_geometry');
|
|
127
|
+
checks.push(check('protected_geometry',removedProtectedSurface||unassignedProtectedSurface?'conflict':protectedNodes.length?'pass':'unknown',removedProtectedSurface?'replacement_removes_protected_region_triangles':unassignedProtectedSurface?'protected_region_geometry_has_no_protected_node_binding':protectedNodes.length?'original_protected_mesh_nodes_and_resources_preserved_exactly':'no_trusted_protected_mesh_nodes'));
|
|
128
|
+
checks.push(check('protected_regions',protectedIntrusion?'conflict':!constraints?.protectedRegions.length||witnessedRegions.size!==constraints.protectedRegions.length?'unknown':'pass',protectedIntrusion?'generated_triangle_bounds_enter_protected_region':!constraints?.protectedRegions.length?'no_trusted_protected_regions':witnessedRegions.size!==constraints.protectedRegions.length?'region_has_no_original_protected_surface_witness':'generated_surface_bounds_excluded_from_witnessed_protected_regions'));
|
|
129
|
+
checks.push(check('keepouts',keepoutIntrusion?'conflict':constraints?.keepouts.length?'pass':'unknown',keepoutIntrusion?'generated_triangle_bounds_enter_keepout':constraints?.keepouts.length?'generated_surface_bounds_excluded_from_declared_keepouts':'no_trusted_keepouts'));
|
|
130
|
+
checks.push(check('allowed_envelope',outsideEnvelope?'conflict':constraints?.allowedEnvelope?'pass':'unknown',outsideEnvelope?'generated_surface_outside_allowed_envelope':constraints?.allowedEnvelope?'all_generated_triangles_inside_allowed_envelope':'no_trusted_allowed_envelope'));
|
|
131
|
+
const prefix=`appearance-${request.candidateGlbSha256.slice(0,16)}`;
|
|
132
|
+
if(baseline.doc.nodes.some(node=>typeof node.name==='string'&&node.name.startsWith(prefix)))fail('fit_candidate_name_collision');
|
|
133
|
+
const output=appendCandidate(baseline,candidate,prefix);
|
|
134
|
+
for(const index of replacements)delete output.doc.nodes[index].mesh;
|
|
135
|
+
const wrapper=output.doc.nodes.length;
|
|
136
|
+
output.doc.nodes.push({name:`${prefix}-placement`,matrix:placement,children:output.roots});
|
|
137
|
+
output.doc.nodes[target].children=[...(output.doc.nodes[target].children??[]),wrapper];
|
|
138
|
+
// Exactly the named mesh bindings and target children change. All ancestor
|
|
139
|
+
// transforms, unrelated nodes and original binary resource bytes remain exact.
|
|
140
|
+
let preservedNodeCount=0;
|
|
141
|
+
for(let index=0;index<baseline.doc.nodes.length;index++){
|
|
142
|
+
const expected=structuredClone(baseline.doc.nodes[index]);if(replaced.has(index))delete expected.mesh;
|
|
143
|
+
if(index===target)expected.children=[...(expected.children??[]),wrapper];
|
|
144
|
+
if(canonical(expected)!==canonical(output.doc.nodes[index]))fail('fit_preservation_failed');
|
|
145
|
+
if(index!==target&&!replaced.has(index))preservedNodeCount++;
|
|
146
|
+
}
|
|
147
|
+
if(!output.bin.subarray(0,baseline.bin.length).equals(baseline.bin))fail('fit_preservation_failed');
|
|
148
|
+
const assemblyGlb=encodeGlb(output.doc,output.bin);
|
|
149
|
+
parseGlb(assemblyGlb,{allowedUniformScaleNodeIndices:visualNormalization?[wrapper]:[]}); // Independently decode exported resource indices/hierarchy.
|
|
150
|
+
checks.push(check('unrelated_preservation','pass','original_nodes_ancestor_transforms_and_resource_bytes_preserved'));
|
|
151
|
+
checks.push(check('joint_continuity','unknown','no_mesh_boolean_stitch_or_manifold_joint_validation'));
|
|
152
|
+
checks.push(check('solid_collision','unknown','surface_box_exclusion_does_not_establish_solid_volume_clearance'));
|
|
153
|
+
checks.push(check('wall_thickness','unknown','no_trusted_solid_or_wall_thickness_validation'));
|
|
154
|
+
checks.push(check('engineering_fit','unknown','mesh_preview_cannot_certify_mechanical_interface_tolerance_or_manufacturing'));
|
|
155
|
+
const hasConflict=checks.some(item=>item.status==='conflict');
|
|
156
|
+
return {assemblyGlb,report:{schema:'abelien.appearance-component-fit-report.v1',checkerVersion:'mesh-assembly-preview.v1',baseline:structuredClone(trusted.baseline),candidateGlbSha256:request.candidateGlbSha256,
|
|
157
|
+
assemblyGlbSha256:sha256(assemblyGlb),trustedInputSha256:sha256(canonical(trusted)),representation:'mesh_assembly_preview',fitStatus:hasConflict?'conflict':'unknown',previewEligible:!hasConflict,adoptionEligible:false,checks,
|
|
158
|
+
...(visualNormalization?{visualNormalization}:{}),preservedNodeCount,replacedNodeNames:replacements.map(index=>baseline.doc.nodes[index].name??`node:${index}`),candidateTriangleCount,candidateBounds}};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function verifyAppearanceComponentFit(input){
|
|
162
|
+
try{
|
|
163
|
+
if(!(input.assemblyGlb instanceof Uint8Array))return false;
|
|
164
|
+
const result=fitAppearanceComponent(input);
|
|
165
|
+
return Buffer.from(input.assemblyGlb).equals(result.assemblyGlb)&&canonical(input.report)===canonical(result.report);
|
|
166
|
+
}catch{return false;}
|
|
167
|
+
}
|