@hatiolab/figure-model 0.1.30 → 0.1.31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. package/README.md +2 -0
  2. package/dist/index.d.ts +12 -0
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +11 -0
  5. package/dist/index.js.map +1 -1
  6. package/dist/release-contract.d.ts +2 -2
  7. package/dist/release-contract.d.ts.map +1 -1
  8. package/dist/release-contract.js +45 -3
  9. package/dist/release-contract.js.map +1 -1
  10. package/dist/types.d.ts +15 -4
  11. package/dist/types.d.ts.map +1 -1
  12. package/dist/types.js +15 -4
  13. package/dist/types.js.map +1 -1
  14. package/dist/v3-asset-types.d.ts +116 -0
  15. package/dist/v3-asset-types.d.ts.map +1 -0
  16. package/dist/v3-asset-types.js +2 -0
  17. package/dist/v3-asset-types.js.map +1 -0
  18. package/dist/v3-asset.d.ts +8 -0
  19. package/dist/v3-asset.d.ts.map +1 -0
  20. package/dist/v3-asset.js +135 -0
  21. package/dist/v3-asset.js.map +1 -0
  22. package/dist/v3-authoring-session.d.ts +119 -0
  23. package/dist/v3-authoring-session.d.ts.map +1 -0
  24. package/dist/v3-authoring-session.js +132 -0
  25. package/dist/v3-authoring-session.js.map +1 -0
  26. package/dist/v3-capabilities.d.ts +3 -0
  27. package/dist/v3-capabilities.d.ts.map +1 -0
  28. package/dist/v3-capabilities.js +221 -0
  29. package/dist/v3-capabilities.js.map +1 -0
  30. package/dist/v3-contract.d.ts +7 -0
  31. package/dist/v3-contract.d.ts.map +1 -0
  32. package/dist/v3-contract.js +12 -0
  33. package/dist/v3-contract.js.map +1 -0
  34. package/dist/v3-create.d.ts +25 -0
  35. package/dist/v3-create.d.ts.map +1 -0
  36. package/dist/v3-create.js +50 -0
  37. package/dist/v3-create.js.map +1 -0
  38. package/dist/v3-document.d.ts +5 -0
  39. package/dist/v3-document.d.ts.map +1 -0
  40. package/dist/v3-document.js +31 -0
  41. package/dist/v3-document.js.map +1 -0
  42. package/dist/v3-editor-workspace.d.ts +334 -0
  43. package/dist/v3-editor-workspace.d.ts.map +1 -0
  44. package/dist/v3-editor-workspace.js +130 -0
  45. package/dist/v3-editor-workspace.js.map +1 -0
  46. package/dist/v3-graph-types.d.ts +91 -0
  47. package/dist/v3-graph-types.d.ts.map +1 -0
  48. package/dist/v3-graph-types.js +2 -0
  49. package/dist/v3-graph-types.js.map +1 -0
  50. package/dist/v3-graph.d.ts +5 -0
  51. package/dist/v3-graph.d.ts.map +1 -0
  52. package/dist/v3-graph.js +631 -0
  53. package/dist/v3-graph.js.map +1 -0
  54. package/dist/v3-json.d.ts +13 -0
  55. package/dist/v3-json.d.ts.map +1 -0
  56. package/dist/v3-json.js +94 -0
  57. package/dist/v3-json.js.map +1 -0
  58. package/dist/v3-part-edit.d.ts +12 -0
  59. package/dist/v3-part-edit.d.ts.map +1 -0
  60. package/dist/v3-part-edit.js +53 -0
  61. package/dist/v3-part-edit.js.map +1 -0
  62. package/docs/assembly-constraints-v3.md +141 -0
  63. package/docs/prototypes/README.md +73 -0
  64. package/docs/prototypes/render-dryroom.py +60 -0
  65. package/docs/prototypes/render-full-dryroom.mjs +86 -0
  66. package/docs/prototypes/shapeassembly-probe-results.json +325 -0
  67. package/docs/prototypes/shapeassembly-probe.py +84 -0
  68. package/docs/prototypes/v3-asset-samples.mjs +15 -0
  69. package/docs/prototypes/v3-asset-store.mjs +52 -0
  70. package/docs/prototypes/v3-asset-store.test.mjs +59 -0
  71. package/docs/prototypes/v3-asset.mjs +1 -0
  72. package/docs/prototypes/v3-asset.schema.json +44 -0
  73. package/docs/prototypes/v3-asset.test.mjs +68 -0
  74. package/docs/prototypes/v3-authoring-session.mjs +1 -0
  75. package/docs/prototypes/v3-authoring-session.test.mjs +65 -0
  76. package/docs/prototypes/v3-capability-plan.mjs +1 -0
  77. package/docs/prototypes/v3-capability-plan.test.mjs +56 -0
  78. package/docs/prototypes/v3-capability-samples.mjs +23 -0
  79. package/docs/prototypes/v3-document.mjs +2 -0
  80. package/docs/prototypes/v3-document.schema.json +66 -0
  81. package/docs/prototypes/v3-document.test.mjs +55 -0
  82. package/docs/prototypes/v3-dryroom-mesh.mjs +60 -0
  83. package/docs/prototypes/v3-dryroom-mesh.test.mjs +30 -0
  84. package/docs/prototypes/v3-editor-browser.mjs +97 -0
  85. package/docs/prototypes/v3-editor-workspace.mjs +1 -0
  86. package/docs/prototypes/v3-editor-workspace.test.mjs +35 -0
  87. package/docs/prototypes/v3-feature-pattern.test.mjs +131 -0
  88. package/docs/prototypes/v3-full-conveyor.mjs +60 -0
  89. package/docs/prototypes/v3-full-conveyor.test.mjs +61 -0
  90. package/docs/prototypes/v3-full-dryroom.mjs +67 -0
  91. package/docs/prototypes/v3-full-dryroom.test.mjs +54 -0
  92. package/docs/prototypes/v3-full-forklift.mjs +40 -0
  93. package/docs/prototypes/v3-full-forklift.test.mjs +37 -0
  94. package/docs/prototypes/v3-full-robot.mjs +35 -0
  95. package/docs/prototypes/v3-full-robot.test.mjs +68 -0
  96. package/docs/prototypes/v3-kernel.mjs +147 -0
  97. package/docs/prototypes/v3-kernel.test.mjs +144 -0
  98. package/docs/prototypes/v3-layout.test.mjs +281 -0
  99. package/docs/prototypes/v3-linear-comparison.py +183 -0
  100. package/docs/prototypes/v3-more-samples.test.mjs +182 -0
  101. package/docs/prototypes/v3-payload-collision.mjs +54 -0
  102. package/docs/prototypes/v3-payload-collision.test.mjs +68 -0
  103. package/docs/prototypes/v3-payload-fit.mjs +58 -0
  104. package/docs/prototypes/v3-payload-fit.test.mjs +59 -0
  105. package/docs/prototypes/v3-repeated-binding.mjs +13 -0
  106. package/docs/prototypes/v3-repeated-binding.test.mjs +46 -0
  107. package/docs/prototypes/v3-runtime-browser.mjs +76 -0
  108. package/docs/prototypes/v3-samples.test.mjs +144 -0
  109. package/docs/prototypes/v3-semantics.test.mjs +55 -0
  110. package/docs/prototypes/v3-spatial-plan.test.mjs +102 -0
  111. package/docs/prototypes/v3-typed-plan.mjs +3 -0
  112. package/docs/prototypes/v3-typed-plan.test.mjs +70 -0
  113. package/docs/prototypes/v3-typed-repeat.test.mjs +96 -0
  114. package/docs/v3-asset-persistence.md +66 -0
  115. package/docs/v3-authoring-transactions.md +57 -0
  116. package/docs/v3-capability-bindings.md +109 -0
  117. package/docs/v3-common-kernel-experiment.md +116 -0
  118. package/docs/v3-core-semantics.md +303 -0
  119. package/docs/v3-cutover.md +27 -0
  120. package/docs/v3-design.md +388 -0
  121. package/docs/v3-document-contract.md +55 -0
  122. package/docs/v3-dryroom-profile-validation.md +87 -0
  123. package/docs/v3-editor-integration.md +59 -0
  124. package/docs/v3-full-conveyor-validation.md +69 -0
  125. package/docs/v3-full-dryroom-validation.md +55 -0
  126. package/docs/v3-full-forklift-validation.md +41 -0
  127. package/docs/v3-full-robot-validation.md +64 -0
  128. package/docs/v3-layout-system.md +318 -0
  129. package/docs/v3-minimal-model.md +255 -0
  130. package/docs/v3-operator-contracts.md +148 -0
  131. package/docs/v3-payload-collision-validation.md +42 -0
  132. package/docs/v3-payload-contact-validation.md +56 -0
  133. package/docs/v3-repeated-binding-validation.md +42 -0
  134. package/docs/v3-research-review.md +160 -0
  135. package/docs/v3-runtime-extraction.md +107 -0
  136. package/docs/v3-sample-validation-expanded.md +106 -0
  137. package/docs/v3-sample-validation.md +97 -0
  138. package/docs/v3-shapeassembly-reproduction.md +99 -0
  139. package/docs/v3-spatial-contracts.md +84 -0
  140. package/docs/v3-status.md +41 -0
  141. package/docs/v3-storage-integration.md +58 -0
  142. package/docs/v3-typed-repeat-contracts.md +79 -0
  143. package/package.json +1 -1
