Skip to content

Commit

Permalink
Add ModuleObj experimental feature (#141)
Browse files Browse the repository at this point in the history
  • Loading branch information
ricktu288 committed May 22, 2024
1 parent ed6ab00 commit 0ecbb23
Show file tree
Hide file tree
Showing 17 changed files with 470 additions and 2 deletions.
1 change: 1 addition & 0 deletions simulator/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -1327,6 +1327,7 @@ <h5 class="modal-title" id="staticBackdropLabel" data-text="language"></h5>

<script src="js/objs/special/Handle.js"></script>
<script src="js/objs/special/CropBox.js"></script>
<script src="js/objs/special/ModuleObj.js"></script>

</body>
</html>
18 changes: 16 additions & 2 deletions simulator/js/Scene.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const DATA_VERSION = 5;
/**
* Represents the scene in this simulator.
* @class Scene
* @property {Object<string,ModuleDef>} modules - The definitions of modules used in the scene.
* @property {Array<BaseSceneObj>} objs - The objects (optical elements and/or decorations created by the user with "Tools") in the scene.
* @property {string} mode - The mode of the scene. Possible values: 'rays' (Rays), 'extended' (Extended Rays), 'images' (All Images), 'observer' (Seen by Observer).
* @property {number} rayModeDensity - The density of rays in 'rays' and 'extended' modes.
Expand All @@ -26,6 +27,7 @@ const DATA_VERSION = 5;
*/
class Scene {
static serializableDefaults = {
modules: {},
objs: [],
mode: 'rays',
rayModeDensity: 0.1,
Expand Down Expand Up @@ -73,9 +75,21 @@ class Scene {
}
}

/** @property {Array<BaseSceneObj>} opticalObjs - The objects in the scene which are optical. If the user edits only the non-optical part of the scene, then the content of this array will not change. */
/** @property {Array<BaseSceneObj>} opticalObjs - The objects in the scene which are optical. Module objects are expanded recursively. If the user edits only the non-optical part of the scene, then the content of this array will not change. */
get opticalObjs() {
return this.objs.filter(obj => obj.constructor.isOptical);
function expandObjs(objs) {
let expandedObjs = [];
for (let obj of objs) {
if (obj.constructor === objTypes['ModuleObj']) {
expandedObjs = expandedObjs.concat(expandObjs(obj.objs));
} else {
expandedObjs.push(obj);
}
}
return expandedObjs;
}

return expandObjs(this.objs).filter(obj => obj.constructor.isOptical);
}

/**
Expand Down
347 changes: 347 additions & 0 deletions simulator/js/objs/special/ModuleObj.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,347 @@
/**
* @typedef {Object} ModuleDef
* @property {Array<string>} params - The parameters of the module.
* @property {Array<Object>} objs - The objects in the module in the form of JSON objects with template syntax.
*/

/**
* The class for a module object.
* This feature is experimental and may be changed in the future without backward compatibility.
* Currently, there is no UI for creating a module object. You can create a module object by directly editing the JSON data of the scene.
* @property {string} module - The name of the module.
* @property {ModuleDef} moduleDef - The definition of the module.
* @property {Array<Point>} points - The control points of the module.
* @property {Object} params - The parameters of the module.
* @property {Array<BaseSceneObj>} objs - The expanded objects in the module.
*/
objTypes['ModuleObj'] = class extends BaseSceneObj {
static type = 'ModuleObj';
static isOptical = true;
static serializableDefaults = {
module: null,
points: [],
params: {}
};

constructor(scene, jsonObj) {
super(scene, jsonObj);

this.moduleDef = this.scene.modules[this.module];
this.objs = [];
this.expandObjs();

console.log(this.moduleDef, this.objs);
}

populateObjBar(objBar) {
objBar.createNote(this.module);

for (let param of this.moduleDef.params) {
const parsed = this.parseVariableRange(param, {});
objBar.createNumber(parsed.name, parsed.start, parsed.end, parsed.step, this.params[parsed.name], function (obj, value) {
obj.params[parsed.name] = value;
obj.expandObjs();
});
}

objBar.createButton(getMsg('demodulize'), function (obj) {
obj.demodulize();
});
}

draw(canvasRenderer, isAboveLight, isHovered) {
// Sort the expanded objects with z-index.
let mapped = this.objs.map(function(obj, i) {
return {index: i, value: obj.getZIndex()};
});
mapped.sort(function(a, b) {
return a.value - b.value;
});
// Draw the expanded objects
for (let j = 0; j < this.objs.length; j++) {
let i = mapped[j].index;
this.objs[i].draw(canvasRenderer, isAboveLight, isHovered);
}

// Draw the control points
for (let point of this.points) {
ctx.beginPath();
ctx.strokeStyle = isHovered ? 'cyan' : ('gray');
ctx.arc(point.x, point.y, 2, 0, Math.PI * 2, false);
ctx.stroke();
ctx.beginPath();
ctx.arc(point.x, point.y, 5, 0, Math.PI * 2, false);
ctx.stroke();
}
}

move(diffX, diffY) {
// Note that translational symmetry is not guaranteed for the module. Some may have absolute positions. So instead of calling `move` of the expanded objects, we move the control points directly to maintain consistency of expansion.

if (this.points.length === 0) {
return;
}

// Move the control points
for (let point of this.points) {
point.x += diffX;
point.y += diffY;
}
this.expandObjs();
}

checkMouseOver(mouse) {
let dragContext = {};

// Check if the mouse is on any control point
let click_lensq = Infinity;
let click_lensq_temp;
let targetPoint_index = -1;
for (var i = 0; i < this.points.length; i++) {
if (mouse.isOnPoint(this.points[i])) {
click_lensq_temp = geometry.distanceSquared(mouse.pos, this.points[i]);
if (click_lensq_temp <= click_lensq) {
click_lensq = click_lensq_temp;
targetPoint_index = i;
}
}
}
if (targetPoint_index != -1) {
dragContext.part = 1;
dragContext.index = targetPoint_index;
dragContext.targetPoint = geometry.point(this.points[targetPoint_index].x, this.points[targetPoint_index].y);
return dragContext;
}

// Check if the mouse is on any expanded object
for (let obj of this.objs) {
let dragContext1 = obj.checkMouseOver(mouse);
if (dragContext1) {
// If the mouse is on any expanded object, then the entire module is considered to be hovered. However, dragging the entire module object is allowed only when there are control points. Otherwise the module is defined with absolute positions and hence cannot be dragged.

if (this.points.length === 0) {
dragContext.part = -1;
if (dragContext1.targetPoint) {
// Here the mouse is on a control point of the expanded object which is not a control point of the module. The user may expect that the control point is draggable but it is not. So we change the cursor to not-allowed to warn the user.
dragContext.cursor = 'not-allowed';
} else {
dragContext.cursor = 'pointer';
}
return dragContext;
} else {
const mousePos = mouse.getPosSnappedToGrid();
dragContext.part = 0;
dragContext.mousePos0 = mousePos; // Mouse position when the user starts dragging
dragContext.mousePos1 = mousePos; // Mouse position at the last moment during dragging
dragContext.snapContext = {};
return dragContext;
}
}
}

return null;
}

onDrag(mouse, dragContext, ctrl, shift) {
const mousePos = mouse.getPosSnappedToGrid();

if (dragContext.part == 1) {
this.points[dragContext.index].x = mousePos.x;
this.points[dragContext.index].y = mousePos.y;
}

if (dragContext.part == 0) {
if (shift) {
var mousePosSnapped = mouse.getPosSnappedToDirection(dragContext.mousePos0, [{ x: 1, y: 0 }, { x: 0, y: 1 }], dragContext.snapContext);
} else {
var mousePosSnapped = mouse.getPosSnappedToGrid();
dragContext.snapContext = {}; // Unlock the dragging direction when the user release the shift key
}
this.move(mousePosSnapped.x - dragContext.mousePos1.x, mousePosSnapped.y - dragContext.mousePos1.y);
dragContext.mousePos1 = mousePosSnapped;
}

this.expandObjs();
}

// Optical methods are not implemented for the module class, since the simulator operates on `scene.opticalObjs` which already expands all the modules.

/* Utility methods */


/**
* Parse the variable range description of the form "name=start:step:end", where start, step, and end are ASCIIMath expressions to be evaluated with the given parameters.
* @param {string} str - The variable range description.
* @param {Object} params - The parameters to be used for evaluating the expressions.
* @returns {Object} The parsed variable range.
*/
parseVariableRange(str, params) {
try {
let parts = str.split('=');
let name = parts[0].trim();
let parts2 = parts[1].split(':');
let start = parts2[0];
let step = parts2[1];
let end = parts2[2];
let startVal = evaluatex(start, params)();
let stepVal = evaluatex(step, params)();
let endVal = evaluatex(end, params)();
return {name: name, start: startVal, step: stepVal, end: endVal};
} catch (e) {
console.log(`Error parsing variable range "${str}" with parameters ${JSON.stringify(params)}: ${e}`);
}
}

/**
* Expand a string with template syntax, where the format "`eqn`" is replaced with the value of "eqn" interpreted as an ASCIIMath expression with a given set of parameters. If the entire string is a single equation, then the result is a number. Otherwise, the result is a string.
* @param {string} str - The string with template syntax.
* @param {Object} params - The parameters to be used for evaluating the expressions.
* @returns {number|string} The expanded string.
*/
expandString(str, params) {
try {
let parts = str.split('`');
if (parts.length == 3 && parts[0] == '' && parts[2] == '') {

return math.evaluate(parts[1], params);
} else {
let result = '';
for (let i = 0; i < parts.length; i++) {
if (i % 2 == 0) {
result += parts[i];
} else {
result += math.evaluate(parts[i], params);
}
}
return result;
}
} catch (e) {
console.log(`Error expanding string "${str}" with parameters ${JSON.stringify(params)}: ${e}`);
}
}

/**
* Expand a (JavaScript) object with template syntax, where the string values of the object are interpreted with template syntax. Arrays and other objects are expanded recursively.
* @param {Object} obj - The object with template syntax.
* @param {Object} params - The parameters to be used for evaluating the expressions.
* @returns {Object} The expanded object.
*/
expandObject(obj, params) {
let result = {};
for (let key in obj) {
if (key === 'for' || key === 'if') {
continue;
} else if (typeof obj[key] === 'string') {
result[key] = this.expandString(obj[key], params);
} else if (Array.isArray(obj[key])) {
result[key] = this.expandArray(obj[key], params);
} else if (typeof obj[key] === 'object') {
result[key] = this.expandObject(obj[key], params);
} else {
result[key] = obj[key];
}
}
return result;
}

/**
* Expand an array with template syntax, where the string values of the array are interpreted with template syntax. Arrays and objects are expanded recursively. If an object in the array has a key "for", then the object is expanded multiple times with the given range of values. If the value of "for" is a string, then the range is interpreted with `parseVariableRange`. If the value of "for" is an array of strings, then each string is witn `parseVariableRange` and there are multiple loop variable. If an object in the array has a key "if", then the object is included only if the condition is true.
* @param {Array} arr - The array with template syntax.
* @param {Object} params - The parameters to be used for evaluating the expressions.
* @returns {Array} The expanded array.
*/
expandArray(arr, params) {
let result = [];
for (let obj of arr) {
try {
if ('for' in obj) {
let forObj = obj['for'];
let loopVars = [];
if (typeof forObj === 'string') {
loopVars.push(this.parseVariableRange(forObj, params));
} else if (Array.isArray(forObj)) {
for (let forObj1 of forObj) {
loopVars.push(this.parseVariableRange(forObj1, params));
}
}

// Expand `loopVars` to a list of objects, each a key-value pair of loop variable names and values (the Cartesian product of the ranges)
function expandLoopVars(loopVars) {
if (loopVars.length == 0) {
return [params];
} else {
let result = [];
let loopVars1 = loopVars.slice(1);
for (let value = loopVars[0].start; value <= loopVars[0].end; value += loopVars[0].step) {
for (let obj of expandLoopVars(loopVars1)) {
let obj1 = Object.assign({}, obj);
obj1[loopVars[0].name] = value;
result.push(obj1);
}
}
return result;
}
}

const loopParams = expandLoopVars(loopVars);
for (let loopParam of loopParams) {
if ('if' in obj && !math.evaluate(obj['if'], loopParam)) {
continue;
}
result.push(this.expandObject(obj, loopParam));
}

} else if ('if' in obj) {
if (math.evaluate(obj['if'], params)) {
result.push(this.expandObject(obj, params));
}
} else if (typeof obj === 'string') {
result.push(this.expandString(obj, params));
} else if (Array.isArray(obj)) {
result.push(this.expandArray(obj, params));
} else if (typeof obj === 'object') {
result.push(this.expandObject(obj, params));
} else {
result.push(obj);
}
} catch (e) {
console.log(`Error expanding object ${JSON.stringify(obj)} in array with parameters ${JSON.stringify(params)}: ${e}`);
}
}
return result;
}

/**
* Expand the objects in the module.
*/
expandObjs() {
// Construct the full parameters including the coordinates of points with names "x_1", "y_1", "x_2", "y_2", ...
const fullParams = {};
for (let name in this.params) {
fullParams[name] = this.params[name];
}
for (let i = 0; i < this.points.length; i++) {
fullParams['x_' + (i+1)] = this.points[i].x;
fullParams['y_' + (i+1)] = this.points[i].y;
}


const expandedObjs = this.expandArray(this.moduleDef.objs, fullParams);

this.objs = expandedObjs.map(objData =>
new objTypes[objData.type](this.scene, objData)
);
}

/**
* Demodulize the module object.
*/
demodulize() {
// Remove the module object and add the expanded objects to the scene
removeObj(this.scene.objs.indexOf(this));
for (let obj of this.objs) {
this.scene.objs.push(obj);
}
draw(false, true);
}
}
Loading

0 comments on commit 0ecbb23

Please sign in to comment.