@hatiolab/figure-model 0.6.7 → 0.7.1

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 (127) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/README.md +8 -4
  3. package/dist/asset-types.d.ts +8 -13
  4. package/dist/asset-types.d.ts.map +1 -1
  5. package/dist/asset-types.js.map +1 -1
  6. package/dist/asset.js +4 -4
  7. package/dist/asset.js.map +1 -1
  8. package/dist/authoring-actions.js +18 -5
  9. package/dist/authoring-actions.js.map +1 -1
  10. package/dist/authoring-types.d.ts +6 -11
  11. package/dist/authoring-types.d.ts.map +1 -1
  12. package/dist/authoring-types.js.map +1 -1
  13. package/dist/editor-workspace.d.ts +1 -1
  14. package/dist/editor-workspace.d.ts.map +1 -1
  15. package/dist/editor-workspace.js +13 -6
  16. package/dist/editor-workspace.js.map +1 -1
  17. package/dist/extensions.d.ts +15 -0
  18. package/dist/extensions.d.ts.map +1 -0
  19. package/dist/extensions.js +63 -0
  20. package/dist/extensions.js.map +1 -0
  21. package/dist/gate.d.ts +1 -1
  22. package/dist/gate.d.ts.map +1 -1
  23. package/dist/gate.js +28 -33
  24. package/dist/gate.js.map +1 -1
  25. package/dist/index.d.ts +2 -2
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +2 -2
  28. package/dist/index.js.map +1 -1
  29. package/dist/input-preview.d.ts +48 -0
  30. package/dist/input-preview.d.ts.map +1 -0
  31. package/dist/{authoring-session.js → input-preview.js} +7 -63
  32. package/dist/input-preview.js.map +1 -0
  33. package/dist/kernel-version.d.ts +1 -1
  34. package/dist/kernel-version.js +1 -1
  35. package/dist/names.d.ts.map +1 -1
  36. package/dist/names.js +5 -1
  37. package/dist/names.js.map +1 -1
  38. package/dist/package.d.ts.map +1 -1
  39. package/dist/package.js +18 -15
  40. package/dist/package.js.map +1 -1
  41. package/dist/proposal.d.ts +5 -1
  42. package/dist/proposal.d.ts.map +1 -1
  43. package/dist/proposal.js +18 -5
  44. package/dist/proposal.js.map +1 -1
  45. package/dist/release-contract.d.ts +1 -1
  46. package/dist/release-contract.d.ts.map +1 -1
  47. package/dist/release-contract.js +13 -24
  48. package/dist/release-contract.js.map +1 -1
  49. package/dist/shape-edit.d.ts +1 -1
  50. package/dist/shape-edit.d.ts.map +1 -1
  51. package/dist/size-edit.d.ts +1 -1
  52. package/dist/size-edit.d.ts.map +1 -1
  53. package/docs/asset-persistence.md +6 -3
  54. package/docs/authoring-transactions.md +21 -19
  55. package/docs/capability-bindings.md +6 -3
  56. package/docs/common-kernel-experiment.md +3 -0
  57. package/docs/document-contract.md +6 -3
  58. package/docs/dryroom-profile-validation.md +3 -0
  59. package/docs/editor-integration.md +5 -2
  60. package/docs/full-conveyor-validation.md +5 -2
  61. package/docs/full-dryroom-validation.md +6 -3
  62. package/docs/full-forklift-validation.md +5 -2
  63. package/docs/full-robot-validation.md +5 -2
  64. package/docs/payload-collision-validation.md +5 -2
  65. package/docs/payload-contact-validation.md +5 -2
  66. package/docs/prototypes/README.md +27 -61
  67. package/docs/repeated-binding-validation.md +6 -3
  68. package/docs/review-2026-09-22.md +3 -0
  69. package/docs/runtime-extraction.md +9 -2
  70. package/docs/sample-validation-expanded.md +3 -0
  71. package/docs/sample-validation.md +3 -0
  72. package/docs/shape-contract.md +7 -5
  73. package/docs/spatial-contracts.md +3 -0
  74. package/docs/status.md +2 -1
  75. package/docs/storage-integration.md +6 -3
  76. package/docs/typed-repeat-contracts.md +3 -0
  77. package/package.json +9 -9
  78. package/dist/authoring-session.d.ts +0 -121
  79. package/dist/authoring-session.d.ts.map +0 -1
  80. package/dist/authoring-session.js.map +0 -1
  81. package/dist/carrier.d.ts +0 -23
  82. package/dist/carrier.d.ts.map +0 -1
  83. package/dist/carrier.js +0 -65
  84. package/dist/carrier.js.map +0 -1
  85. package/docs/prototypes/asset-samples.mjs +0 -15
  86. package/docs/prototypes/asset-store.mjs +0 -52
  87. package/docs/prototypes/asset-store.test.mjs +0 -59
  88. package/docs/prototypes/asset.mjs +0 -1
  89. package/docs/prototypes/asset.schema.json +0 -36
  90. package/docs/prototypes/asset.test.mjs +0 -68
  91. package/docs/prototypes/authoring-session.mjs +0 -1
  92. package/docs/prototypes/authoring-session.test.mjs +0 -65
  93. package/docs/prototypes/capability-plan.mjs +0 -1
  94. package/docs/prototypes/capability-plan.test.mjs +0 -56
  95. package/docs/prototypes/capability-samples.mjs +0 -23
  96. package/docs/prototypes/document.mjs +0 -2
  97. package/docs/prototypes/document.schema.json +0 -66
  98. package/docs/prototypes/document.test.mjs +0 -55
  99. package/docs/prototypes/dryroom-mesh.mjs +0 -60
  100. package/docs/prototypes/dryroom-mesh.test.mjs +0 -30
  101. package/docs/prototypes/editor-browser.mjs +0 -97
  102. package/docs/prototypes/editor-workspace.mjs +0 -1
  103. package/docs/prototypes/editor-workspace.test.mjs +0 -35
  104. package/docs/prototypes/feature-pattern.test.mjs +0 -131
  105. package/docs/prototypes/full-conveyor.mjs +0 -60
  106. package/docs/prototypes/full-conveyor.test.mjs +0 -61
  107. package/docs/prototypes/full-dryroom.mjs +0 -67
  108. package/docs/prototypes/full-dryroom.test.mjs +0 -54
  109. package/docs/prototypes/full-forklift.mjs +0 -40
  110. package/docs/prototypes/full-forklift.test.mjs +0 -37
  111. package/docs/prototypes/full-robot.mjs +0 -35
  112. package/docs/prototypes/full-robot.test.mjs +0 -68
  113. package/docs/prototypes/kernel.mjs +0 -147
  114. package/docs/prototypes/kernel.test.mjs +0 -144
  115. package/docs/prototypes/more-samples.test.mjs +0 -182
  116. package/docs/prototypes/payload-collision.mjs +0 -54
  117. package/docs/prototypes/payload-collision.test.mjs +0 -68
  118. package/docs/prototypes/payload-fit.mjs +0 -58
  119. package/docs/prototypes/payload-fit.test.mjs +0 -59
  120. package/docs/prototypes/render-dryroom.py +0 -60
  121. package/docs/prototypes/render-full-dryroom.mjs +0 -86
  122. package/docs/prototypes/repeated-binding.mjs +0 -13
  123. package/docs/prototypes/repeated-binding.test.mjs +0 -46
  124. package/docs/prototypes/runtime-browser.mjs +0 -76
  125. package/docs/prototypes/samples.test.mjs +0 -144
  126. package/docs/prototypes/spatial-plan.test.mjs +0 -102
  127. package/docs/prototypes/typed-repeat.test.mjs +0 -96