@@ -0,0 +1,86 @@
1
+ // Isolated browser experiment; never connects to the product server or edits assets.
2
+ import {createServer} from 'node:http'
3
+ import {readFile} from 'node:fs/promises'
4
+ import {chromium} from '/Users/super/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/node_modules/playwright/index.mjs'
5
+ import {seed,defaults,evaluateFull,geometryOf,sourceGeometry} from './v3-full-dryroom.mjs'
6
+ import * as conveyor from './v3-full-conveyor.mjs'
7
+ import * as robot from './v3-full-robot.mjs'
8
+ import * as forklift from './v3-full-forklift.mjs'
9
+ import {forkliftBindings} from './v3-capability-samples.mjs'
10
+ import {compilePayloadInspection} from './v3-payload-fit.mjs'
11
+ import {compileCollisionInspection} from './v3-payload-collision.mjs'
12
+ const isConveyor=process.argv.includes('--conveyor')
13
+ const isRobot=process.argv.includes('--robot')
14
+ const isCollision=process.argv.includes('--collision')
15
+ const isPayload=process.argv.includes('--payload')||isCollision
16
+ const isForklift=process.argv.includes('--forklift')||isPayload
17
+ const pack=(items)=>items.map(x=>({id:x.id,material:x.material,geometry:x.geometry.toJSON()}))
18
+ const fromV3=args=>pack(evaluateFull(args).map(x=>({...x,geometry:geometryOf(x)})))
19
+ const robotPose={...robot.defaults,'base-pan':30,'shoulder-lift':-35,'elbow-flex':65,'wrist-pitch':-25,'wrist-yaw':40,'tool-roll':20,gripper:50}
20
+ function payloadVariants(){
21
+ const sample=forkliftBindings(),p=(isCollision?compileCollisionInspection:compilePayloadInspection)(sample.model,sample.declaration)
22
+ const scenarios=isCollision?[['Clear placement',[260,50,75],620,100,700],['Upper forks penetrated',[260,250,75],620,500,700],['Floating',[260,200,75],620,100,700],['Curved body: unresolved',[260,343,-1485],50,50,50]]:[['Contact',[260,50,0],620],['Floating 150mm',[260,200,0],620],['Penetrates 20mm',[260,30,0],620],['Between forks: no contact',[260,50,0],100]]
23
+ return scenarios.map(([title,center,width,height=100,depth=850])=>{
24
+ const q={bindingId:'load-low',type:'PALLET',occupied:0,dimensions:[width,height,depth],center,yawDeg:0},r=p.evaluate(forklift.defaults,q)
25
+ const b=r.evaluation.bindings[0],t=b.pose.r.map((row,j)=>b.pose.t[j]+row.reduce((v,x,i)=>v+x*center[i],0))
26
+ const placed={shape:{provider:'box',dimensions:{width,height,depth}},pose:{r:b.pose.r,t}}
27
+ return {title:title+' · contact '+r.inspection.contact.status+(isCollision?' / collision '+r.inspection.collision.status:''),parts:pack([...forklift.evaluateFull().map(x=>({...x,material:isCollision&&r.inspection.collision.pairs.some(p=>p.geometryId===x.id&&p.status==='failed')?{token:'palette.status-danger'}:x.material,geometry:forklift.geometryOf(x)})),{id:'payload',material:{token:'palette.accent'},geometry:forklift.geometryOf(placed)}])}
28
+ })
29
+ }
30
+ const variants=isPayload?payloadVariants():isForklift?[
31
+ {title:'Source seed · rest pose',parts:pack(forklift.seed.parts.map(p=>({id:p.name,material:p.material,geometry:forklift.sourceGeometry(p)})))},
32
+ ...[
33
+ ['V3 · same rest pose',forklift.defaults],
34
+ ['V3 · lift 1000 / reach 200 / drive 37',{...forklift.defaults,lift:1000,reach:200,drive:37}],
35
+ ['V3 · longer forks / lift 2000 / reach 400',{forkLength:1275,lift:2000,reach:400,drive:73}]
36
+ ].map(([title,args])=>({title,parts:pack(forklift.evaluateFull(args).map(x=>({...x,geometry:forklift.geometryOf(x)})))}))
37
+ ]:isRobot?[
38
+ {title:'Source fixture · rest pose',parts:pack(robot.seed.parts.map(p=>({id:p.name,material:p.material??{token:'palette.neutral'},geometry:robot.sourceGeometry(p)})))},
39
+ ...[
40
+ ['V3 · same rest pose',robot.defaults],
41
+ ['V3 · six joints + gripper',robotPose],
42
+ ['V3 · changed link lengths + same joints',{...robotPose,upperLength:630,foreLength:570}]
43
+ ].map(([title,args])=>({title,parts:pack(robot.evaluateFull(args).map(x=>({...x,geometry:robot.geometryOf(x)})))}))
44
+ ]:isConveyor?[
45
+ {title:'Source seed · overlapping end rollers',parts:pack(conveyor.sourceItems())},
46
+ ...[
47
+ ['V3 · reserved end roller space',conveyor.defaults],
48
+ ['V3 · roller rotation 37 degrees',{...conveyor.defaults,angle:37}],
49
+ ['V3 · resized assembly',{length:3000,width:900,rollerHeight:700,angle:37}]
50
+ ].map(([title,args])=>({title,parts:pack(conveyor.evaluateFull(args).map(x=>({...x,geometry:conveyor.geometryOf(x)})))}))
51
+ ]:[
52
+ {title:'Source seed · six parts',parts:pack(seed.parts.map(p=>({id:p.name,material:p.material,geometry:sourceGeometry(p)})))},
53
+ {title:'V3 · same dimensions',parts:fromV3(defaults)},
54
+ {title:'V3 · portal bottom trimmed to floor',parts:fromV3({...defaults,bottom:0})},
55
+ {title:'V3 · resized + lamps raised',parts:fromV3({...defaults,width:4800,height:3600,depth:7200,bottom:0,lampLift:100})}
56
+ ]
57
+ const html=`<!doctype html><meta charset="utf-8"><style>body{margin:0;background:#e9edf2;font:16px sans-serif;color:#17283b}main{display:grid;grid-template-columns:1fr 1fr;gap:12px;padding:12px}section{background:white;border-radius:8px;overflow:hidden}h3{margin:12px;font-size:16px}canvas{display:block;width:100%}p{margin:12px;font-size:12px}</style><main></main><script type="module">
58
+ import * as T from '/three.module.js';
59
+ const variants=${JSON.stringify(variants)};
60
+ const palette={'palette.primary':'#154473','palette.accent':'#f79c40','palette.neutral':'#96989b','palette.dark':'#27292c','palette.light':'#d6d7d9','palette.surface':'#56585b','palette.status-danger':'#f61b30','palette.status-warning':'#e68d1a','palette.status-success':'#1cb648','palette.status-info':'#158bde'};
61
+ window.results=[];window.pixels=[];
62
+ for(const v of variants){
63
+ const section=document.createElement('section');section.innerHTML='<h3>'+v.title+'</h3>';document.querySelector('main').append(section);
64
+ const renderer=new T.WebGLRenderer({antialias:true,preserveDrawingBuffer:true});renderer.setSize(640,420);renderer.setClearColor('#f7f9fb');section.append(renderer.domElement);
65
+ const scene=new T.Scene();scene.add(new T.HemisphereLight(0xffffff,0x687381,2));const light=new T.DirectionalLight(0xffffff,3);light.position.set(5000,9000,9000);scene.add(light);
66
+ // Metal needs reflected illumination; a hemisphere light alone leaves it nearly black.
67
+ const envCanvas=document.createElement('canvas');envCanvas.width=512;envCanvas.height=256;const ctx=envCanvas.getContext('2d');const grad=ctx.createLinearGradient(0,0,0,256);grad.addColorStop(0,'#ffffff');grad.addColorStop(.5,'#b8c4d4');grad.addColorStop(1,'#4e5c6b');ctx.fillStyle=grad;ctx.fillRect(0,0,512,256);ctx.fillStyle='#ffffff';ctx.fillRect(70,30,90,80);const envTex=new T.CanvasTexture(envCanvas);envTex.colorSpace=T.SRGBColorSpace;const pmrem=new T.PMREMGenerator(renderer);scene.environment=pmrem.fromEquirectangular(envTex).texture;
68
+ for(const p of v.parts){const m=p.material,e=m.emissive;const color=palette[e?.on?e.token:m.token]??'#96989b';const mat=new T.MeshStandardMaterial({color,metalness:m.preset==='metal'?1:0,roughness:m.preset==='rubber'?.9:m.preset==='metal'?.3:.4,flatShading:true,transparent:!!m.transparent,emissive:e?.on?color:0,emissiveIntensity:e?.on?e.intensity:0});scene.add(new T.Mesh(new T.BufferGeometryLoader().parse(p.geometry),mat));}
69
+ const camera=new T.PerspectiveCamera(38,640/420,1,50000);camera.position.set(...${JSON.stringify(isForklift?[5200,4200,6200]:isRobot?[2300,1900,2900]:isConveyor?[3000,2400,3300]:[10000,8000,13500])});camera.lookAt(0,${isForklift?1250:isRobot?750:isConveyor?350:1300},0);renderer.render(scene,camera);
70
+ const gl=renderer.getContext(),px=new Uint8Array(640*420*4);gl.readPixels(0,0,640,420,gl.RGBA,gl.UNSIGNED_BYTE,px);window.pixels.push(px);
71
+ window.results.push({title:v.title,calls:renderer.info.render.calls,triangles:renderer.info.render.triangles});
72
+ section.insertAdjacentHTML('beforeend','<p>'+renderer.info.render.calls+' draw calls / '+renderer.info.render.triangles+' triangles · identical camera</p>');
73
+ }
74
+ let different=0,max=0;for(let i=0;i<pixels[0].length;i++){const d=Math.abs(pixels[0][i]-pixels[1][i]);if(d)different++;max=Math.max(max,d)}window.comparison={differentChannels:different,maxChannelDifference:max};window.ready=true;
75
+ </script>`
76
+ const server=createServer(async(req,res)=>{try{if(req.url==='/'){res.setHeader('Content-Type','text/html');res.end(html)}else if(['/three.module.js','/three.core.js'].includes(req.url)){res.setHeader('Content-Type','text/javascript');res.end(await readFile(new URL('../../../things-scene/node_modules/three/build'+req.url,import.meta.url)))}else{res.statusCode=404;res.end()}}catch(e){res.statusCode=500;res.end(String(e))}})
77
+ await new Promise(r=>server.listen(0,'127.0.0.1',r));let browser
78
+ try{
79
+ browser=await chromium.launch({headless:true,executablePath:'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'});
80
+ const page=await browser.newPage({viewport:{width:1328,height:990},deviceScaleFactor:1});const errors=[];page.on('pageerror',e=>errors.push(String(e)));
81
+ await page.goto('http://127.0.0.1:'+server.address().port);await page.waitForFunction(()=>window.ready);
82
+ const screenshot='/private/tmp/figure-v3-full-'+(isCollision?'collision':isPayload?'payload':isForklift?'forklift':isRobot?'robot':isConveyor?'conveyor':'dryroom')+'.png';
83
+ await page.screenshot({path:screenshot,fullPage:true});
84
+ console.log(JSON.stringify({...await page.evaluate(()=>({results:window.results,comparison:window.comparison})),errors,screenshot},null,2));
85
+ if(errors.length)process.exitCode=1;
86
+ }finally{await browser?.close();server.close()}
@@ -0,0 +1,325 @@
1
+ {
2
+ "revision": "cc22bd84afb65e8e37c7d14bebd9a4a629b5d943",
3
+ "python": "3.12.14 (main, Aug 25 2026, 13:50:33) [Clang 22.1.3 ]",
4
+ "torch": "2.8.0",
5
+ "numpy": "2.2.6",
6
+ "faiss": "1.12.0",
7
+ "units": "native abstract units, not Figure mm",
8
+ "results": [
9
+ {
10
+ "case": "lamp_fresh_replay_height_1",
11
+ "lines": [
12
+ "bbox = Cuboid(4, 1, 4, True)",
13
+ "body = Cuboid(1, 1, 1, True)",
14
+ "lamp = Cuboid(.2, .2, .2, True)",
15
+ "attach(body, bbox, .5, 0, .5, .5, 0, .5)",
16
+ "attach(lamp, body, .5, 0, .5, .5, 1, .5)"
17
+ ],
18
+ "result": {
19
+ "body": {
20
+ "size": [
21
+ 1,
22
+ 1,
23
+ 1
24
+ ],
25
+ "center": [
26
+ 0,
27
+ 0,
28
+ 0
29
+ ]
30
+ },
31
+ "lamp": {
32
+ "size": [
33
+ 0.20000000298023224,
34
+ 0.20000000298023224,
35
+ 0.20000000298023224
36
+ ],
37
+ "center": [
38
+ 0,
39
+ 0.6000000238418579,
40
+ 0
41
+ ]
42
+ }
43
+ },
44
+ "contact_point_residuals": [
45
+ 0
46
+ ]
47
+ },
48
+ {
49
+ "case": "lamp_fresh_replay_height_2",
50
+ "lines": [
51
+ "bbox = Cuboid(4, 2, 4, True)",
52
+ "body = Cuboid(1, 2, 1, True)",
53
+ "lamp = Cuboid(.2, .2, .2, True)",
54
+ "attach(body, bbox, .5, 0, .5, .5, 0, .5)",
55
+ "attach(lamp, body, .5, 0, .5, .5, 1, .5)"
56
+ ],
57
+ "result": {
58
+ "body": {
59
+ "size": [
60
+ 1,
61
+ 2,
62
+ 1
63
+ ],
64
+ "center": [
65
+ 0,
66
+ 0,
67
+ 0
68
+ ]
69
+ },
70
+ "lamp": {
71
+ "size": [
72
+ 0.20000000298023224,
73
+ 0.20000000298023224,
74
+ 0.20000000298023224
75
+ ],
76
+ "center": [
77
+ 0,
78
+ 1.100000023841858,
79
+ 0
80
+ ]
81
+ }
82
+ },
83
+ "contact_point_residuals": [
84
+ 0
85
+ ]
86
+ },
87
+ {
88
+ "case": "lamp_fresh_replay_height_4",
89
+ "lines": [
90
+ "bbox = Cuboid(4, 4, 4, True)",
91
+ "body = Cuboid(1, 4, 1, True)",
92
+ "lamp = Cuboid(.2, .2, .2, True)",
93
+ "attach(body, bbox, .5, 0, .5, .5, 0, .5)",
94
+ "attach(lamp, body, .5, 0, .5, .5, 1, .5)"
95
+ ],
96
+ "result": {
97
+ "body": {
98
+ "size": [
99
+ 1,
100
+ 4,
101
+ 1
102
+ ],
103
+ "center": [
104
+ 0,
105
+ 0,
106
+ 0
107
+ ]
108
+ },
109
+ "lamp": {
110
+ "size": [
111
+ 0.20000000298023224,
112
+ 0.20000000298023224,
113
+ 0.20000000298023224
114
+ ],
115
+ "center": [
116
+ 0,
117
+ 2.0999999046325684,
118
+ 0
119
+ ]
120
+ }
121
+ },
122
+ "contact_point_residuals": [
123
+ 1.1920928955078125e-7
124
+ ]
125
+ },
126
+ {
127
+ "case": "squeeze_height_0.5_aligned_True",
128
+ "lines": [
129
+ "bbox = Cuboid(1, 1, 1, True)",
130
+ "beam = Cuboid(.2, 0.5, .2, True)",
131
+ "squeeze(beam, bbox, bbox, top, .5, .5)"
132
+ ],
133
+ "result": {
134
+ "beam": {
135
+ "size": [
136
+ 0.20000000298023224,
137
+ 0.5,
138
+ 0.20000000298023224
139
+ ],
140
+ "center": [
141
+ 0,
142
+ 0.25,
143
+ 0
144
+ ]
145
+ }
146
+ },
147
+ "contact_point_residuals": [
148
+ 0,
149
+ 0.5
150
+ ]
151
+ },
152
+ {
153
+ "case": "squeeze_height_2_aligned_True",
154
+ "lines": [
155
+ "bbox = Cuboid(1, 1, 1, True)",
156
+ "beam = Cuboid(.2, 2, .2, True)",
157
+ "squeeze(beam, bbox, bbox, top, .5, .5)"
158
+ ],
159
+ "result": {
160
+ "beam": {
161
+ "size": [
162
+ 0.20000000298023224,
163
+ 2,
164
+ 0.20000000298023224
165
+ ],
166
+ "center": [
167
+ 0,
168
+ -0.5,
169
+ 0
170
+ ]
171
+ }
172
+ },
173
+ "contact_point_residuals": [
174
+ 0,
175
+ 1
176
+ ]
177
+ },
178
+ {
179
+ "case": "squeeze_height_0.5_aligned_False",
180
+ "lines": [
181
+ "bbox = Cuboid(1, 1, 1, True)",
182
+ "beam = Cuboid(.2, 0.5, .2, False)",
183
+ "squeeze(beam, bbox, bbox, top, .5, .5)"
184
+ ],
185
+ "result": {
186
+ "beam": {
187
+ "size": [
188
+ 0.20000000298023224,
189
+ 1,
190
+ 0.20000000298023224
191
+ ],
192
+ "center": [
193
+ 0,
194
+ 0,
195
+ 0
196
+ ]
197
+ }
198
+ },
199
+ "contact_point_residuals": [
200
+ 0,
201
+ 0
202
+ ]
203
+ },
204
+ {
205
+ "case": "squeeze_height_2_aligned_False",
206
+ "lines": [
207
+ "bbox = Cuboid(1, 1, 1, True)",
208
+ "beam = Cuboid(.2, 2, .2, False)",
209
+ "squeeze(beam, bbox, bbox, top, .5, .5)"
210
+ ],
211
+ "result": {
212
+ "beam": {
213
+ "size": [
214
+ 0.20000000298023224,
215
+ 1,
216
+ 0.20000000298023224
217
+ ],
218
+ "center": [
219
+ 0,
220
+ 0,
221
+ 0
222
+ ]
223
+ }
224
+ },
225
+ "contact_point_residuals": [
226
+ 0,
227
+ 0
228
+ ]
229
+ },
230
+ {
231
+ "case": "direct_resize_after_attachment",
232
+ "lines": [
233
+ "bbox = Cuboid(4, 4, 4, True)",
234
+ "body = Cuboid(1, 1, 1, True)",
235
+ "lamp = Cuboid(.2, .2, .2, True)",
236
+ "attach(body, bbox, .5, 0, .5, .5, 0, .5)",
237
+ "attach(lamp, body, .5, 0, .5, .5, 1, .5)",
238
+ "body = Cuboid(1, 2, 1, True)"
239
+ ],
240
+ "result": {
241
+ "body": {
242
+ "size": [
243
+ 1,
244
+ 2,
245
+ 1
246
+ ],
247
+ "center": [
248
+ 0,
249
+ -1.5,
250
+ 0
251
+ ]
252
+ },
253
+ "lamp": {
254
+ "size": [
255
+ 0.20000000298023224,
256
+ 0.20000000298023224,
257
+ 0.20000000298023224
258
+ ],
259
+ "center": [
260
+ 0,
261
+ -0.8999999761581421,
262
+ 0
263
+ ]
264
+ }
265
+ },
266
+ "contact_point_residuals": [
267
+ 0.5
268
+ ]
269
+ },
270
+ {
271
+ "case": "conflicting_same_point_reverse_False",
272
+ "lines": [
273
+ "bbox = Cuboid(1, 1, 1, True)",
274
+ "part = Cuboid(.2, .2, .2, True)",
275
+ "attach(part, bbox, .5, 0, .5, .5, 0, .5)",
276
+ "attach(part, bbox, .5, 0, .5, .5, 1, .5)"
277
+ ],
278
+ "result": {
279
+ "part": {
280
+ "size": [
281
+ 0.20000000298023224,
282
+ 0.20000000298023224,
283
+ 0.20000000298023224
284
+ ],
285
+ "center": [
286
+ 0,
287
+ -0.4000000059604645,
288
+ 0
289
+ ]
290
+ }
291
+ },
292
+ "contact_point_residuals": [
293
+ 0,
294
+ 1
295
+ ]
296
+ },
297
+ {
298
+ "case": "conflicting_same_point_reverse_True",
299
+ "lines": [
300
+ "bbox = Cuboid(1, 1, 1, True)",
301
+ "part = Cuboid(.2, .2, .2, True)",
302
+ "attach(part, bbox, .5, 0, .5, .5, 1, .5)",
303
+ "attach(part, bbox, .5, 0, .5, .5, 0, .5)"
304
+ ],
305
+ "result": {
306
+ "part": {
307
+ "size": [
308
+ 0.20000000298023224,
309
+ 0.20000000298023224,
310
+ 0.20000000298023224
311
+ ],
312
+ "center": [
313
+ 0,
314
+ 0.6000000238418579,
315
+ 0
316
+ ]
317
+ }
318
+ },
319
+ "contact_point_residuals": [
320
+ 1,
321
+ 0
322
+ ]
323
+ }
324
+ ]
325
+ }
@@ -0,0 +1,84 @@
1
+ """Independent research probe. Requires an external checkout; not product code.
2
+
3
+ Usage: python shapeassembly-probe.py /path/to/ShapeAssembly
4
+ No upstream source is copied or modified. No models are trained/downloaded.
5
+ Reports observed behavior, not a claim that Figure or the paper is fully tested.
6
+ """
7
+ import json
8
+ import subprocess
9
+ import sys
10
+ from pathlib import Path
11
+
12
+ checkout = Path(sys.argv[1]).resolve()
13
+ expected = 'cc22bd84afb65e8e37c7d14bebd9a4a629b5d943'
14
+ revision = subprocess.check_output(['git', '-C', str(checkout), 'rev-parse', 'HEAD'], text=True).strip()
15
+ if revision != expected:
16
+ raise RuntimeError(f'Expected {expected}, found {revision}')
17
+ sys.path.insert(0, str(checkout / 'code'))
18
+ import torch
19
+ import numpy as np
20
+ import faiss
21
+ from ShapeAssembly import Program
22
+
23
+ torch.set_num_threads(1)
24
+
25
+ def point(p, name, xyz):
26
+ return p.cuboids[name].getPos(*(torch.tensor(float(v)) for v in xyz))
27
+
28
+ def snapshot(p):
29
+ return {name: {'size': c.dims.tolist(), 'center': c.pos.tolist()}
30
+ for name, c in p.cuboids.items() if name != 'bbox'}
31
+
32
+ def run(name, lines, contacts):
33
+ p = Program()
34
+ try:
35
+ for line in lines:
36
+ p.execute(line)
37
+ residuals = [float(torch.linalg.vector_norm(point(p,a,u)-point(p,b,v)))
38
+ for a,u,b,v in contacts]
39
+ return {'case': name, 'lines': lines, 'result': snapshot(p),
40
+ 'contact_point_residuals': residuals}
41
+ except Exception as exc:
42
+ return {'case': name, 'lines': lines, 'error': type(exc).__name__ + ': ' + str(exc)}
43
+
44
+ results = []
45
+ for height in [1, 2, 4]:
46
+ results.append(run(f'lamp_fresh_replay_height_{height}', [
47
+ f'bbox = Cuboid(4, {height}, 4, True)',
48
+ f'body = Cuboid(1, {height}, 1, True)',
49
+ 'lamp = Cuboid(.2, .2, .2, True)',
50
+ 'attach(body, bbox, .5, 0, .5, .5, 0, .5)',
51
+ 'attach(lamp, body, .5, 0, .5, .5, 1, .5)'
52
+ ], [('lamp',(.5,0,.5),'body',(.5,1,.5))]))
53
+
54
+ for aligned in ['True', 'False']:
55
+ for size in [.5, 2]:
56
+ results.append(run(f'squeeze_height_{size}_aligned_{aligned}', [
57
+ 'bbox = Cuboid(1, 1, 1, True)',
58
+ f'beam = Cuboid(.2, {size}, .2, {aligned})',
59
+ 'squeeze(beam, bbox, bbox, top, .5, .5)'
60
+ ], [('beam',(.5,1,.5),'bbox',(.5,1,.5)),
61
+ ('beam',(.5,0,.5),'bbox',(.5,0,.5))]))
62
+
63
+ results.append(run('direct_resize_after_attachment', [
64
+ 'bbox = Cuboid(4, 4, 4, True)',
65
+ 'body = Cuboid(1, 1, 1, True)',
66
+ 'lamp = Cuboid(.2, .2, .2, True)',
67
+ 'attach(body, bbox, .5, 0, .5, .5, 0, .5)',
68
+ 'attach(lamp, body, .5, 0, .5, .5, 1, .5)',
69
+ 'body = Cuboid(1, 2, 1, True)'
70
+ ], [('lamp',(.5,0,.5),'body',(.5,1,.5))]))
71
+
72
+ for reverse in [False, True]:
73
+ attach = ['attach(part, bbox, .5, 0, .5, .5, 0, .5)',
74
+ 'attach(part, bbox, .5, 0, .5, .5, 1, .5)']
75
+ results.append(run(f'conflicting_same_point_reverse_{reverse}', [
76
+ 'bbox = Cuboid(1, 1, 1, True)',
77
+ 'part = Cuboid(.2, .2, .2, True)',
78
+ *(reversed(attach) if reverse else attach)
79
+ ], [('part',(.5,0,.5),'bbox',(.5,0,.5)),
80
+ ('part',(.5,0,.5),'bbox',(.5,1,.5))]))
81
+
82
+ print(json.dumps({'revision': revision, 'python': sys.version,
83
+ 'torch': torch.__version__, 'numpy': np.__version__, 'faiss': faiss.__version__,
84
+ 'units': 'native abstract units, not Figure mm', 'results': results}, indent=2))
@@ -0,0 +1,15 @@
1
+ import * as dryroom from './v3-full-dryroom.mjs'
2
+ import * as conveyor from './v3-full-conveyor.mjs'
3
+ import * as robot from './v3-full-robot.mjs'
4
+ import * as forklift from './v3-full-forklift.mjs'
5
+ import {robotBinding,forkliftBindings} from './v3-capability-samples.mjs'
6
+ export function sampleAsset(kind){
7
+ const modules={dryroom,conveyor,robot,forklift},sample=modules[kind]
8
+ if(!sample)throw Error('unknown sample')
9
+ const pair=kind==='robot'?robotBinding():kind==='forklift'?forkliftBindings():{model:sample.fullModel(),declaration:{version:'r3-capability-3',assetFrame:'world',bindings:[]}}
10
+ const designInputs={},stateDefaults={}
11
+ for(const i of pair.model.inputs)(i.role==='state'?stateDefaults:designInputs)[i.id]=sample.defaults[i.id]
12
+ return {version:'figure-v3-asset-experiment-1',name:sample.seed.type,document:{version:'figure-v3-prototype-1',model:pair.model,capabilities:pair.declaration},designInputs,stateDefaults,
13
+ palette:{'palette.primary':'#154473','palette.accent':'#f79c40','palette.neutral':'#96989b','palette.dark':'#27292c','palette.light':'#d6d7d9','palette.surface':'#56585b','palette.status-danger':'#f61b30','palette.status-warning':'#e68d1a','palette.status-info':'#158bde','palette.status-success':'#1cb648'},
14
+ appearance:sample.seed.parts.map(p=>({target:p.name,material:structuredClone(p.material??{token:'palette.neutral'}),...(p.primitive==='cylinder'?{segments:p.segments??8}:{}),...(p.capability?{legacyCapability:structuredClone(p.capability)}:{})}))}
15
+ }
@@ -0,0 +1,52 @@
1
+ // Single-process, isolated disk store. Not the product database or an authorization layer.
2
+ import {readFile, open, rename, unlink} from 'node:fs/promises'
3
+ import {join, resolve} from 'node:path'
4
+ import {randomUUID, createHash} from 'node:crypto'
5
+ import {parseAsset, serializeAsset} from './v3-asset.mjs'
6
+
7
+ export class StoreError extends Error {
8
+ constructor(code,message){super(message);this.code=code}
9
+ }
10
+ const fail=(code,message)=>{throw new StoreError(code,message)}
11
+ // Shared across store handles in this process; a multi-process deployment needs database CAS.
12
+ const queues=new Map()
13
+ async function exclusive(key,operation){
14
+ const previous=queues.get(key)||Promise.resolve()
15
+ const next=previous.catch(()=>{}).then(operation);queues.set(key,next)
16
+ try{return await next}finally{if(queues.get(key)===next)queues.delete(key)}
17
+ }
18
+ export function createAssetStore(directory){
19
+ const root=resolve(directory)
20
+ function file(scope,id){
21
+ for(const v of [scope,id])if(typeof v!=='string'||!v.trim()||v.length>200)fail('IDENTITY','scope and asset ID required')
22
+ return join(root,createHash('sha256').update(JSON.stringify([scope,id])).digest('hex')+'.json')
23
+ }
24
+ async function load(path,scope,id){
25
+ let raw;try{raw=await readFile(path,'utf8')}catch(e){if(e.code==='ENOENT')return null;throw e}
26
+ let record;try{record=JSON.parse(raw)}catch{fail('CORRUPT','stored record is not JSON')}
27
+ if(record?.format!=='v3-store-experiment-1'||record.scope!==scope||record.id!==id||typeof record.revision!=='string'||!record.revision)fail('CORRUPT','stored record identity or revision invalid')
28
+ // Stored content must pass the same semantic validation as incoming content.
29
+ parseAsset(record.text)
30
+ return record
31
+ }
32
+ return {
33
+ async read({scope,id}){const record=await load(file(scope,id),scope,id);if(!record)fail('NOT_FOUND','asset not found');return record},
34
+ async save({scope,id,expectedRevision,text}){
35
+ if(expectedRevision!==null&&(typeof expectedRevision!=='string'||!expectedRevision))fail('PRECONDITION','expectedRevision required; null means create only')
36
+ const path=file(scope,id),canonical=serializeAsset(parseAsset(text))
37
+ return exclusive(path,async()=>{
38
+ const current=await load(path,scope,id)
39
+ if((current?.revision??null)!==expectedRevision)fail('CONFLICT','asset changed; reload before saving')
40
+ const record={format:'v3-store-experiment-1',scope,id,revision:randomUUID(),text:canonical}
41
+ const temporary=path+'.'+randomUUID()+'.tmp';let handle
42
+ try{
43
+ handle=await open(temporary,'wx',0o600);await handle.writeFile(JSON.stringify(record));await handle.sync();await handle.close();handle=null
44
+ await rename(temporary,path)
45
+ }finally{
46
+ await handle?.close();await unlink(temporary).catch(e=>{if(e.code!=='ENOENT')throw e})
47
+ }
48
+ return record
49
+ })
50
+ }
51
+ }
52
+ }
@@ -0,0 +1,59 @@
1
+ import {test} from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import {mkdtemp,rm,readdir} from 'node:fs/promises'
4
+ import {tmpdir} from 'node:os'
5
+ import {join} from 'node:path'
6
+ import {execFile} from 'node:child_process'
7
+ import {promisify} from 'node:util'
8
+ import {createAssetStore} from './v3-asset-store.mjs'
9
+ import {sampleAsset} from './v3-asset-samples.mjs'
10
+ import {serializeAsset,parseAsset} from './v3-asset.mjs'
11
+ import {createEditorWorkspace} from './v3-editor-workspace.mjs'
12
+ async function fixture(t){const dir=await mkdtemp(join(tmpdir(),'v3-store-'));t.after(()=>rm(dir,{recursive:true,force:true}));return {dir,store:createAssetStore(dir)}}
13
+ const body=()=>serializeAsset(sampleAsset('forklift'))
14
+ const key={scope:'test-domain',id:'forklift'}
15
+ test('disk save survives independent store handle; explicit create and scoped identity',async t=>{
16
+ const {dir,store}=await fixture(t),saved=await store.save({...key,expectedRevision:null,text:body()})
17
+ assert.deepEqual(await createAssetStore(dir).read(key),saved)
18
+ await assert.rejects(store.read({...key,scope:'other-domain'}),e=>e.code==='NOT_FOUND')
19
+ await assert.rejects(store.save({...key,expectedRevision:null,text:body()}),e=>e.code==='CONFLICT')
20
+ await assert.rejects(store.save({...key,text:body()}),e=>e.code==='PRECONDITION')
21
+ // Raw identifiers never become filesystem paths.
22
+ await store.save({scope:'../other',id:'../../escape',expectedRevision:null,text:body()})
23
+ assert.equal((await readdir(dir)).length,2)
24
+ assert.ok((await readdir(dir)).every(f=>/^[0-9a-f]{64}\.json$/.test(f)))
25
+ })
26
+ test('two writers cannot silently overwrite; content ABA still gets new revision',async t=>{
27
+ const {dir,store}=await fixture(t),a=await store.save({...key,expectedRevision:null,text:body()})
28
+ const asset=parseAsset(body());asset.designInputs.forkLength=1275;const changed=serializeAsset(asset)
29
+ const results=await Promise.allSettled([store,createAssetStore(dir)].map(s=>s.save({...key,expectedRevision:a.revision,text:changed})))
30
+ assert.equal(results.filter(r=>r.status==='fulfilled').length,1)
31
+ assert.equal(results.find(r=>r.status==='rejected').reason.code,'CONFLICT')
32
+ const b=await store.read(key),c=await store.save({...key,expectedRevision:b.revision,text:body()})
33
+ assert.notEqual(c.revision,a.revision)
34
+ await assert.rejects(store.save({...key,expectedRevision:a.revision,text:changed}),e=>e.code==='CONFLICT')
35
+ })
36
+ test('invalid source leaves prior snapshot intact and produces no temporary file',async t=>{
37
+ const {dir,store}=await fixture(t),a=await store.save({...key,expectedRevision:null,text:body()})
38
+ await assert.rejects(store.save({...key,expectedRevision:a.revision,text:'{"version":2}'}))
39
+ assert.deepEqual(await store.read(key),a);assert.equal((await readdir(dir)).length,1)
40
+ })
41
+ test('failed persistence does not clear editor dirty state',async t=>{
42
+ const {store}=await fixture(t),original=await store.save({...key,expectedRevision:null,text:body()})
43
+ const w=createEditorWorkspace(),v=w.open(original.text)
44
+ w.apply({context:v.context,origin:'ui',expectedRevision:0,changes:[{kind:'set-design',input:'forkLength',unit:'mm',value:1275}]})
45
+ const snapshot=w.export({context:v.context})
46
+ await store.save({...key,expectedRevision:original.revision,text:body()})
47
+ await assert.rejects(store.save({...key,expectedRevision:original.revision,text:snapshot.text}),e=>e.code==='CONFLICT')
48
+ assert.equal(w.read().dirty,true)
49
+ assert.equal(w.read().fields.find(f=>f.id==='forkLength').value,1275)
50
+ })
51
+ test('a separate Node process can restore the saved asset without editor memory',async t=>{
52
+ const {dir,store}=await fixture(t),asset=parseAsset(body());asset.designInputs.forkLength=1275
53
+ const saved=await store.save({...key,expectedRevision:null,text:serializeAsset(asset)})
54
+ const script=`import {createAssetStore} from ${JSON.stringify(new URL('./v3-asset-store.mjs',import.meta.url).href)};
55
+ const record=await createAssetStore(process.argv[1]).read(${JSON.stringify(key)});
56
+ console.log(JSON.stringify({revision:record.revision,length:JSON.parse(record.text).designInputs.forkLength}));`
57
+ const {stdout}=await promisify(execFile)(process.execPath,['--input-type=module','-e',script,dir])
58
+ assert.deepEqual(JSON.parse(stdout),{revision:saved.revision,length:1275})
59
+ })
@@ -0,0 +1 @@
1
+ export {compileV3Asset as compileAsset,parseV3Asset as parseAsset,serializeV3Asset as serializeAsset,captureV3AuthoredInputs as captureAuthoredInputs} from '../../src/v3-asset.ts'