Skip to content

Commit cc822d1

Browse files
committed
Added Component test with local script file. Fixed some bugs in parameter handling
1 parent 5aef8b3 commit cc822d1

File tree

8 files changed

+138
-21
lines changed

8 files changed

+138
-21
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,4 @@ Thumbs.db
1313
dist/**/*
1414
yarn.lock
1515
coverage/
16+
tests/*.glb

src/ParamManager.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@
2828
*
2929
*/
3030

31-
import { ScriptParam, ScriptParamData, ParamOperation, ParamManagerOperator } from './internal'
31+
import type { ScriptParamData } from './internal'
32+
import { ScriptParam, ParamOperation, ParamManagerOperator } from './internal'
3233

3334
import deepEqual from 'deep-is'
3435

@@ -45,13 +46,18 @@ export class ParamManager
4546
paramOperators:Array<ParamManagerOperator> = [];
4647

4748
/** Set up ParamManager with current params */
48-
constructor(params?:Array<ScriptParam>)
49+
constructor(params?:Array<ScriptParam|ScriptParamData>)
4950
{
5051
if(Array.isArray(params) && params.length > 0)
5152
{
5253
//this.paramOperators = params.map(p => new ParamManagerOperator(this, this._validateParam(ScriptParamToParam(p)))); // always make sure we use Param internally
5354
// Disable validation because its old
54-
this.paramOperators = params.map(p => new ParamManagerOperator(this, p)); // always make sure we use Param internally
55+
this.paramOperators = params
56+
.map(
57+
p => {
58+
const paramDef = (p instanceof ScriptParam) ? p : new ScriptParam().fromData(p);
59+
return new ParamManagerOperator(this, paramDef); // always make sure we use Param internally
60+
});
5561
this.paramOperators.forEach( p => this[p.name] = p) // set param access
5662
}
5763
}

src/Runner.ts

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
import type { ArchiyouApp, ExportGLTFOptions, Statement,
1717
StatementResult, RunnerScriptScopeState, ScriptOutputPath, ScriptOutputFormatModel,
1818
ScriptOutputFormat, ScriptOutputData, DocData,
19-
ConsoleMessageType,
19+
ConsoleMessageType, ScriptData
2020
} from "./internal"
2121

2222

@@ -25,7 +25,7 @@ import { OcLoader, Console, Brep, Doc, Calc, Exporter, Services, Make, Db,
2525
RunnerScriptExecutionResult, Script, CodeParser, LibraryConnector,
2626
ScriptOutputManager, Pipeline } from "./internal"
2727

28-
import { Point, Vector, Bbox, Edge, Vertex, Wire, Face, Shell, Solid, ShapeCollection, Obj, ParamManager } from "./internal"
28+
import { Point, Vector, Bbox, Edge, Vertex, Wire, Face, Shell, Solid, ShapeCollection, Obj, ScriptParam, ParamManager } from "./internal"
2929

3030
import { RunnerComponentImporter } from "./internal"
3131

@@ -1131,9 +1131,15 @@ ${e.message === '***** CODE ****\nUnexpected end of input' ? code : ''}
11311131
_executionStartRunInScope(scope:any, request: RunnerScriptExecutionRequest):void
11321132
{
11331133
// Setup ParamManager - it still uses Array of Param defintions with _value
1134-
const paramDefsWithValues = [...Object.values(request.script.params || {})]
1135-
paramDefsWithValues.forEach(p => p._value = request.params[p.name]);
1134+
const paramDefsWithValues = [...Object.values(request.script.params || {})] as Array<ScriptParam|ScriptParamData>; // param names are uppercase
1135+
paramDefsWithValues.forEach(pd =>
1136+
{
1137+
const n = Object.keys(request.params || {}).find( p => p.toLowerCase() === pd.name.toLowerCase())
1138+
pd._value = request.params[n];
1139+
});
1140+
11361141
console.info(`Runner::_executionStartRunInScope()[in execution context]: Setting up ParamManager in scope with params "${JSON.stringify(paramDefsWithValues)}"`);
1142+
11371143
scope.ay.paramManager = new ParamManager(paramDefsWithValues)
11381144
.setParent(scope); // sets globals in method setParent()
11391145
scope.$PARAMS = scope.ay.paramManager;
@@ -1375,14 +1381,30 @@ ${e.message === '***** CODE ****\nUnexpected end of input' ? code : ''}
13751381
// Load dynamically to avoid issues in browser
13761382
const FS_PROMISES_LIB = 'fs/promises'; // avoid problems with older build systems preparsing import statement
13771383
const fs = await import(FS_PROMISES_LIB); // use promises version of fs
1378-
const data = await fs.readFile(path, 'utf-8');
1384+
const pathLib = await import('path');
1385+
1386+
// NOTE: absolute paths are recommended - otherwise we take the working directory as root
1387+
// There is no way to get the main script from here
1388+
if(path[0] === '.')
1389+
{
1390+
path = pathLib.resolve(process.cwd(), path);
1391+
console.warn(`$component("${path}")::_prepareComponentScript(): Resolved relative path using current working to: "${path}"`);
1392+
}
13791393

1380-
if(!data)
1394+
try {
1395+
// Scripts are modules (not JSON)
1396+
const data:ScriptData = (await import(path))?.default;
1397+
1398+
if(!data){ throw new Error(`$component("${path}")::_prepareComponentScript(): Cannot read component script from file "${path}". File is empty!`);}
1399+
1400+
const componentScript = new Script().fromData(data);
1401+
return componentScript;
1402+
}
1403+
catch(e)
13811404
{
1382-
throw new Error(`$component("${path}")::_prepareComponentScript(): Cannot read component script from file "${path}". File not found or empty!`);
1405+
throw new Error(`$component("${path}")::_prepareComponentScript(): Cannot read component script from file "${path}". File not found`);
13831406
}
1384-
const componentScript = new Script().fromData(data);
1385-
return componentScript;
1407+
13861408
}
13871409
// path in format like 'archiyou/testcomponent:0.5' or 'archiyou/testcomponent' (default library)
13881410
// or externally: 'pubv2.archiyou.com/archiyou/myscript:1.0'