@@ -1,102 +0,0 @@
1
- import {test} from 'node:test'
2
- import assert from 'node:assert/strict'
3
- import {compile,ContractError} from './typed-plan.mjs'
4
- const near=(a,b)=>assert.ok(Number.isFinite(a)&&Math.abs(a-b)<1e-8,`${a} != ${b}`)
5
- const equal=(a,b)=>a.forEach((v,i)=>near(v,b[i]))
6
- const c=(id,value,unit='mm')=>({id,value,unit})
7
- const n=(id,op,args,outputs,params)=>({id,op,args,outputs,...(params?{params}:{})})
8
- const rigid=(id,from,to,translation=['zero','zero','zero'],rotation=['angle0','angle0','angle0'])=>n(id,'rigid@1',[...translation,...rotation],{pose:id},{from,to})
9
- function base(){return {version:'r3-spatial-1',inputs:[],constants:[c('zero',0),c('angle0',0,'rad')],nodes:[]}}
10
- function wheel(){
11
- const m=base();m.inputs=[{id:'radius',unit:'mm',min:10,max:1000},{id:'floorY',unit:'mm',min:-100,max:100},{id:'heading',unit:'deg',min:-180,max:180}]
12
- m.constants.push(c('axle',84),c('tilt',-90,'deg'))
13
- m.nodes.push(n('wheel','cylinder-shape@1',['radius','axle'],{shape:'shape'},{frame:'wheelLocal'}),
14
- n('bottom','feature@1',['shape'],{pose:'self'},{frame:'touch',name:'radialBottom'}),
15
- rigid('floor','floorFrame','world',['zero','floorY','zero'],['angle0','heading','angle0']),
16
- rigid('mating','touch','floorFrame',['zero','zero','zero'],['tilt','angle0','angle0']),
17
- n('attachment','attach@1',['floor','mating','self'],{pose:'pose'}),
18
- n('place','place@1',['shape','pose'],{placed:'placed'}),
19
- n('witness','compose@1',['pose','self'],{pose:'contactWorld'}))
20
- return m
21
- }
22
- test('AGV wheel intent: changed radius, floor height and heading preserve contact (125 cases)',()=>{
23
- const plan=compile(wheel())
24
- for(const radius of [50,115,162.63455967,250,500])for(const floorY of [-100,-20,0,20,100])for(const heading of [-180,-45,0,60,180]){
25
- const r=plan.evaluate({radius,floorY,heading})
26
- equal(r.values.contactWorld.t,[0,floorY,0]);near(r.values.pose.t[1],floorY+radius)
27
- near(r.geometry[0].shape.dimensions.radius,radius)
28
- }
29
- })
30
- function lamp(){
31
- const m=base();m.inputs=[{id:'height',unit:'mm',min:100,max:4000},{id:'rx',unit:'deg',min:-180,max:180},{id:'ry',unit:'deg',min:-180,max:180},{id:'rz',unit:'deg',min:-180,max:180}]
32
- m.constants.push(c('width',400),c('depth',100),c('lampSize',40))
33
- m.nodes.push(n('support','box-shape@1',['width','height','depth'],{shape:'supportShape'},{frame:'supportLocal'}),
34
- n('supportFeature','feature@1',['supportShape'],{pose:'top'},{frame:'supportTop',name:'top'}),
35
- rigid('supportPose','supportLocal','world',['zero','zero','zero'],['rx','ry','rz']),
36
- n('worldTop','compose@1',['supportPose','top'],{pose:'worldTop'}),
37
- n('lamp','box-shape@1',['lampSize','lampSize','lampSize'],{shape:'lampShape'},{frame:'lampLocal'}),
38
- n('lampFeature','feature@1',['lampShape'],{pose:'bottom'},{frame:'lampBottom',name:'bottom'}),
39
- rigid('offset','lampBottom','supportTop'),
40
- n('attachment','attach@1',['worldTop','offset','bottom'],{pose:'lampPose'}),
41
- n('lampWitness','compose@1',['lampPose','bottom'],{pose:'lampContact'}),
42
- n('placedSupport','place@1',['supportShape','supportPose'],{placed:'supportPlaced'}),
43
- n('placedLamp','place@1',['lampShape','lampPose'],{placed:'lampPlaced'}))
44
- return m
45
- }
46
- test('support/lamp attachment: 4 heights × 27 general 3-axis orientations',()=>{
47
- const p=compile(lamp())
48
- for(const height of [100,500,1500,3000])for(const rx of [-65,0,80])for(const ry of [-35,0,90])for(const rz of [-40,0,160]){
49
- const r=p.evaluate({height,rx,ry,rz}),a=r.values.lampContact,b=r.values.worldTop
50
- equal(a.t,b.t);equal(a.r.flat(),b.r.flat());assert.equal(r.geometry.length,2)
51
- }
52
- })
53
- test('two-link articulation: same compiler resolves dimensions, features and moving attachment',()=>{
54
- const m=lamp()
55
- // Replace static offset with a hinge at the shared interface; no special robot engine.
56
- m.nodes.find(n=>n.id==='offset').args[3]='joint'
57
- m.inputs.push({id:'joint',unit:'deg',min:-150,max:150})
58
- const p=compile(m)
59
- for(const height of [100,1000,3000])for(const joint of [-150,-60,0,60,150]){
60
- const r=p.evaluate({height,rx:30,ry:45,rz:70,joint})
61
- equal(r.values.lampContact.t,r.values.worldTop.t)
62
- near(r.values.lampPose.r[0].reduce((s,x)=>s+x*x,0),1)
63
- }
64
- })
65
- test('world-point distance may drive another shape length in the same graph',()=>{
66
- const m=base();m.constants.push(c('x',30),c('y',40),c('r',5))
67
- m.nodes.push(n('a','point@1',['zero','zero','zero'],{point:'a'},{frame:'world'}),
68
- n('b','point@1',['x','y','zero'],{point:'b'},{frame:'local'}),rigid('map','local','world'),
69
- n('mapped','map-point@1',['map','b'],{point:'worldB'}),n('distance','distance@1',['a','worldB'],{length:'len'}),
70
- n('beam','cylinder-shape@1',['r','len'],{shape:'shape'},{frame:'beamLocal'}))
71
- near(compile(m).evaluate({}).values.shape.dimensions.length,50)
72
- m.nodes.find(n=>n.id==='distance').args[1]='b'
73
- assert.throws(()=>compile(m),e=>e.code==='FRAME_TYPE')
74
- })
75
- test('wrong transform order, wrong self feature frame and scalar/spatial misuse reject',()=>{
76
- for(const mutate of [
77
- m=>{m.nodes.find(n=>n.id==='witness').args.reverse()},
78
- m=>{m.nodes.find(n=>n.id==='mating').params.from='different'},
79
- m=>{m.nodes.push(n('bad','add@1',['pose','pose'],{value:'bad'}))},
80
- m=>{m.nodes.find(n=>n.id==='place').args[1]='floor'}
81
- ]){
82
- const m=wheel();mutate(m);assert.throws(()=>compile(m),e=>e instanceof ContractError&&['FRAME_TYPE','TYPE'].includes(e.code))
83
- }
84
- })
85
- test('cross geometry/feature/placement cycle is diagnosed as one graph',()=>{
86
- const m=wheel()
87
- m.nodes.push(n('a','point@1',['zero','zero','zero'],{point:'origin'},{frame:'wheelLocal'}),
88
- n('mapped','map-point@1',['pose','origin'],{point:'worldOrigin'}),
89
- n('b','point@1',['zero','zero','zero'],{point:'worldZero'},{frame:'world'}),
90
- n('distance','distance@1',['worldOrigin','worldZero'],{length:'derivedRadius'}))
91
- m.nodes.find(n=>n.id==='wheel').args[0]='derivedRadius'
92
- assert.throws(()=>compile(m),e=>e.code==='CYCLE'&&e.message.includes('wheel'))
93
- })
94
- test('missing feature and declaration reorder; unsuccessful evaluation leaves no stale geometry',()=>{
95
- const m=lamp(),args={height:500,rx:30,ry:70,rz:-20},p=compile(m),r=p.evaluate(args)
96
- m.nodes.reverse();const reversed=compile(m).evaluate(args)
97
- equal(r.values.lampPose.t,reversed.values.lampPose.t)
98
- assert.throws(()=>p.evaluate({...args,height:0}),e=>e.code==='INPUT_RANGE')
99
- assert.deepEqual(p.evaluate(args),r)
100
- m.nodes.find(n=>n.id==='lampFeature').params.name='opening'
101
- assert.throws(()=>compile(m),e=>e.code==='UNKNOWN_FEATURE')
102
- })
@@ -1,96 +0,0 @@
1
- import fs from 'node:fs'
2
- import {test} from 'node:test'
3
- import assert from 'node:assert/strict'
4
- import {compile} from './typed-plan.mjs'
5
- import {meshOf,meshAudit} from './dryroom-mesh.mjs'
6
- const seeds=JSON.parse(fs.readFileSync(new URL('../../../operato-application/packages/operato-figure/server/seed/figure-samples.json',import.meta.url)))
7
- const c=(id,value,unit='mm')=>({id,value,unit})
8
- const n=(id,op,args,outputs,params)=>({id,op,args,outputs,...(params?{params}:{})})
9
- const near=(a,b)=>assert.ok(Math.abs(a-b)<1e-6,`${a} != ${b}`)
10
- export function repeatModel(kind){
11
- const type={tray:'FORMATION_TRAY',charger:'FORMATION_CHARGER',conveyor:'CONVEYOR'}[kind]
12
- const s=seeds.find(s=>s.type===type).source,primary=s.parts.find(p=>p.name===({tray:'cell',charger:'shelf',conveyor:'roller'}[kind]))
13
- const secondary=kind==='conveyor'?null:s.parts.find(p=>p.name===(kind==='tray'?'contact':'channel-lamp'))
14
- const axis=kind==='charger'?'y':'x',extent=kind==='charger'?60:primary.transform.size.x
15
- const margin=kind==='tray'?30:kind==='charger'?160:150
16
- const m={version:'r3-spatial-1',inputs:[{id:'span',unit:'mm',min:0,max:10000},{id:'angle',unit:'deg',min:-180,max:180,role:'state'}],constants:[c('zero',0),c('a0',0,'rad'),c('quarter',90,'deg'),c('negHalf',-.5,'ratio'),c('margin',margin),c('extent',extent),c('pitch',primary.repeat.pitch),c('align',0,'ratio'),c('limit',100,'count'),c('eight',8,'count')],nodes:[]}
17
- const rigid=(id,from,to,xyz,rot=['a0','a0','a0'])=>n(id,'rigid@1',[...xyz,...rot],{pose:id},{from,to})
18
- m.nodes.push(n('half','mul@1',['span','negHalf'],{value:'left'}),rigid('root','item','world',axis==='x'?['left','zero','zero']:['zero','zero','zero']))
19
- m.nodes.push(kind==='tray'?n('layout','fixed-count@1',['eight','span','margin','margin','extent','align','limit'],{layout:'layout'}):n('layout','fit-pitch@1',['span','margin','margin','extent','pitch','align','limit'],{layout:'layout'}))
20
- const members=[]
21
- for(const [index,p] of [primary,secondary].filter(Boolean).entries()){
22
- const label=index?'secondary':'primary',d=p.transform.size,t=p.transform.position
23
- for(const a of ['x','y','z'])m.constants.push(c(`${label}${a}`,t[a]-(a===axis?primary.transform.position[a]:0)))
24
- let shape
25
- if(kind==='conveyor'){
26
- m.constants.push(c('radius',d.x/2),c('length',d.y))
27
- shape=n(label,'cylinder-shape@1',['radius','length'],{shape:`${label}Shape`},{frame:label})
28
- // Rx(90) lays the cylinder along Z; its local Y rotation follows afterwards.
29
- } else {
30
- for(const [key,val] of Object.entries(d))m.constants.push(c(`${label}Size${key}`,val))
31
- shape=n(label,'box-shape@1',['x','y','z'].map(a=>`${label}Size${a}`),{shape:`${label}Shape`},{frame:label})
32
- }
33
- m.nodes.push(shape,rigid(`${label}Pose`,label,'item',['x','y','z'].map(a=>`${label}${a}`),kind==='conveyor'?['quarter','angle','a0']:['a0','a0','a0']),n(`${label}Member`,'member@1',[`${label}Shape`,`${label}Pose`],{placed:`${label}Member`}))
34
- members.push(`${label}Member`)
35
- }
36
- m.nodes.push(n('assembly','assembly@1',members,{assembly:'assembly'}),n('repeat','repeat@1',['layout','assembly','root'],{collection:'collection'},{axis}))
37
- return m
38
- }
39
-
40
- test('three actual-seed-derived assemblies: combined span and spin, no template duplicates',()=>{
41
- let states=0
42
- for(const [kind,spans] of [['tray',[500,600,1200]],['charger',[1000,2800,5600]],['conveyor',[1200,2400,4800]]]){
43
- const p=compile(repeatModel(kind))
44
- for(const span of spans)for(const angle of [-180,-90,0,90,180]){
45
- const r=p.evaluate({span,angle}),count=r.values.layout.count
46
- assert.equal(r.geometry.length,count*(kind==='conveyor'?1:2))
47
- for(let i=0;i<count;i++){
48
- const a=r.geometry.find(g=>g.index===i&&g.key==='primaryMember')
49
- const axis=kind==='charger'?1:0,center=r.values.layout.centers[i]
50
- near(a.pose.t[axis],center+(axis===0?-span/2:0))
51
- const extent=kind==='conveyor'?96:kind==='tray'?52:60,margin=kind==='conveyor'?150:kind==='tray'?30:160
52
- assert.ok(center-extent/2>=margin-1e-6&&center+extent/2<=span-margin+1e-6)
53
- if(kind==='conveyor')near(a.pose.t[1],500)
54
- else {
55
- const b=r.geometry.find(g=>g.index===i&&g.key==='secondaryMember')
56
- near(b.pose.t[axis]-a.pose.t[axis],0)
57
- if(kind==='tray')near(b.pose.t[1]-a.pose.t[1],90)
58
- else near(b.pose.t[2]-a.pose.t[2],295)
59
- }
60
- }
61
- states++
62
- }
63
- }
64
- assert.equal(states,45)
65
- })
66
- test('extent-aware threshold, empty result, limit and fixed-count insufficiency',()=>{
67
- const p=compile(repeatModel('conveyor'))
68
- for(const [span,want] of [[395,0],[396,1],[545.99,1],[546,2],[546.01,2]])assert.equal(p.evaluate({span,angle:0}).geometry.length,want)
69
- const m=repeatModel('conveyor');m.constants.find(c=>c.id==='limit').value=1
70
- assert.throws(()=>compile(m).evaluate({span:546,angle:0}),e=>e.code==='RESOURCE_LIMIT')
71
- assert.throws(()=>compile(repeatModel('tray')).evaluate({span:400,angle:0}),e=>e.code==='NO_SPACE')
72
- })
73
- test('stable keys with grow/shrink/regrow and explicit dangling reference',()=>{
74
- const m=repeatModel('conveyor'),p=compile(m),small=p.evaluate({span:545.99,angle:0}),big=p.evaluate({span:546,angle:0})
75
- assert.equal(small.geometry[0].id,big.geometry[0].id)
76
- assert.deepEqual(p.evaluate({span:546,angle:0}),big)
77
- m.constants.push(c('selected',1,'count'));m.nodes.push(n('select','select-item@1',['collection','selected'],{placed:'selectedItem'},{member:'primaryMember'}))
78
- const withRef=compile(m);withRef.evaluate({span:546,angle:0})
79
- assert.throws(()=>withRef.evaluate({span:545.99,angle:0}),e=>e.code==='TARGET_ABSENT')
80
- })
81
- test('topology state dependency, count type, frame mismatch and cycles reject',()=>{
82
- let m=repeatModel('tray');m.constants.find(c=>c.id==='eight').unit='ratio';assert.throws(()=>compile(m),e=>e.code==='TYPE')
83
- m=repeatModel('tray');m.constants.find(c=>c.id==='eight').value=8.5;assert.throws(()=>compile(m),e=>e.code==='COUNT')
84
- m=repeatModel('charger');m.inputs[0].role='state';assert.throws(()=>compile(m),e=>e.code==='TOPOLOGY_STATE')
85
- m=repeatModel('conveyor');m.nodes.find(n=>n.id==='root').params.from='wrong';assert.throws(()=>compile(m),e=>e.code==='FRAME_TYPE')
86
- m=repeatModel('conveyor');m.nodes.find(n=>n.id==='layout').args[0]='collection';assert.throws(()=>compile(m),e=>e.code==='CYCLE')
87
- })
88
- test('all generated boxes and cylinders are closed oriented meshes; layout snapshots repeat exactly',()=>{
89
- let meshes=0
90
- for(const [kind,span] of [['tray',600],['charger',2800],['conveyor',2400]]){
91
- const p=compile(repeatModel(kind)),r=p.evaluate({span,angle:0})
92
- for(const placed of r.geometry){const g=meshOf(placed),a=meshAudit(g);assert.equal(a.badEdges,0);assert.equal(a.euler,2);assert.ok(a.volume>0&&a.minArea>0);g.dispose();meshes++}
93
- assert.deepEqual(p.evaluate({span,angle:0}),r)
94
- }
95
- console.log({meshes})
96
- })