@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.
- package/CHANGELOG.md +49 -0
- package/README.md +8 -4
- package/dist/asset-types.d.ts +8 -13
- package/dist/asset-types.d.ts.map +1 -1
- package/dist/asset-types.js.map +1 -1
- package/dist/asset.js +4 -4
- package/dist/asset.js.map +1 -1
- package/dist/authoring-actions.js +18 -5
- package/dist/authoring-actions.js.map +1 -1
- package/dist/authoring-types.d.ts +6 -11
- package/dist/authoring-types.d.ts.map +1 -1
- package/dist/authoring-types.js.map +1 -1
- package/dist/editor-workspace.d.ts +1 -1
- package/dist/editor-workspace.d.ts.map +1 -1
- package/dist/editor-workspace.js +13 -6
- package/dist/editor-workspace.js.map +1 -1
- package/dist/extensions.d.ts +15 -0
- package/dist/extensions.d.ts.map +1 -0
- package/dist/extensions.js +63 -0
- package/dist/extensions.js.map +1 -0
- package/dist/gate.d.ts +1 -1
- package/dist/gate.d.ts.map +1 -1
- package/dist/gate.js +28 -33
- package/dist/gate.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/input-preview.d.ts +48 -0
- package/dist/input-preview.d.ts.map +1 -0
- package/dist/{authoring-session.js → input-preview.js} +7 -63
- package/dist/input-preview.js.map +1 -0
- package/dist/kernel-version.d.ts +1 -1
- package/dist/kernel-version.js +1 -1
- package/dist/names.d.ts.map +1 -1
- package/dist/names.js +5 -1
- package/dist/names.js.map +1 -1
- package/dist/package.d.ts.map +1 -1
- package/dist/package.js +18 -15
- package/dist/package.js.map +1 -1
- package/dist/proposal.d.ts +5 -1
- package/dist/proposal.d.ts.map +1 -1
- package/dist/proposal.js +18 -5
- package/dist/proposal.js.map +1 -1
- package/dist/release-contract.d.ts +1 -1
- package/dist/release-contract.d.ts.map +1 -1
- package/dist/release-contract.js +13 -24
- package/dist/release-contract.js.map +1 -1
- package/dist/shape-edit.d.ts +1 -1
- package/dist/shape-edit.d.ts.map +1 -1
- package/dist/size-edit.d.ts +1 -1
- package/dist/size-edit.d.ts.map +1 -1
- package/docs/asset-persistence.md +6 -3
- package/docs/authoring-transactions.md +21 -19
- package/docs/capability-bindings.md +6 -3
- package/docs/common-kernel-experiment.md +3 -0
- package/docs/document-contract.md +6 -3
- package/docs/dryroom-profile-validation.md +3 -0
- package/docs/editor-integration.md +5 -2
- package/docs/full-conveyor-validation.md +5 -2
- package/docs/full-dryroom-validation.md +6 -3
- package/docs/full-forklift-validation.md +5 -2
- package/docs/full-robot-validation.md +5 -2
- package/docs/payload-collision-validation.md +5 -2
- package/docs/payload-contact-validation.md +5 -2
- package/docs/prototypes/README.md +27 -61
- package/docs/repeated-binding-validation.md +6 -3
- package/docs/review-2026-09-22.md +3 -0
- package/docs/runtime-extraction.md +9 -2
- package/docs/sample-validation-expanded.md +3 -0
- package/docs/sample-validation.md +3 -0
- package/docs/shape-contract.md +7 -5
- package/docs/spatial-contracts.md +3 -0
- package/docs/status.md +2 -1
- package/docs/storage-integration.md +6 -3
- package/docs/typed-repeat-contracts.md +3 -0
- package/package.json +9 -9
- package/dist/authoring-session.d.ts +0 -121
- package/dist/authoring-session.d.ts.map +0 -1
- package/dist/authoring-session.js.map +0 -1
- package/dist/carrier.d.ts +0 -23
- package/dist/carrier.d.ts.map +0 -1
- package/dist/carrier.js +0 -65
- package/dist/carrier.js.map +0 -1
- package/docs/prototypes/asset-samples.mjs +0 -15
- package/docs/prototypes/asset-store.mjs +0 -52
- package/docs/prototypes/asset-store.test.mjs +0 -59
- package/docs/prototypes/asset.mjs +0 -1
- package/docs/prototypes/asset.schema.json +0 -36
- package/docs/prototypes/asset.test.mjs +0 -68
- package/docs/prototypes/authoring-session.mjs +0 -1
- package/docs/prototypes/authoring-session.test.mjs +0 -65
- package/docs/prototypes/capability-plan.mjs +0 -1
- package/docs/prototypes/capability-plan.test.mjs +0 -56
- package/docs/prototypes/capability-samples.mjs +0 -23
- package/docs/prototypes/document.mjs +0 -2
- package/docs/prototypes/document.schema.json +0 -66
- package/docs/prototypes/document.test.mjs +0 -55
- package/docs/prototypes/dryroom-mesh.mjs +0 -60
- package/docs/prototypes/dryroom-mesh.test.mjs +0 -30
- package/docs/prototypes/editor-browser.mjs +0 -97
- package/docs/prototypes/editor-workspace.mjs +0 -1
- package/docs/prototypes/editor-workspace.test.mjs +0 -35
- package/docs/prototypes/feature-pattern.test.mjs +0 -131
- package/docs/prototypes/full-conveyor.mjs +0 -60
- package/docs/prototypes/full-conveyor.test.mjs +0 -61
- package/docs/prototypes/full-dryroom.mjs +0 -67
- package/docs/prototypes/full-dryroom.test.mjs +0 -54
- package/docs/prototypes/full-forklift.mjs +0 -40
- package/docs/prototypes/full-forklift.test.mjs +0 -37
- package/docs/prototypes/full-robot.mjs +0 -35
- package/docs/prototypes/full-robot.test.mjs +0 -68
- package/docs/prototypes/kernel.mjs +0 -147
- package/docs/prototypes/kernel.test.mjs +0 -144
- package/docs/prototypes/more-samples.test.mjs +0 -182
- package/docs/prototypes/payload-collision.mjs +0 -54
- package/docs/prototypes/payload-collision.test.mjs +0 -68
- package/docs/prototypes/payload-fit.mjs +0 -58
- package/docs/prototypes/payload-fit.test.mjs +0 -59
- package/docs/prototypes/render-dryroom.py +0 -60
- package/docs/prototypes/render-full-dryroom.mjs +0 -86
- package/docs/prototypes/repeated-binding.mjs +0 -13
- package/docs/prototypes/repeated-binding.test.mjs +0 -46
- package/docs/prototypes/runtime-browser.mjs +0 -76
- package/docs/prototypes/samples.test.mjs +0 -144
- package/docs/prototypes/spatial-plan.test.mjs +0 -102
- 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&¢er+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
|
-
})
|