src/RunnerComponentImporter.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,7 @@ export class RunnerComponentImporter
217217
const request:RunnerScriptExecutionRequest = {
218218
script: script,
219219
component: this.label, // scope identifier
220-
params: this.params,
220+
params: this._params,
221221
outputs: (this._requestedOutputs.length === 0) ? this.DEFAULT_OUTPUTS : this._requestedOutputs,
222222
};
223223

@@ -261,7 +261,9 @@ export class RunnerComponentImporter
261261
{
262262
console.info(`$component("${this.label}")::_executeComponentScript(): Recreating component Obj tree in main scope for pipeline "${pl}"...`);
263263
const recreatedObj = this._recreateComponentObjTree(outPathObj._output as Object);
264-
pipelineResult['model'] = recreatedObj.shapes(true); // result is ShapeCollection of all shapes in the Obj tree
264+
// result is ShapeCollection of all shapes in the Obj tree
265+
pipelineResult['model'] = recreatedObj.shapes(true); // NOTE: only visible shapes, filted out in _recreateComponentObjTree
266+
265267
console.info(`$component("${this.label}")::_executeComponentScript(): Recreated component Obj tree in main scope for pipeline "${pl}".`);
266268
}
267269
});
@@ -293,16 +295,19 @@ export class RunnerComponentImporter
293295
}
294296

