Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for field restrictions to web proto #5720

Merged
merged 16 commits into from
Jan 9, 2023
127 changes: 117 additions & 10 deletions resources/web/wwi/FloatingProtoParameterWindow.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,12 @@ export default class FloatingProtoParameterWindow extends FloatingWindow {

this.fieldsToExport = new Map();

this.unsupportedRestrictions = [
VRML.SFBool, VRML.SFNode, VRML.MFNode, VRML.MFBool,
VRML.MFString, VRML.MFInt32, VRML.MFFloat, VRML.MFVec2f,
VRML.MFVec3f, VRML.MFColor, VRML.MFRotation
];

// create tabs
const infoTabsBar = document.createElement('div');
infoTabsBar.className = 'proto-tabs-bar';
Expand Down Expand Up @@ -134,7 +140,10 @@ export default class FloatingProtoParameterWindow extends FloatingWindow {
const keys = this.proto.parameters.keys();
for (let key of keys) {
const parameter = this.proto.parameters.get(key);
if (parameter.type === VRML.SFVec3f)

if (parameter.restrictions.length > 0 && !this.unsupportedRestrictions.includes(parameter.type))
this.#createRestrictedField(key, contentDiv);
else if (parameter.type === VRML.SFVec3f)
this.#createSFVec3Field(key, contentDiv);
else if (parameter.type === VRML.SFRotation)
this.#createSFRotation(key, contentDiv);
Expand Down Expand Up @@ -189,6 +198,17 @@ export default class FloatingProtoParameterWindow extends FloatingWindow {
currentNodeButton.innerHTML = parameter.value.value.name;
}
}

// note: SFNode/MFNode rows don't need refresh since the restriction and its handling is done in the node selector window
if (parameter.restrictions.length > 0 && ![VRML.SFNode, VRML.MFNode].includes(parameter.type)) {
const select = document.getElementById('select-' + parameter.name);
for (const [i, restriction] of parameter.restrictions.entries()) {
if (parameter.value && restriction.equals(parameter.value)) {
select.options[i].selected = true;
break;
}
}
}
}

#disableResetButton(resetButton) {
Expand Down Expand Up @@ -297,7 +317,7 @@ export default class FloatingProtoParameterWindow extends FloatingWindow {
}

#createMFVec3fField(key, parent) {
const parameter = this.#protoManager.exposedParameters.get(key);
const parameter = this.proto.parameters.get(key);
const p = document.createElement('p');
p.innerHTML = key + ': ';
p.key = key;
Expand All @@ -306,8 +326,11 @@ export default class FloatingProtoParameterWindow extends FloatingWindow {
p.style.gridColumn = '2 / 2';
p.className = 'key-parameter';

const exportCheckbox = this.#createCheckbox(parent, key);
p.checkbox = exportCheckbox;
if (this.proto.isRoot) {
const exportCheckbox = this.#createCheckbox(parent, key);
p.checkbox = exportCheckbox;
} else
p.style.marginLeft = '20px';
BenjaminDeleze marked this conversation as resolved.
Show resolved Hide resolved

const hideShowButton = document.createElement('button');
hideShowButton.style.gridRow = '' + this.#rowNumber + ' / ' + this.#rowNumber;
Expand Down Expand Up @@ -589,6 +612,84 @@ export default class FloatingProtoParameterWindow extends FloatingWindow {
this.#refreshParameterRow(node.parameter);
}

#createRestrictedField(key, parent) {
const parameter = this.proto.parameters.get(key);

const p = document.createElement('p');
p.innerHTML = key + ': ';
p.parameter = parameter;
p.key = key;
p.style.gridRow = '' + this.#rowNumber + ' / ' + this.#rowNumber;
p.style.gridColumn = '2 / 2';
p.className = 'key-parameter';

if (this.proto.isRoot) {
const exportCheckbox = this.#createCheckbox(parent, key);
p.checkbox = exportCheckbox;
} else
p.style.marginLeft = '20px';

const value = document.createElement('p');
value.style.gridRow = '' + this.#rowNumber + ' / ' + this.#rowNumber;
value.style.gridColumn = '4 / 4';
value.className = 'value-parameter';

const select = document.createElement('select');
select.id = 'select-' + parameter.name;
select.parameter = parameter;

for (const item of parameter.restrictions) {
let value;
switch(parameter.type) {
case VRML.SFString:
value = this.#stringRemoveQuote(item.value);
break;
case VRML.SFFloat:
case VRML.SFInt32:
value = item.value;
break;
case VRML.SFVec2f:
case VRML.SFVec3f:
case VRML.SFColor:
case VRML.SFRotation:
value = item.toVrml(); // does what is needed, useless to create another ad-hoc method
break;
default:
throw new Error('Unsupported parameter type: ', parameter.type);
}

const option = document.createElement('option');
option.value = value;
option.innerText = value;
if (parameter.value && item.equals(parameter.value))
option.selected = true;

select.appendChild(option);
}

select.onchange = (e) => {
const parameter = e.target.parameter;
const selectionIndex = e.target.selectedIndex;
parameter.setValueFromJavaScript(this.#view, parameter.restrictions[selectionIndex].toJS(false));
this.#refreshParameterRow(parameter);
};
value.appendChild(select);
p.input = select;

const resetButton = this.#createResetButton(parent, p.style.gridRow, parameter.name);
this.#disableResetButton(resetButton);
resetButton.onclick = () => {
// we can use stringify because SFNodes/MFNodes are handled separately (through the node selection window)
parameter.setValueFromJavaScript(this.#view, JSON.parse(JSON.stringify(parameter.defaultValue.value)));
this.#refreshParameterRow(parameter);
};

parent.appendChild(p);
parent.appendChild(value);

this.#refreshParameterRow(parameter);
}

#createSFStringField(key, parent) {
const parameter = this.proto.parameters.get(key);

Expand Down Expand Up @@ -797,7 +898,7 @@ export default class FloatingProtoParameterWindow extends FloatingWindow {
}

#createSFInt32Field(key, parent) {
const parameter = this.#protoManager.exposedParameters.get(key);
const parameter = this.proto.parameters.get(key);

const p = document.createElement('p');
p.className = 'key-parameter';
Expand All @@ -807,7 +908,11 @@ export default class FloatingProtoParameterWindow extends FloatingWindow {
p.style.gridRow = '' + this.#rowNumber + ' / ' + this.#rowNumber;
p.style.gridColumn = '2 / 2';

const exportCheckbox = this.#createCheckbox(parent, key);
if (this.proto.isRoot) {
const exportCheckbox = this.#createCheckbox(parent, key);
p.checkbox = exportCheckbox;
} else
p.style.marginLeft = '20px';

const value = document.createElement('p');
value.className = 'value-parameter';
Expand All @@ -823,7 +928,6 @@ export default class FloatingProtoParameterWindow extends FloatingWindow {
input.oninput = () => this.#intOnChange(p);
input.onchange = () => this.#floatOnChange(p);
p.input = input;
p.checkbox = exportCheckbox;
value.appendChild(input);

const resetButton = this.#createResetButton(parent, p.style.gridRow, parameter.name);
Expand All @@ -845,7 +949,7 @@ export default class FloatingProtoParameterWindow extends FloatingWindow {
}

#createSFBoolField(key, parent) {
const parameter = this.#protoManager.exposedParameters.get(key);
const parameter = this.proto.parameters.get(key);

const p = document.createElement('p');
p.className = 'key-parameter';
Expand All @@ -855,7 +959,11 @@ export default class FloatingProtoParameterWindow extends FloatingWindow {
p.style.gridRow = '' + this.#rowNumber + ' / ' + this.#rowNumber;
p.style.gridColumn = '2 / 2';

const exportCheckbox = this.#createCheckbox(parent, key);
if (this.proto.isRoot) {
const exportCheckbox = this.#createCheckbox(parent, key);
p.checkbox = exportCheckbox;
} else
p.style.marginLeft = '20px';

const value = document.createElement('p');
value.className = 'value-parameter';
Expand All @@ -868,7 +976,6 @@ export default class FloatingProtoParameterWindow extends FloatingWindow {
input.className = 'bool-field';

p.input = input;
p.checkbox = exportCheckbox;
value.appendChild(input);

const boolText = document.createElement('span');
Expand Down
16 changes: 16 additions & 0 deletions resources/web/wwi/NodeSelectorWindow.js
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,10 @@ export default class NodeSelectorWindow {

const compatibleNodes = [];
for (const [name, info] of this.nodes) {
// filter nodes based on restrictions
if (!this.doFieldRestrictionsAllowNode(name))
continue;

// filter incompatible nodes
if (typeof info.tags !== 'undefined' && (info.tags.includes('hidden') || info.tags.includes('deprecated')))
continue;
Expand Down Expand Up @@ -308,6 +312,18 @@ export default class NodeSelectorWindow {
this.hide();
}

doFieldRestrictionsAllowNode(nodeName) {
if (this.parameter.restrictions.length === 0)
return true;

for (const restriction of this.parameter.restrictions) {
if (nodeName === restriction.value.name)
return true;
}

return false;
}

isAllowedToInsert(baseType, slotType) {
if (typeof this.parameter === 'undefined')
throw new Error('The parameter is expected to be defined prior to checking node compatibility.');
Expand Down
21 changes: 14 additions & 7 deletions resources/web/wwi/protoVisualizer/Node.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export default class Node {
const defaultValue = vrmlFactory(type);
defaultValue.setValueFromJavaScript(FieldModel[this.name][parameterName]['defaultValue']);
const value = defaultValue.clone(true);
const parameter = new Parameter(this, parameterName, type, defaultValue, value, false);
const parameter = new Parameter(this, parameterName, type, [], defaultValue, value, false);
// console.log(parameterName + ' has parent ' + this.name);
this.parameters.set(parameterName, parameter);
}
Expand Down Expand Up @@ -177,11 +177,18 @@ export default class Node {
const token = headTokenizer.nextToken();
let nextToken = headTokenizer.peekToken();

const restrictions = [];
if (token.isKeyword() && nextToken.isPunctuation()) {
if (nextToken.word() === '{') {
// TODO: field restrictions are not supported yet, consume the tokens
headTokenizer.consumeTokensByType(VRML.SFNode);
nextToken = headTokenizer.peekToken(); // update upcoming token reference after consumption
// parse field restrictions
headTokenizer.skipToken('{');
const parameterType = token.fieldTypeFromVrml();
while (headTokenizer.peekWord() !== '}') {
const value = vrmlFactory(parameterType, headTokenizer);
restrictions.push(value);
}
headTokenizer.skipToken('}');
nextToken = headTokenizer.peekToken(); // we need to update the nextToken as it has to point after the restrictions
}
}

Expand All @@ -191,10 +198,10 @@ export default class Node {
const isRegenerator = this.isTemplate ? (this.rawBody.search('fields.' + parameterName + '.') !== -1) : false;
headTokenizer.nextToken(); // consume the token containing the parameter name

// console.log('INTERFACE PARAMETER ' + parameterName + ', TYPE: ' + parameterType + ', VALUE:');
// console.log('INTERFACE PARAMETER ' + parameterName + ', TYPE: ' + parameterType);
const defaultValue = vrmlFactory(parameterType, headTokenizer);
const value = defaultValue.clone(true);
const parameter = new Parameter(this, parameterName, parameterType, defaultValue, value, isRegenerator);
const parameter = new Parameter(this, parameterName, parameterType, restrictions, defaultValue, value, isRegenerator);
// console.log(parameterName + ' has parent ' + this.name);
this.parameters.set(parameterName, parameter);
}
Expand Down Expand Up @@ -319,7 +326,7 @@ export default class Node {

regenerateBodyVrml() {
const fieldsEncoding = this.toJS(true); // make current proto parameters in a format compliant to template engine
// console.log(fieldsEncoding);
// console.log('Encoded fields:', fieldsEncoding);

if (typeof this.templateEngine === 'undefined')
throw new Error('Regeneration was called but the template engine is not defined (i.e this.isTemplate is false)');
Expand Down
37 changes: 32 additions & 5 deletions resources/web/wwi/protoVisualizer/Parameter.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,27 @@ export default class Parameter {
#value;
#defaultValue;
#isTemplateRegenerator;
#restrictions;
#parameterLinks;
constructor(node, name, type, defaultValue, value, isTemplateRegenerator) {
constructor(node, name, type, restrictions, defaultValue, value, isTemplateRegenerator) {
this.node = node; // node this parameter belongs to
this.type = type;
this.name = name;
this.#type = type;
this.#name = name;
this.#restrictions = restrictions;
this.defaultValue = defaultValue;
this.value = value;
this.isTemplateRegenerator = isTemplateRegenerator;
this.#isTemplateRegenerator = isTemplateRegenerator;
this.#parameterLinks = []; // list of other parameters to notify whenever this instance changes
}

get restrictions() {
return this.#restrictions;
}

set restrictions(newValue) {
this.#restrictions = newValue;
}

get value() {
return this.#value;
}
Expand All @@ -28,6 +38,17 @@ export default class Parameter {
if (v.type() !== this.type)
throw new Error('Type mismatch, setting ' + stringifyType(v.type()) + ' to ' + stringifyType(this.type) + ' parameter.');

if (this.restrictions.length > 0) {
for (const item of this.restrictions) {
if ((v instanceof SFNode && v.value === null) || item.equals(v)) {
this.#value = v;
return;
}
}

throw new Error('Parameter ' + this.name + ' is restricted and the value being set is not permitted.');
}

this.#value = v;
}

Expand Down Expand Up @@ -141,8 +162,14 @@ export default class Parameter {
}

clone(deep = false) {
const copy = new Parameter(this.node, this.name, this.type, deep ? this.defaultValue.clone(deep) : this.defaultValue,
const restrictions = [];
for (const item of this.restrictions)
restrictions.push(item.clone(deep));

const copy = new Parameter(this.node, this.name, this.type, restrictions,
deep ? this.defaultValue.clone(deep) : this.defaultValue,
deep ? this.value.clone(deep) : this.value, this.isTemplateRegenerator);

return copy;
}
}
Expand Down
Loading