sunrize
Version:
Sunrize — A Multi-Platform X3D Editor
1,006 lines (832 loc) • 30.8 kB
JavaScript
const
$ = require ("jquery"),
electron = require ("electron"),
TweakPane = require ("tweakpane"),
Interface = require ("../Application/Interface"),
X3D = require ("../X3D"),
Editor = require ("../Undo/Editor"),
X3DUOM = require ("../Bits/X3DUOM"),
util = require ("util"),
_ = require ("../Application/GetText");
module .exports = new class Panel extends Interface
{
constructor ()
{
super ("Sunrize.Panel.");
this .pane = new TweakPane .Pane ();
this .container = $(this .pane .element) .parent ();
this .selection = require ("../Application/Selection");
this .container .hide () .appendTo ($("#browser-frame"));
this .container .css ({
"z-index": "3000",
"overflow": "unset",
"width": "fit-content",
"bottom": this .container .css ("top"),
"top": "unset",
});
this .container .get (0) .addEventListener ("mousedown", event => this .onmousedown (event), true);
this .browser .getBrowserOptions () ._ColorSpace .addFieldCallback ("Panel", () => this .updateNode ());
this .setup ();
}
configure ()
{
this .container .css ({
"right": `${this .config .file .right}px`,
"bottom": `${this .config .file .bottom}px`,
});
}
get visible ()
{
return this .container .is (":visible");
}
show ()
{
this .selection .addInterest (this, () => this .onselection ());
this .onselection ();
this .container .show (300);
}
hide ()
{
this .selection .removeInterest (this);
this .container .hide (300, () => this .removeNode (this .node));
}
onmousedown (event)
{
$(document) .on ("mouseup.Panel", () => this .onmouseup ());
this .mousedown = true;
this .startX = event .pageX + parseFloat (this .container .css ("right"));
this .startY = event .pageY + parseFloat (this .container .css ("bottom"));
}
onmouseup ()
{
$(document) .off (".Panel");
this .mousedown = false;
if (!this .original)
return;
const { executionContext, node, field, previous } = this .original;
const value = field .copy ();
field .assign (previous);
this .assign (executionContext, node, field, value);
this .original = null;
}
onmousemove (event)
{
const
right = this .startX - event .pageX,
bottom = this .startY - event .pageY;
this .container .css ({
"right": `${right}px`,
"bottom": `${bottom}px`,
});
this .config .file .right = right;
this .config .file .bottom = bottom;
}
onselection ()
{
this .setNode (this .selection .nodes .at (-1));
}
setNode (node)
{
this .removeNode (this .node);
this .node = node;
this .addNode (this .node);
}
addNode (node)
{
if (!node)
{
this .pane .hidden = true;
return;
}
this .container .css ({
"overflow": "visible",
"max-height": "unset",
});
// Create folders.
const concreteNode = X3DUOM .find (`ConcreteNode[name="${node .getTypeName ()}"]`);
this .browser .currentScene .units .addInterest ("updateNode", this);
node .getPredefinedFields () .addInterest ("updateNode", this);
node .getUserDefinedFields () .addInterest ("updateNode", this);
for (const type of node .getType () .toReversed ())
{
switch (type)
{
case X3D .X3DConstants .X3DBoundedObject:
{
this .browser .currentScene .bbox_changed .addInterest ("refreshBBox", this);
break;
}
case X3D .X3DConstants .X3DGeometryNode:
{
node ._rebuild .addInterest ("refreshGeometry", this);
break;
}
}
}
this .addBlades (node, concreteNode);
this .pane .hidden = !this .pane .children .length;
// Set title.
const nodeElement = concreteNode .find (`InterfaceDefinition`);
this .container .attr ("title", this .getNodeTitle (node, nodeElement));
// Make first folder title draggable.
this .container .find (".tp-fldv_t") .first () .css ("cursor", "move") .on ("mousedown", () =>
{
$(document) .on ("mousemove.Panel", event => this .onmousemove (event));
})
.on ("click", event =>
{
event .stopPropagation ();
});
// Move panel in view if top, left, bottom or right is outside of window.
const
body = $("body"),
offset = this .container .parent () .offset (),
width = this .container .width (),
height = Math .min (this .container .height (), body .height () - 16);
let
right = parseFloat (this .container .css ("right")) || 0,
bottom = parseFloat (this .container .css ("bottom")) || 0;
const
left = this .container .parent () .width () - right - width + offset .left,
top = this .container .parent () .height () - bottom - height + offset .top;
if (left + width > body .width ())
right += (left + width) - body .width () + 8;
if (left < 0)
right += left - 8;
if (top + height > body .height ())
bottom += (top + height) - body .height () + 8;
if (top < 0)
bottom += top - 8;
this .container .css ({
"right": `${right}px`,
"bottom": `${bottom}px`,
});
if (this .container .height () > body .height () - 16)
{
this .container .css ({
"overflow": "auto",
"max-height": "calc(100vh - 16px)",
});
}
}
removeNode (node)
{
// Remove all folders.
for (const folder of Array .from (this .pane .children))
folder .dispose ();
if (!node)
return;
// Disconnect interests.
this .browser .currentScene .units .removeInterest ("updateNode", this);
node .getPredefinedFields () .removeInterest ("updateNode", this);
node .getUserDefinedFields () .removeInterest ("updateNode", this);
for (const type of node .getType () .toReversed ())
{
switch (type)
{
case X3D .X3DConstants .X3DBoundedObject:
{
this .browser .currentScene .bbox_changed .removeInterest ("refreshBBox", this);
break;
}
case X3D .X3DConstants .X3DGeometryNode:
{
node ._rebuild .removeInterest ("refreshGeometry", this);
break;
}
}
}
for (const field of node .getFields ())
field .removeFieldCallback (this);
}
updateNode ()
{
this .setNode (this .node);
}
addBlades (node, concreteNode)
{
const
seen = new Set (["IS", "DEF", "USE", "class", "id", "style"]),
userDefinedFields = node .getUserDefinedFields ();
for (const type of node .getType ())
{
if (type === X3D .X3DConstants .X3DPrototypeInstance)
{
this .addFolder ({
concreteNode: concreteNode,
title: node .getTypeName (),
node: node,
fields: Array .from (node .getFields ())
.filter (field => !seen .has (field .getName ()))
.map (field => field .getName ()),
});
}
else
{
const
typeName = X3D .X3DConstants [type],
fields = new Set (X3DUOM .find (`ConcreteNode[name="${typeName}"],AbstractNodeType[name="${typeName}"],AbstractObjectType[name="${typeName}"]`) .find ("field") .map (function () { return this .getAttribute ("name"); }) .get ());
switch (type)
{
case X3D .X3DConstants .FontStyle:
case X3D .X3DConstants .ScreenFontStyle:
{
seen .delete ("style");
break;
}
case X3D .X3DConstants .Script:
case X3D .X3DConstants .ComposedShader:
case X3D .X3DConstants .PackagedShader:
case X3D .X3DConstants .ShaderProgram:
{
for (const field of userDefinedFields)
fields .add (field .getName ());
break;
}
}
this .addFolder ({
concreteNode: concreteNode,
title: typeName,
node: node,
fields: Array .from (node .getFields ())
.filter (field => !seen .has (field .getName ()))
.filter (field => fields .has (field .getName ()))
.sort ((a, b) => userDefinedFields .includes (b) - userDefinedFields .includes (a))
.map (field => field .getName ()),
});
for (const name of fields)
seen .add (name);
}
}
}
addFolder ({ concreteNode, title, node, fields })
{
const folder = this .pane .addFolder ({
title: title,
expanded: this .config .global [`${title}.expanded`] ?? true,
index: 0,
});
// Update expanded state of folder.
folder .on ("fold", () => this .config .global [`${title}.expanded`] = folder .expanded)
// Add fields.
const parameter = { };
for (const name of fields)
this .addBinding (folder, parameter, node, node .getField (name), concreteNode);
switch (title)
{
case "X3DBoundedObject":
{
folder .addBlade ({ view: "separator" });
const options = {
format: value => this .format (this .#vector, value),
};
for (const key in this .#vector)
options [key] = { format: options .format };
this .refreshBBox ();
this .bbox .bboxSizeInput = folder .addBinding (this .bbox, "calculatedSize", options);
this .bbox .bboxCenterInput = folder .addBinding (this .bbox, "calculatedCenter", options);
$(this .bbox .bboxSizeInput .element) .find ("input") .attr ("readonly", "");
$(this .bbox .bboxCenterInput .element) .find ("input") .attr ("readonly", "");
$(this .bbox .bboxSizeInput .element) .find (".tp-txtv_k") .detach ();
$(this .bbox .bboxCenterInput .element) .find (".tp-txtv_k") .detach ();
break;
}
case "X3DGeometryNode":
{
this .refreshGeometry ();
switch (($.try (() => node .getInnerNode ()) ?? node) .getGeometryType ())
{
case 0:
this .numPrimitives .monitor = folder .addBinding (this .numPrimitives, "numberOfPoints", { readonly: true });
break
case 1:
this .numPrimitives .monitor = folder .addBinding (this .numPrimitives, "numberOfLines", { readonly: true });
break
case 2:
case 3:
this .numPrimitives .monitor = folder .addBinding (this .numPrimitives, "numberOfTriangles", { readonly: true });
break
}
break;
}
}
if (!folder .children .length)
folder .dispose ();
}
addBinding (folder, parameter, node, field, concreteNode)
{
if (!field .isInitializable ())
return;
const
fieldElement = concreteNode .find (`field[name="${field .getName ()}"]`),
options = { format: value => this .format (field, value) };
switch (field .getType ())
{
case X3D .X3DConstants .SFColor:
case X3D .X3DConstants .SFColorRGBA:
{
options .color = { type: "float" };
break;
}
case X3D .X3DConstants .SFInt32:
{
options .step = 1;
break;
}
case X3D .X3DConstants .SFString:
{
const enumerations = fieldElement .find ("enumeration") .map (function () { return this .getAttribute ("value"); }) .get ();
if (enumerations .length)
{
options .options = { };
for (const value of enumerations)
options .options [value] = value;
}
break;
}
case X3D .X3DConstants .SFVec2d:
case X3D .X3DConstants .SFVec2f:
{
options .y = { inverted: true };
break;
}
}
switch (field .getType ())
{
case X3D .X3DConstants .SFBool:
case X3D .X3DConstants .SFColor:
case X3D .X3DConstants .SFColorRGBA:
case X3D .X3DConstants .SFDouble:
case X3D .X3DConstants .SFFloat:
case X3D .X3DConstants .SFInt32:
case X3D .X3DConstants .SFMatrix3d:
case X3D .X3DConstants .SFMatrix3f:
case X3D .X3DConstants .SFMatrix4d:
case X3D .X3DConstants .SFMatrix4f:
case X3D .X3DConstants .SFRotation:
case X3D .X3DConstants .SFString:
case X3D .X3DConstants .SFTime:
case X3D .X3DConstants .SFVec2d:
case X3D .X3DConstants .SFVec2f:
case X3D .X3DConstants .SFVec3d:
case X3D .X3DConstants .SFVec3f:
case X3D .X3DConstants .SFVec4d:
case X3D .X3DConstants .SFVec4f:
{
const
scene = this .browser .currentScene,
category = field .getUnit (),
min = fieldElement .attr ("minInclusive") ?? fieldElement .attr ("minExclusive"),
max = fieldElement .attr ("maxInclusive") ?? fieldElement .attr ("maxExclusive");
for (const key in field)
options [key] ??= { };
for (const key in field)
options [key] .format = options .format;
if (min !== undefined)
{
options .min = scene .toUnit (category, parseFloat (min));
for (const key in field)
options [key] .min = options .min;
}
if (max !== undefined)
{
options .max = scene .toUnit (category, parseFloat (max));
for (const key in field)
options [key] .max = options .max;
}
this .refresh (parameter, node, field);
const input = $.try (() => folder .addBinding (parameter, field .getName (), options));
if (!input)
break;
$(input .element) .on ("mouseenter", () =>
{
$(input .element) .attr ("title", this .getFieldTitle (node, field, fieldElement));
});
input .on ("change", ({ value }) => this .onchange (node, field, value));
field .addFieldCallback (this, () =>
{
if (this .changing)
return;
this .refresh (parameter, node, field);
input .refresh ();
});
break;
}
case X3D .X3DConstants .SFImage:
case X3D .X3DConstants .MFBool:
case X3D .X3DConstants .MFColor:
case X3D .X3DConstants .MFColorRGBA:
case X3D .X3DConstants .MFDouble:
case X3D .X3DConstants .MFFloat:
case X3D .X3DConstants .MFImage:
case X3D .X3DConstants .MFInt32:
case X3D .X3DConstants .MFMatrix3d:
case X3D .X3DConstants .MFMatrix3f:
case X3D .X3DConstants .MFMatrix4d:
case X3D .X3DConstants .MFMatrix4f:
case X3D .X3DConstants .MFRotation:
case X3D .X3DConstants .MFString:
case X3D .X3DConstants .MFTime:
case X3D .X3DConstants .MFVec2d:
case X3D .X3DConstants .MFVec2f:
case X3D .X3DConstants .MFVec3d:
case X3D .X3DConstants .MFVec3f:
case X3D .X3DConstants .MFVec4d:
case X3D .X3DConstants .MFVec4f:
{
const tooMuchValues = (field instanceof X3D .X3DArrayField) && field .length >= 1_000;
if (tooMuchValues)
parameter [field .getName ()] = util .format (_("%s values"), field .length .toLocaleString (_.locale));
else
this .refresh (parameter, node, field);
const input = folder .addBinding (parameter, field .getName (),
{
readonly: true,
multiline: !tooMuchValues,
rows: tooMuchValues ? 1 : 3,
});
$(input .element) .on ("mouseenter", () =>
{
$(input .element) .attr ("title", this .getFieldTitle (node, field, fieldElement));
});
if (tooMuchValues)
break;
const
original = $(input .element) .find ("textarea"),
textarea = $("<textarea></textarea>");
textarea
.attr ("class", original .attr ("class"))
.val (parameter [field .getName ()])
.on ("focusout", () => this .onchange (node, field, textarea .val ()));
original .replaceWith (textarea);
field .addFieldCallback (this, () =>
{
if (this .changing)
return;
this .refresh (parameter, node, field);
textarea .val (parameter [field .getName ()]);
});
break;
}
}
}
#float = new X3D .SFFloat ();
#double = new X3D .SFDouble ();
format (field, value)
{
switch (field .getType ())
{
case X3D .X3DConstants .SFColor:
case X3D .X3DConstants .SFColorRGBA:
case X3D .X3DConstants .SFFloat:
case X3D .X3DConstants .SFMatrix3f:
case X3D .X3DConstants .SFMatrix4f:
case X3D .X3DConstants .SFVec2f:
case X3D .X3DConstants .SFVec3f:
case X3D .X3DConstants .SFVec4f:
{
this .#float .setValue (value);
return this .#float .toString ();
}
case X3D .X3DConstants .SFDouble:
case X3D .X3DConstants .SFMatrix3d:
case X3D .X3DConstants .SFMatrix4d:
case X3D .X3DConstants .SFRotation:
case X3D .X3DConstants .SFTime:
case X3D .X3DConstants .SFVec2d:
case X3D .X3DConstants .SFVec3d:
case X3D .X3DConstants .SFVec4d:
{
this .#double .setValue (value);
return this .#double .toString ();
}
default:
return value;
}
}
refresh (parameter, node, field)
{
const
scene = this .browser .currentScene,
executionContext = node .getExecutionContext (),
category = field .getUnit (),
name = field .getName ();
switch (field .getType ())
{
case X3D .X3DConstants .SFBool:
case X3D .X3DConstants .SFDouble:
case X3D .X3DConstants .SFFloat:
case X3D .X3DConstants .SFInt32:
case X3D .X3DConstants .SFString:
case X3D .X3DConstants .SFTime:
{
parameter [name] = scene .toUnit (category, field .getValue ());
break;
}
case X3D .X3DConstants .SFImage:
case X3D .X3DConstants .SFMatrix3d:
case X3D .X3DConstants .SFMatrix3f:
case X3D .X3DConstants .SFMatrix4d:
case X3D .X3DConstants .SFMatrix4f:
{
parameter [name] = field .toString ();
break;
}
case X3D .X3DConstants .SFRotation:
{
const p = parameter [name] ??= { };
p .x = field .x;
p .y = field .y;
p .z = field .z;
p .w = scene .toUnit ("angle", field .angle);
break;
}
case X3D .X3DConstants .SFColor:
case X3D .X3DConstants .SFColorRGBA:
{
field = this .linearToSRGB (node, field);
// Proceed with next case:
}
case X3D .X3DConstants .SFVec2d:
case X3D .X3DConstants .SFVec2f:
case X3D .X3DConstants .SFVec3d:
case X3D .X3DConstants .SFVec3f:
case X3D .X3DConstants .SFVec4d:
case X3D .X3DConstants .SFVec4f:
{
const p = parameter [name] ??= { };
for (const key in field)
p [key] = scene .toUnit (category, field [key]);
break;
}
case X3D .X3DConstants .MFBool:
case X3D .X3DConstants .MFDouble:
case X3D .X3DConstants .MFFloat:
case X3D .X3DConstants .MFImage:
case X3D .X3DConstants .MFInt32:
case X3D .X3DConstants .MFMatrix3d:
case X3D .X3DConstants .MFMatrix3f:
case X3D .X3DConstants .MFMatrix4d:
case X3D .X3DConstants .MFMatrix4f:
case X3D .X3DConstants .MFString:
case X3D .X3DConstants .MFTime:
{
const
single = new (field .getSingleType ()) (),
options = { scene: executionContext };
single .setUnit (field .getUnit ());
const value = Array .from (field, value =>
{
single .setValue (value);
return single .toString (options);
})
.join (",\n");
parameter [name] = value;
break;
}
case X3D .X3DConstants .MFColor:
case X3D .X3DConstants .MFColorRGBA:
case X3D .X3DConstants .MFRotation:
case X3D .X3DConstants .MFVec2d:
case X3D .X3DConstants .MFVec2f:
case X3D .X3DConstants .MFVec3d:
case X3D .X3DConstants .MFVec3f:
case X3D .X3DConstants .MFVec4d:
case X3D .X3DConstants .MFVec4f:
{
const
single = new (field .getSingleType ()) (),
options = { scene: executionContext };
single .setUnit (field .getUnit ());
const value = Array .from (field, value =>
{
single .assign (value);
return single .toString (options);
})
.join (",\n");
parameter [name] = value;
break;
}
}
}
onchange (node, field, value)
{
const
scene = this .browser .currentScene,
executionContext = node .getExecutionContext (),
category = field .getUnit ();
if (!(this .mousedown || this .container .find (":focus") .is ("input, textarea, select")))
return;
this .changing = true;
this .browser .nextFrame () .then (() => this .changing = false);
switch (field .getType ())
{
case X3D .X3DConstants .SFBool:
case X3D .X3DConstants .SFString:
{
Editor .setFieldValue (executionContext, node, field, value);
break;
}
case X3D .X3DConstants .SFColor:
{
value = this .sRGBToLinear (node, new X3D .Color3 (value .r, value .g, value .b));
this .assign (executionContext, node, field, value);
break;
}
case X3D .X3DConstants .SFColorRGBA:
{
value = this .sRGBToLinear (node, new X3D .Color4 (value .r, value .g, value .b, value .a));
this .assign (executionContext, node, field, value);
break;
}
case X3D .X3DConstants .SFDouble:
case X3D .X3DConstants .SFFloat:
case X3D .X3DConstants .SFInt32:
case X3D .X3DConstants .SFTime:
{
this .assign (executionContext, node, field, scene .fromUnit (category, value));
break;
}
case X3D .X3DConstants .SFRotation:
{
value = new X3D .Rotation4 (value .x,
value .y,
value .z,
scene .fromUnit ("angle", value .w));
this .assign (executionContext, node, field, value);
break;
}
case X3D .X3DConstants .SFVec2d:
case X3D .X3DConstants .SFVec2f:
{
value = new X3D .Vector2 (scene .fromUnit (category, value .x),
scene .fromUnit (category, value .y));
this .assign (executionContext, node, field, value);
break;
}
case X3D .X3DConstants .SFVec3d:
case X3D .X3DConstants .SFVec3f:
{
value = new X3D .Vector3 (scene .fromUnit (category, value .x),
scene .fromUnit (category, value .y),
scene .fromUnit (category, value .z));
this .assign (executionContext, node, field, value);
break;
}
case X3D .X3DConstants .SFVec4d:
case X3D .X3DConstants .SFVec4f:
{
value = new X3D .Vector4 (scene .fromUnit (category, value .x),
scene .fromUnit (category, value .y),
scene .fromUnit (category, value .z),
scene .fromUnit (category, value .w));
this .assign (executionContext, node, field, value);
break;
}
case X3D .X3DConstants .SFImage:
case X3D .X3DConstants .SFMatrix3d:
case X3D .X3DConstants .SFMatrix3f:
case X3D .X3DConstants .SFMatrix4d:
case X3D .X3DConstants .SFMatrix4f:
{
try
{
Editor .setFieldFromString (executionContext, node, field, value);
}
catch
{
electron .shell .beep ();
}
break;
}
case X3D .X3DConstants .MFBool:
case X3D .X3DConstants .MFColor:
case X3D .X3DConstants .MFColorRGBA:
case X3D .X3DConstants .MFDouble:
case X3D .X3DConstants .MFFloat:
case X3D .X3DConstants .MFImage:
case X3D .X3DConstants .MFInt32:
case X3D .X3DConstants .MFMatrix3d:
case X3D .X3DConstants .MFMatrix3f:
case X3D .X3DConstants .MFMatrix4d:
case X3D .X3DConstants .MFMatrix4f:
case X3D .X3DConstants .MFRotation:
case X3D .X3DConstants .MFString:
case X3D .X3DConstants .MFTime:
case X3D .X3DConstants .MFVec2d:
case X3D .X3DConstants .MFVec2f:
case X3D .X3DConstants .MFVec3d:
case X3D .X3DConstants .MFVec3f:
case X3D .X3DConstants .MFVec4d:
case X3D .X3DConstants .MFVec4f:
{
try
{
Editor .setFieldFromString (executionContext, node, field, `[${value}]`);
}
catch
{
electron .shell .beep ();
}
break;
}
}
}
assign (executionContext, node, field, value)
{
if (this .mousedown)
{
this .original ??= {
executionContext,
node,
field: field,
previous: field .copy (),
};
field .setValue (value);
executionContext .getOuterNode () ?.requestUpdateInstances ?.();
}
else
{
Editor .setFieldValue (executionContext, node, field, value);
}
}
#box = new X3D .Box3 ();
#vector = new X3D .SFVec3f ();
bbox = {
calculatedSize: { },
calculatedCenter: { },
bboxSizeInput: null,
bboxCenterInput: null,
};
refreshBBox ()
{
const bbox = this .node .getBBox (this .#box);
this .#vector .setValue (bbox .size);
this .#vector .setName ("calculatedSize");
this .#vector .setUnit ("length");
this .refresh (this .bbox, this .node, this .#vector)
this .#vector .setValue (bbox .center);
this .#vector .setName ("calculatedCenter");
this .#vector .setUnit ("length");
this .refresh (this .bbox, this .node, this .#vector)
this .bbox .bboxSizeInput ?.refresh ();
this .bbox .bboxCenterInput ?.refresh ();
}
numPrimitives = {
numberOfPoints: "0",
numberOfLines: "0",
numberOfTriangles: "0",
monitor: null,
}
refreshGeometry ()
{
const
node = $.try (() => this .node .getInnerNode ()) ?? this .node,
numVertices = node .getVertices () .length / 4;
switch (node .getGeometryType ())
{
case 0:
this .numPrimitives .numberOfPoints = (numVertices) .toLocaleString (_.locale);
break
case 1:
this .numPrimitives .numberOfLines = (numVertices / 2) .toLocaleString (_.locale);
break
case 2:
case 3:
this .numPrimitives .numberOfTriangles = (numVertices / 3) .toLocaleString (_.locale);
break
}
this .numPrimitives .monitor ?.refresh ();
}
getNodeTitle (node, nodeElement)
{
const description = nodeElement .attr ("appinfo") ?? node .getAppInfo ?.();
let title = "";
if (description)
title += `Description:\n\n${description}`;
return title;
}
getFieldTitle (node, field, fieldElement)
{
function truncate (string, n)
{
return string .length > n ? string .slice (0, n) + "..." : string;
};
if (node .getType () .includes (X3D .X3DConstants .X3DPrototypeInstance))
field = node .getFieldDefinitions () .get (field .getName ()) .getValue ();
const description = fieldElement .attr ("description") ?? field .getAppInfo ();
let title = "";
if (description)
title += `Description:\n\n${description}\n\n`;
title += `Type: ${field .getTypeName ()}\n`;
if (field instanceof X3D .X3DArrayField)
title += `Number of values: ${field .length}`;
else if (field .getType () === X3D .X3DConstants .SFImage)
title += `Current value: ${field .width} ${field .height} ${field .comp} ...`;
else if (field .getType () === X3D .X3DConstants .SFString)
title += `Current value: ${truncate (field .toString (), 20)}`;
else
title += `Current value: ${field .toString ({ scene: node .getExecutionContext () })}`;
return title;
}
};