295297
/** We need to recreate the component object tree into the current scope */
296-
_recreateComponentObjTree(tree:Object, parentObj?:Obj):Obj
298+
_recreateComponentObjTree(tree:Object, parentObj?:Obj, onlyVisible:boolean=true):Obj
297299
{
298300
console.info(`$component("${this.label}")::_recreateComponentObjTree(): Recreating component object tree...`);
299301
const mainScope = this._scope;
300302

301303
const curNode = tree as Object as any; // TODO: TS typing
302304
const newObj = new mainScope.Obj(); // create Geom Obj container
303305
newObj.name((curNode as any).name);
306+
307+
const curNodeVisibleShapes = onlyVisible ? curNode.shapes.filter(s => s.visible()) : curNode.shapes;
308+
304309
// IMPORTANT: Shapes are still tied to Geom of component scope - change that
305-
newObj._updateShapes(curNode.shapes.map(s => { s._brep = mainScope.brep; return s; }));
310+
newObj._updateShapes(curNodeVisibleShapes.map(s => { s._brep = mainScope.brep; return s; }));
306311

307312
console.info(`$component("${this.label}")::_recreateComponentObjTree(): Recreated object "${newObj.name()}" with ${newObj.shapes(false).length} shapes`);
308313

src/Script.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ export class Script
9797

9898
_validateBasics():boolean
9999
{
100+
// TODO: use a validation library at the start
100101
const VALIDATIONS = {
101102
id: this.id && typeof this.id === "string", // should always be a string
102103
name : !this.name || (typeof this.name === "string" && this.name.length > 0), // can be undefined, but accept only strings
@@ -115,6 +116,7 @@ export class Script
115116
{
116117
const firstErrorIndex = Object.values(VALIDATIONS).findIndex(v => v !== true);
117118
console.error(`Script._validateBasics(): Basic validation failed: ${Object.keys(VALIDATIONS)[firstErrorIndex]}`);
119+
console.error(`Incoming data: ${JSON.stringify(this[Object.keys(VALIDATIONS)[firstErrorIndex]])}`);
118120
}
119121

120122
return this._valid = isValidBasic && this._validatePublished();
@@ -301,7 +303,7 @@ export class Script
301303
return Infinity;
302304
}
303305

304-
return Object.values(this.params).reduce((acc, paraissm) => {
306+
return Object.values(this.params).reduce((acc, param) => {
305307
acc *= param.numValues();
306308
return acc;
307309
}, 1);
@@ -367,6 +369,7 @@ export class Script
367369

368370
/** Load from raw data
369371
* Some backwards compatibility
372+
* TODO: Make this a static method
370373
*/
371374
fromData(data:Script|ScriptData|Record<string, any>):Script|this
372375
{

tests/unit/Runner.components.test.ts

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Runner } from '../../src/internal'
1+
import { Runner, RunnerOps } from '../../src/internal'
22
import type { ScriptData } from '../../src/internal'
33

44
import { describe, it, beforeAll, expect } from 'vitest'
@@ -15,7 +15,7 @@ describe('Runner with components', () =>
1515
});
1616

1717
/*
18-
it('it should execute a script with a component script define by inline code',
18+
it('should execute a script with a component script define by inline code',
1919
async () =>
2020
{
2121
const mainScript = {
@@ -45,7 +45,43 @@ describe('Runner with components', () =>
4545
);
4646
*/
4747

48-
it('it should handle recursive component scripts', async () => {
48+
it('should load a local script file as component', async () => {
49+
50+
// from working dir
51+
const LOCAL_SCRIPT_PATH = './tests/unit/RunnerComponent.js'
52+
53+
const mainScript = {
54+
code : `
55+
// Make a wall
56+
wall = boxbetween([0,0,0], [4000, 200, 3000]) // leftbottom at center
57+
.move(0,-100); // align with X axis
58+
59+
// Place Frame as component
60+
frame = $component('${LOCAL_SCRIPT_PATH}')
61+
.params({ width: 1000, height: 500, depth: 300 })
62+
.model();
63+
frame.moveTo(0, 0, 0) // center at origin
64+
.move(2000, 0, 1500);
65+
66+
// subtract frame from wall
67+
wall.subtract(frame.bbox().shape());
68+
69+
print(frame.length); // ==> 1
70+
`
71+
} as ScriptData;
72+
73+
const r = await runner.execute({ script: mainScript });
74+
expect(r).toBeDefined();
75+
expect(r.status).toBe('success');
76+
expect(r?.messages?.filter(m => m.type === 'user')[0].message).toBe('1');
77+
expect((r?.outputs?.[0].output as ArrayBuffer)?.byteLength).toBeGreaterThan(30000); // 30508 (but some small differences)
78+
79+
// for visible check: save glb to ./tests/unit/RunnerComponent.glb
80+
new RunnerOps().saveBlobToFile(r?.outputs?.[0]?.output as any, './tests/unit/RunnerComponent.glb');
81+
})
82+
83+
/*
84+
it('should handle recursive component scripts', async () => {
4985
// NOTE: make sure you nest backticks correctly or avoid!
5086
const mainScript = {
5187
code: `
@@ -73,6 +109,6 @@ describe('Runner with components', () =>
73109
expect(r?.messages?.filter(m => m.type === 'user')[0].message).toBe('4');
74110
expect((r?.outputs?.[0].output as ArrayBuffer)?.byteLength).toBeGreaterThan(69000); // can vary
75111
});
76-
112+
*/
77113

78114
})

tests/unit/RunnerComponent.glb

32.3 KB
Binary file not shown.

tests/unit/RunnerComponent.js

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// used with Runner.components.test.ts
2+
export default {
3+
id : "Frame",
4+
name: "Frame",
5+
author: "test",
6+
params: {
7+
width: {
8+
type: "number",
9+
default: 1000,
10+
min: 300,
11+
max: 2000
12+
},
13+
height: {
14+
type: "number",
15+
default: 1000,
16+
min: 300,
17+
max: 2000
18+
},
19+
depth: {
20+
type: "number",
21+
default: 200,
22+
min: 100,
23+
max: 500
24+
}
25+
},
26+
code: `
27+
// Create the simple frame
28+
frameOuter = line([0,0,0],[$WIDTH,0,0])
29+
.hide()
30+
.extruded($HEIGHT, [0,0,1])
31+
.hide();
32+
33+
frameInner = frameOuter.offsetted(-50).name('h');
34+
frame = frameOuter
35+
.subtract(frameInner.hide())
36+
.extruded($DEPTH)
37+
.color('blue')
38+
39+
`,
40+
published: {
41+
version: "0.1",
42+
}
43+
44+
}

0 commit comments

Comments
 (0)