Skip to content

Advanced Topics

NAS6mixfoolv edited this page Jul 3, 2026 · 65 revisions

* Advanced Topics

Debugging Techniques for Advanced Topics

When dealing with complex coefficients or "magic numbers" in advanced mathematical and physics simulations,
understanding their precise numerical characteristics is crucial. A highly effective debugging technique
involves substituting extreme values (e.g., very large, very small, zero, or near-zero values)
into these coefficients and meticulously observing the system's behavior.

This approach allows you to:

  • Identify numerical instabilities:
    See how the system reacts under stress, revealing potential overflow, underflow, or division-by-zero issues.

  • Isolate component effects:
    Understand the specific contribution of each coefficient to the overall simulation by pushing it to its limits.

  • Validate theoretical assumptions:
    Confirm if the real-world numerical behavior aligns with the mathematical theory behind the coefficients.

By deliberately pushing your simulation to its numerical boundaries, you gain deeper insights into its robustness
and the intricate interplay of its components, which is invaluable for refining complex algorithms.


Programing tips

* Synchronization solutions for asynchronous processing

Back to Table of contents

As a simple synchronization solution,
run the main loop while returning control to the system
in the loop, check the validity of the instance in question
if it is not valid, skip and wait if it is valid, execute the process

Actual implementation example

// --- Global Variables ---
// Timer manager for controlling animation loops
var TMan = new N6LTimerMan(); 
// ID for the main animation timer
var TimerID = -1; 
var intvl = 50; 
// X3DOM runtime object for scene manipulation
var x3domRuntime;

// --- Initialization on Document Ready ---
// Executed when the entire HTML document has been loaded and parsed.
jQuery(document).ready(function(){
  //ToDo : Write Your Initialize

  TimerID = TMan.add(); // Add a new timer instance to the N6LTimerMan
  GLoop(TimerID); // Start the main loop by scheduling its first run
});

// --- Main Loop ---
// The core loop that process and renders the X3D scene.
function GLoop(id){
  // Initialize x3domRuntime if not already obtained
  if(x3domRuntime == undefined) x3domRuntime = document.getElementById('x3dabs').runtime;
  else {
     //ToDo : Write Your process and renders the X3D scene, After x3domRuntime availability

  }

  // Reschedule the main loop to run again after 'intvl' milliseconds using the timer manager
  // By adjusting the intvl value, you can control the smoothness of the animation and the CPU load.
  TMan.timer[id].setalerm(function() { GLoop(id); }, intvl);
}


Class Templates as Managed Class Imitations

[Back to Table of Contents](Home-English#Table of Contents)

Imitating Managed Classes with Javascript [Link Outside Wiki]
Managed Class Template Explanation [Link outside Wiki]

/**
//##################################################################################################################################
* MyManagedClass: Class template for imitating managed classes
* MyManagedClass.property manages class data in one place
* Supports deep/shallow merging
* Automatically converts arrays to objects when merging (avoiding key collisions/complex joins)
* To convert to an array, use function Use toArrayIfIndexed(obj, force = false, sparseHandling = 'keep')
* Serially.
* Note: Including MyManagedClass itself in the MyManagedClass.property element risks circular references and should be avoided.
*/

/**
* The MyManagedClass result is returned as an object, but if you need an array, use toArrayIfIndexed.
* Example: const items = toArrayIfIndexed(instance.property.profile.items, true);
*/
/**
* MyManagedClass handles arrays as objects.
* If you need an array, use toArrayIfIndexed to convert.
* Example:
* const instance = new MyManagedClass({ items: [{ id: 1 }, { id: 2 }] });
* const itemsArray = toArrayIfIndexed(instance.property.items, true); // [{ id: 1 }, { id: 2 }]
// Retrieving array properties
const instance = new MyManagedClass({ profile: { items: [{ id: 1 }, { id: 2 }] } });
const items = toArrayIfIndexed(instance.property.profile.items, true);
items.forEach(item => console.log(item.id)); // 1, 2

// Top-level array
const arrayInstance = new MyManagedClass([{ id: 1 }, { id: 2 }]);
const array = toArrayIfIndexed(arrayInstance.property, true);
console.log(array); // [{ id: 1 }, { id: 2 }] */
/**
* Converts an object with numeric string keys into an array.
* @param {Object} obj - Object to convert.
* @param {boolean} [force=false] - Forces conversion to an array even if the key is not a numeric string.
* @param {String} [sparseHandling = 'keep'] - Selects whether to compact empty arrays.
* @returns {Array|Object} Array (if convertible) or the original object.
*/
function toArrayIfIndexed(obj, force = false, sparseHandling = 'keep') {
if (!obj || typeof obj !== 'object' || Array.isArray(obj)) return obj;
const keys = Object.keys(obj);
const isIndexed = keys.every((key, i) => String(i) === key);
if (!isIndexed && !force && process.env.NODE_ENV !== 'production') {
console.warn('Non-sequential keys detected; consider using force=true or sparseHandling="compact"');
}
const maxIndex = keys.length > 0 ? Math.max(...keys.map(Number)) + 1 : 0;
const result = new Array(maxIndex);
for (const key of keys) {
result[Number(key)] = obj[key];
}
if (sparseHandling === 'compact') {
return result.filter(x => x !== undefined);
}
return result;
}

// Fallback clone function
function fallbackClone(item, seen = new WeakSet()) {
if (item === null || typeof item !== 'object') return item; 
if (seen.has(item)) { 
throw new Error('Circular reference detected in fallbackClone'); 
} 
seen.add(item); 
if (typeof item.clone === 'function') { 
return item.clone(); 
} 
if (Array.isArray(item)) return item.map(item => fallbackClone(item, seen)); 
if (item instanceof Date) return new Date(item); 
if (item instanceof Map) return new Map(fallbackClone([...item], seen)); 
if (item instanceof Set) return new Set(fallbackClone([...item], seen)); 
if (item instanceof RegExp) return new RegExp(item); 
const cloned = {}; 
for (const key in item) { 
cloned[key] = fallbackClone(item[key], seen);
}
return cloned;
}

// Safe structuredClone wrapper
function safeStructuredClone(item) {
try {
return structuredClone(item);
} catch (e) {
return fallbackClone(item);
}
}

function recursiveClone(item, seen = new WeakSet()) {
// Check for circular references
if (item && typeof item === 'object' && seen.has(item)) {
throw new Error('Circular reference detected');
}
if (item && typeof item === 'object') {
seen.add(item);
}

/ 1. If it has a clone() method
if (item && typeof item.clone === 'function') {
return item.clone();
}

/ 2. If it's an array
if (Array.isArray(item)) {
return item.map(element => recursiveClone(element, seen));
}

/ 3. Plain Object
if (item && typeof item === 'object' && item.constructor === Object) {
const clonedObject = {};
for (const key in item) {
clonedObject[key] = recursiveClone(item[key], seen);
}
return clonedObject;
}

/ 4. Other (Primitive Values, etc.)
return safeStructuredClone(item);
}

function simpleDeepMerge(target, source, seen = new WeakSet(), deep = true) {
if (!deep) {
return Object.assign({}, target, source);
}
/ // Check that the target is a valid object
/ // If the target is not an object, return a deep copy (overwrite) of the source
if (target === null || typeof target !== 'object') {
// If the target is invalid, there's no point in continuing processing, so return the source as is or use safeStructuredClone.
return safeStructuredClone(source);
}

// Verify that the source is a valid object.
if (source === null || typeof source !== 'object') {
return target; // Since there's nothing to merge, return the target as is.
}

// 1. Check for circular references and register.
// Register once processing at this layer is confirmed.
if (seen.has(target) || seen.has(source)) {
throw new Error('Circular reference detected');
}
seen.add(target);
seen.add(source);

// Convert the array to an object.A helper function to convert arrays to objects.
const arrayToObject = arr => ({ ...arr });

if (Array.isArray(target) && process.env.NODE_ENV !== 'production') {
console.warn('Target array converted to object in simpleDeepMerge. Use toArrayIfIndexed to convert back.');
}

// Standardize array processing: Treat all arrays as plain indexed objects.
if (Array.isArray(target)) {
// target is the merge target. Convert it to a new object to maintain its type.
// Create a new object to merge it as an object while preserving the contents of the existing target.
target = Object.assign({}, target);
}
if (Array.isArray(source)) {
source = arrayToObject(source);
}

// 2. Merge logic.
for (const key in source) {
const sourceValue = source[key];
const targetValue = target[key];

// Recurse only if the target value is an object and the source value is also an object.
if (sourceValue && typeof sourceValue === 'object' && sourceValue.constructor === Object) {

// If the target value is not an object, initialize it with a new, empty object.
if (!targetValue || typeof targetValue !== 'object' || Array.isArray(targetValue)) {
target[key] = {};
}
// Since we're treating the array as an object, you can omit the Array.isArray(targetValue) check here.
// If the target exists as an array, it should already have been converted to an object with Object.assign({}, target).

simpleDeepMerge(target[key], sourceValue, seen); // Recursive

} else if (typeof sourceValue?.clone === 'function') {
// Override custom classes with clone().
target[key] = sourceValue.clone();

} else {
// Deep copy and overwrite primitives and other objects using safeStructuredClone.
target[key] = safeStructuredClone(sourceValue);
}
}

return target;
}

const MyManagedClassDefaultProperty = Object.freeze({
variablename: "MyManagedClassDefaultProperty",
profile: Object.freeze({ name: "Default Name", age: 25 }),
settings: Object.freeze({ theme: "light", notifications: true })
});

class MyManagedClass {
constructor(p) {
const target = recursiveClone(MyManagedClassDefaultProperty);
this.property = simpleDeepMerge(target, p);
}
clone() {
return new MyManagedClass(this.property);
}
merge(p, deep = true) {
// 1. Determine the base property to copy to.
let baseProperty;
if (deep) {
// To deep copy and then merge, first deep copy the property itself.
baseProperty = recursiveClone(this.property);

// 2. Deep merge the new data (p) into a new property object (baseProperty).
// (Assuming simpleDeepMerge updates the target in-place.)
simpleDeepMerge(baseProperty, p);
} else {
// Shallow merge: Use Object.assign to overwrite the properties of p with a shallow copy of this.property.
baseProperty = Object.assign({}, this.property, p);
}
// 3. Return a new instance with the new properties to maintain immutability.
return new MyManagedClass(baseProperty);
}
isThisType(rh){
return rh instanceof MyManagedClass;
}
toString(){
try {
// JSON.stringify(data, replacer, number of spaces)
// By specifying "2" as the number of spaces, the JSON will be indented with two spaces.
const jsonString = JSON.stringify(this.property, null, 2);

// Add an indent (2 spaces in this case) to the beginning of each line of the JSON string to match the custom header.
// Exclude the first curly brace {.
const indentedJson = jsonString.split('\n')
.map((line, index) => (index > 0 ? ' ' : '') + line)
.join('\n');

// Join the header and formatted JSON.
if(this.property.variablename) return `MyManagedClass Instance (${this.property.variablename}) {\n "property": ${indentedJson}\n}`;
else return `MyManagedClass Instance {\n "property": ${indentedJson}\n}`;
} catch (e) {
// Handle cases where there is a circular reference, etc.
return `MyManagedClass Instance [Serialization Error: ${e.message}]`;

}

}

/Write other methods here

}

const taroData = {
variablename: "taroData",
profile: { name: "Taro", nation: "Japan", items: {0: "ball", 1: "glove"} },
settings: { theme: "red" }
};

const jiroData = {
variablename: "jiroData",
profile: { name: "Jiro", nation: "Japan" },
settings: { theme: "dark" }
};

const zakoData = {
variablename: "zakoData",
profile: { name: "Zako", nation: "Japan" },
settings: { theme: "blue" }
};

function enter5(){

const data = new MyManagedClass(taroData);

const elm = document.getElementById('TDATA'); 
elm.value = data.toString(); 
console.log(data.toString());
/*
MyManagedClass Instance (taroData) { 
"property": { 
"variablename": "taroData", 
"profile": { 
"name": "Taro", 
"age": 25, 
"nation": "Japan", 
"items": { 
"0": "ball", 
"1": "glove" 
} 
}, 
"settings": { 
"theme": "red", 
"notifications": true 
} 
}
}
*/ 

const MyManagedClassMap = { 
leader: new MyManagedClass(taroData), 
member: { 
subleader: new MyManagedClass(jiroData), 
bench: new MyManagedClass(zakoData) 
} 
}; 

const clonedMM = recursiveClone(MyManagedClassMap); 
const tmpM = MyManagedClassMap.leader.clone(); 
MyManagedClassMap.leader = MyManagedClassMap.member.bench.clone(); 
MyManagedClassMap.member.bench = tmpM.clone(); 

console.log(MyManagedClassMap);
/*
const MyManagedClassMap = {
leader: new MyManagedClass(zakoData),
member: {
subleader: new MyManagedClass(jiroData),
bench: new MyManagedClass(taroData)
}
};
*/
console.log(clonedMM);
/*
const clonedPM = {
leader: new MyManagedClass(taroData),
member: {
subleader: new MyManagedClass(jiroData),
bench: new MyManagedClass(zakoData)
}
};
*/

}

【Self-Managed Object Design】

This model is a design pattern that enables type-safe deep copying and immutability control in JavaScript.

##1. Role as a Managed Class (MyManagedClass)

MyManagedClass functions as a "managed class" that centrally manages its own data (state) based on default values.

- Encapsulation and Initialization: The custom function simpleDeepMerge() is used in the constructor to initialize this.property by reliably deep/shallow merging default values ??and input data.
- This prevents unauthorized external changes and reference sharing (shallow copying)

Maintains the integrity of the object.

  • State Update: The merge() method also performs deep/shallow merging, allowing you to safely and reliably update the state of an existing instance.

##2. Type-Preserving Deep Copy Strategy (.clone() Method)

When a copy is requested from the outside, the class itself controls the copy method via the clone() method.

- Guaranteed type safety: clone() always executes new MyManagedClass(...)

Ensures that the copied object retains the type information (prototype) and all methods of the original class.

  • Delegated deep copy: Instead of relying on external general-purpose utilities, it creates new instances using its own internal data (this.property).

This maintains high maintainability even when data structures become complex.

##3. General-purpose structure traversal utility (recursiveClone)

The recursiveClone function provides a universal processing layer that is independent of classes.

  • Processing priority: By checking for the presence of the clone() method at the top of the processing hierarchy,

Custom object deep copy logic (deep copy by the class itself) takes top priority.

  • Generality and recursion: Recursively traverses nested structures of arrays and plain objects (associative arrays).
    Processes all elements. The lowest-level primitive values ??are reliably copied with structuredClone.

Conclusion: This function serves as an abstract entry point for safely deep copying entire complex data structures containing instances of MyManagedClass, nested arrays, and other plain objects, while preserving type information.

##4. Controlled normalization of data structures (toArrayIfIndexed function)

- Usage strategy principle: Maximize predictability
recursiveClone receives a data structure in which Arrays are automatically converted to Objects due to the internal simpleDeepMerge specification. However, by not automatically executing toArrayIfIndexed,
unintentional reconversion (reshaping Objects back to Arrays) during the deep copy process is avoided.
This maximizes the predictability and minimization of side effects of the overall design.

- Separation of responsibilities and functionality
Responsibility of recursiveClone: ??It focuses on its core responsibility of faithfully duplicating and maintaining the current data structure (Object) after conversion by simpleDeepMerge.

Responsibility of toArrayIfIndexed: It performs the secondary process of reshaping (normalizing) the cloned Object into an array based on the force and sparseHandling options only when the user explicitly decides to use the data as an array.

Conclusion: This model strictly copies the structure (Object) after the initial conversion by simpleDeepMerge, while allowing reshaping into an array only under user control with toArrayIfIndexed, thereby achieving both data immutability and transparency of the timing and results of structural changes.


Final Conclusion
This model implements the "object itself is responsible for copying (.clone())" design pattern.
It provides a highly robust and scalable foundation for developing applications where data immutability is essential, such as those with undo/redo functionality and history management.


Caution: Including MyManagedClass itself within MyManagedClass.property

: Avoid this as it creates a circular reference.


Class design is based on the idea of ​​binary operators in managed classes

Back to Table of contents

ret = lh.MyMethod(rh);

In managed class design, basing your approach on the concept of binary operators provides a powerful and intuitive framework.
This involves clearly defining left-hand operands (lh) and right-hand operands (rh).
The left-hand operand is considered the primary actor or "main part" of the member function,
typically represented by this (or me in some contexts). The right-hand operand is then defined
as the entity that acts upon, or is processed by, the left-hand operand.
This structure is incredibly convenient because the left-hand operand isn't just the object calling the method;
it also serves as the initial context or setting for that method's operation.

While JavaScript doesn't support traditional method overloading
(where function behavior automatically differs based on argument types),
you can elegantly implement polymorphic behavior using a property like typename
for type identification, as shown in this example:

class TypeA {
  constructor(...) {
    this.typename = "TypeA";
    // ... constructor logic ...
  }

  MyAdd(rh) {
    var ret; // Declare ret here

    if (rh && rh.typename === "TypeA") {
      ret = new TypeA(this); // Use 'this' (lh) as part of the new object's initialization
      // Process when rh is TypeA
      return ret;
    } else if (rh && rh.typename === "TypeB") {
      ret = new TypeA(this); // Use 'this' (lh) as part of the new object's initialization
      // Process when rh is TypeB
      return ret;
    }
    // Handle other types or throw an error for unsupported types
    // For example: throw new Error("Unsupported operand type");
    return ret; // Or handle default/error case
  }
}

// User usage example
var lh = new TypeA(...);
var rh = new TypeA(...); // or new TypeB(...)
var ret = lh.MyAdd(rh);

This approach allows you to automatically branch MyAdd's behavior based on the class of rh,
all while maintaining a simple and clear code structure.

Practical Application Example
Consider the following examples using N6LVector (Vector) and N6LMatrix (Matrix) classes:

var v1 = new N6LVector([1,2,3,4],true); // Vector instance
var v2 = new N6LVector([1,5,6,7],true); // Another Vector instance
var m1 = new N6LMatrix(4).UnitMat(); // Matrix instance (e.g., a 4x4 Identity Matrix)
var m2 = new N6LMatrix([[1,0,0,0],[0,0,0,1],[0,0,1,0],[0,-1,0,0]]); // Another Matrix instance

var ret1 = v1.Mul(v2);
var ret2 = v1.Mul(m1);
var ret3 = m1.Mul(m2);
var ret4 = m1.Mul(v1);

In the examples above, the clear definition of left-hand and right-hand operands dictates the operation:

ret1 = v1.Mul(v2); means multiplication of two vectors
(e.g., dot product, cross product depending on implementation).
ret2 = v1.Mul(m1); means multiplication of a vector by a matrix from its right side
(vector-matrix multiplication).
ret3 = m1.Mul(m2); means multiplication of two matrices.
ret4 = m1.Mul(v1); means multiplication of a matrix by a vector from its right side
(matrix-vector multiplication).
This demonstrates how adhering to the binary operator concept in your class design leads to
highly intuitive and readable code, especially in contexts like physics simulations
where various types of mathematical operations are common.


* Optimization Tips

Back to Table of contents

Use Quaternions for Faster Rotation

If you want to achieve faster speeds and better optimization,
use quaternions rather than matrices for rotation transformations.

Matrix rotation transformations are implemented using Rodrigues' rotation formula, and since the Maclaurin expansion
in the two trigonometric function calls required to derive sin and cos is a heavier process than rotation using quaternions,
it is more efficient to avoid rotation using matrices and use quaternions instead.

//Before optimization
var m0 = new N6LMatrix(4).UnitMat();
var az = new N6LVector(4, true).UnitVec(3);//global z-axis
var ay = new N6LVector(4, true).UnitVec(2);//global y-axis
var ax = new N6LVector(4, true).UnitVec(1);//global x-axis
var mWK = m0.RotAxis(az, 10 * Math.PI / 180.0);//10 degree rotation around z-axis
mWK = mWK.RotAxis(ay, 20 * Math.PI / 180.0);//20 degree rotation around y-axis
var ansM0 = mWK.RotAxis(ax, 30 * Math.PI / 180.0);//30 degree rotation around x-axis
console.log(ansM0.x);
var ansV0 = ansM0.Vector()//Rotation vector;
console.log(ansV0.x);
// The corresponding XYZ Euler angles for this rotation are 30, 20, 10 degrees.
var fr = 1000; // Factor for rounding Euler angles to a certain decimal place
// Extract Euler angles (XYZ order: 1=X, 2=Y, 3=Z) from matrix ansM0.
var ea = ansM0.EulerAngle(1,2,3);
// Format Euler angles as a comma-separated string, converted to degrees and rounded.
var str = String(Math.floor(ea.x[1]*(180.0/Math.PI)*fr)/fr)+','+String(Math.floor(ea.x[2]*(180.0/Math.PI)*fr)/fr)+','+String(Math.floor(ea.x[3]*(180.0/Math.PI)*fr)/fr);
console.log(str);

//After optimization
var m1 = new N6LMatrix(4).UnitMat();
var q0 = new N6LQuaternion().UnitQuat();//Rotation quaternion
var az1 = new N6LVector(4, true).UnitVec(3);//global z axis
var ay1 = new N6LVector(4, true).UnitVec(2);//global y axis
var ax1 = new N6LVector(4, true).UnitVec(1);//global x axis
var qWK = q0.RotAxisQuat(az1, 10 * Math.PI / 180.0);//10 degree rotation around z axis
qWK = qWK.RotAxisQuat(ay1, 20 * Math.PI / 180.0);//Rotate 20 degrees around the y axis
qWK = qWK.RotAxisQuat(ax1, 30 * Math.PI / 180.0);//Rotate 30 degrees around the x axis
var ansM1 = m1.Mul(qWK.Matrix());//Apply rotation
console.log(ansM1.x);
var ansV1 = ansM1.Vector()//Rotation vector;
console.log(ansV1.x);
// The corresponding XYZ Euler angles for this rotation are 30, 20, 10 degrees.
// Extract Euler angles (XYZ order: 1=X, 2=Y, 3=Z) from matrix ansM1.
var ea1 = ansM1.EulerAngle(1,2,3);
// Format Euler angles as a comma-separated string, converted to degrees and rounded.
var str1 = String(Math.floor(ea1.x[1]*(180.0/Math.PI)*fr)/fr)+','+String(Math.floor(ea1.x[2]*(180.0/Math.PI)*fr)/fr)+','+String(Math.floor(ea1.x[3]*(180.0/Math.PI)*fr)/fr);
console.log(str1);

//Equivalence check//result : "Success!"
if(ansV0.EpsEqual(ansV1, 1e-6)) console.log("Success!");
else console.log("Error!");

* Numerical Stability, Quaternion, and Rounding Error Debugging


Quaternion Normalization and Debugging Pitfalls (SLERP Prerequisites)

Back to Table of contents

Debugging quaternions can be tricky, as seemingly minor deviations can lead to significant issues.
In earlier versions of my implementation (VB+C#+XNA), most normalization-related bugs were already ironed out.
However, during the porting to JavaScript, I found that removing normalization, especially in addition operations,
made the calculations conceptually easier to understand.
This, unfortunately, introduced latent bugs related to quaternion normalization.

Quaternion interpolation methods like LERP (Linear Interpolation) and SLERP (Spherical Linear Interpolation)
heavily rely on the dot product of their vector components for efficient calculation.
In my N6LQuaternion.Slerp2() method, a common approach is to solve SLERP
by leveraging vector addition (or subtraction, depending on the specific algorithm).

The critical issue arises here: if the quaternions involved in these addition (or similar) operations
are not properly normalized, it leads to fatal errors in the interpolation.
While non-normalized quaternions might seem more intuitive for basic addition operations,
they pose a significant risk when used in contexts like SLERP where accurate normalization is paramount.
Therefore, even if additions with non-normalized quaternions appear conceptually simpler,
they are dangerous for rotation-based calculations. Always ensure quaternions are normalized
when their magnitude is critical for operations like rotation or interpolation.


Logarithmic Quaternion Normalization and Debugging Pitfalls (Lie Algebraic Spaces)

・ The Case of Logarithmic Quaternions (ln(q))
The logarithmic quaternion is a mapping from the Lie group to which rotation q
belongs (the rotation group SO(3)) to its Lie algebra
(the velocity space at the instant of rotation).

The space of Lie algebras consists only of information about
the axis and angle of rotation (vector components), and the magnitude of
the rotation axis corresponds to the angle.

ln(q)=[0, (θ/2)v] (v is the axis of rotation, θ is the angle )

In this space, it doesn't matter whether the quaternion has magnitude 1 (it's a unit quaternion).

Rather, the magnitude (norm) of this vector (logarithmic quaternion)
contains information about the rotation angle.

Therefore, if you normalize a logarithmic quaternion, the important
rotation angle information (θ/2) will be lost, and the interpolation will fail.


Efficient Solutions to Floating-Point Rounding Errors (Epsilon Snapping)

Back to Table of contents

Floating-point arithmetic introduces inevitable rounding errors that can silently accumulate and lead to significant bugs,
especially in complex calculations like those involving quaternions and matrices. A highly effective technique to combat
these errors and enforce mathematical precision is to explicitly force values to their theoretical ideal
when they are within an acceptable error margin.

Consider the following code snippet:

if ((NUM - eps < P) && (P < NUM + eps)) {
    P = NUM;
}

Here, NUM represents the theoretically correct value, P is the computed value, and eps (epsilon) is a small tolerance value (e.g., 1e-6).
This code checks if P falls within a small interval around NUM. If it does, P is snapped directly to NUM.

This seemingly simple operation wields immense power against rounding errors. It prevents tiny discrepancies from propagating
and causing issues like NaN (Not-a-Number) in functions such as Math.acos(), which demand arguments strictly
within a -1.0 to 1.0 range. By ensuring that values like dot products, which should theoretically be
1.0, are clamped to 1.0 when they are infinitesimally close, it dramatically enhances the robustness of
your numerical computations, preventing subtle yet critical bugs from manifesting.

Use this method only when the value is theoretically guaranteed to be a specific result.
If the calculated value deviates significantly from the expected range,
it is likely due to a logic error rather than a rounding error;
therefore, you should investigate the cause instead of applying the snap.
The purpose of this process is to eliminate rounding errors,
thereby preventing errors in boundary value checks and avoiding
unnecessary NaN results from inverse trigonometric functions.

Note

Epsilon snapping is a technique for correcting floating-point rounding errors.
Correcting values ​​that deviate significantly from the theoretical value risks masking logic errors.
Consequently, it is recommended that values ​​deviating beyond the tolerance range be treated as
exceptions or errors rather than being corrected.


* Customization


* Input Handling (Keyboard)

N6LKeyBoard: Advanced Keyboard Input Management

Back to Table of contents

The N6LKeyBoard class provides advanced features for managing keyboard input in JavaScript,
going beyond basic event handling. It's designed to support complex input patterns and user customization,
especially useful in games and intricate applications.

Core Concepts of N6LKeyBoard
N6LKeyBoard handles keyboard input based on three main concepts:

1. Real ID:
This is the actual identifier assigned to a physical keyboard key,
often represented by VK_ constants (Virtual Key codes) from the U.S. standard keyboard layout.
Examples include "VK_N1" (numpad 1) or "VK_A" (A key). These are the most fundamental key IDs recognized by the system.

2. Alias ID:
You can assign multiple "aliases" or "alternative names" to a single Real ID or even another Alias ID.
For instance, VK_RETURN (Enter key) can have an alias VK_ENTER. If you want both the spacebar (VK_SPACE)
and the 'W' key (VK_W) to trigger a "JUMP" action, you would create "JUMP" aliases for both their Real IDs.
This allows your program to simply check if the "JUMP" Alias is active, abstracting away the specific physical key.

3. Unity Alias ID:
This concept allows you to group multiple Real IDs or Alias IDs into a single logical entity.
For example, VK_LSHIFT (Left Shift) and VK_RSHIFT (Right Shift) are distinct Real IDs.
If they each have a general VK_SHIFT alias, you could then unify these under a "MODIFIER_SHIFT" Unity Alias.
This simplifies checks like "is any Shift key (left or right) pressed?".
Unity aliases are also crucial for determining when all associated keys for a command have been released,
ensuring robust input handling for combined actions.

N6LKeyBoard Functionality Overview
N6LKeyBoard offers a set of methods to interact with these concepts and efficiently retrieve keyboard states:

  • Initialization and Callbacks:

    • N6LKeyBoard Constructor (via initKeyBoard function):
      Initializes the keyboard manager. As seen in the example <body onload="initKeyBoard(tman, function() { func(); });">,
      it ties a timer manager (tman) with your custom keyboard check method (func).
      This func is regularly called by the timer to monitor key states.
    • N6LKeyBoard.setfunc(func):
      Assigns or changes the keyboard state checking method after initialization.
      • Parameters: func: method
    • N6LKeyBoard.setenable(b):
      This method was introduced to avoid conflicts between monitoring keyboard input and browser text box input, etc.
      Enables or disables keyboard input processing.
      • Parameters: b: enable (bool)
  • ID Conversion and Information Retrieval:

    • N6LKeyBoard.indexof(str):
      Retrieves the internal numeric index (integer) for a given Real ID string.
      This index is then used to access the key's state in the KeyB.keystate array/object.
      • Parameters: str: string (Real ID)
      • Returns: Index of Real ID (integer)
    • N6LKeyBoard.ToAlias(str, ret):
      Converts a Real ID string to its associated Alias ID(s).
      The ret array will be populated with a list of Alias IDs,
      and the method returns the "deepest" Alias ID string (presumably the most specific one).
      • Parameters: str: string (Real ID); ret: Array (list of Alias IDs)
      • Returns: Deepest Alias ID (string)
    • N6LKeyBoard.ToReal(str):
      Converts an Alias ID string to its corresponding Real ID string (physical key identifier).
      • Parameters: str: string (Alias ID)
      • Returns: Real ID (string)
  • Alias and Unity Alias Management:

    • N6LKeyBoard.addAlias(ary):
      Adds a new alias mapping. ary should be an Array in the format [srcID, destID],
      where srcID is the existing Real/Alias ID and destID is the new Alias ID to link to it.
      • Parameters: ary: Array ([srcID, destID])
    • N6LKeyBoard.delAlias(str):
      Removes alias definitions associated with a specified Real ID or Alias ID.
      • Parameters: str: string (Real ID or Alias ID)
    • N6LKeyBoard.addUnityAlias(ary):
      Links multiple IDs (Real IDs, Alias IDs, etc.) to a single Unity Alias ID.
      ary should be an Array in the format [unityAliasID, tiedID1, tiedID2, ...] where unityAliasID is the unified name,
      and tiedIDx are the IDs to be grouped under it.
      • Parameters: ary: Array ([unityAliasID, tiedID, ...])
    • N6LKeyBoard.delUnityAlias(str):
      Deletes the definition of a specified Unity Alias ID.
      • Parameters: str: string (Alias ID)
  • State Retrieval:

    • KeyB.keystate:
      A property (likely an array indexed by internal numeric IDs) that holds the current pressed state of all keys.
      You can check if(KeyB.keystate[KeyB.indexof(KeyB.ToReal("VK_N1"))]) to see if a specific key (like numpad 1) is pressed.
    • N6LKeyBoard.UnityAlias(str):
      Gets the Unity Alias ID string that a given Alias ID string belongs to.
      • Parameters: str: string (Alias ID)
      • Returns: Unity Alias ID (string)
    • N6LKeyBoard.isPressUnityAlias(str):
      Returns a boolean indicating whether a specific Unity Alias ID is currently active
      (i.e., if any key associated with that Unity Alias is pressed).
      This allows checking for complex key combinations or variations using a single logical name.
      • Parameters: str: string (Alias ID)
      • Returns: Press info of Unity Alias ID (boolean)

Key Code / Real ID Table (U.S. Standard Keyboard)
This table lists the "Real IDs" (key codes) used by N6LKeyBoard for a U.S. standard keyboard layout.
0xYX indicates the hexadecimal value of the key ID.

0xYX 0 1 2 3 4 5 6 7
0xYX 8 9 A B C D E F
0x0X VK_$00 VK_LBUTTON VK_RBUTTON VK_CANCEL : Break VK_MBUTTON VK_XBUTTON1 VK_XBUTTON2 VK_$07
0x0X VK_BACK : BackSpace VK_TAB : Tab VK_$0A VK_$0B VK_CLEAR VK_RETURN : Enter VK_$0E VK_$0F
0x1X VK_SHIFT : Shift VK_CONTROL : Ctrl VK_MENU : Alt VK_PAUSE VK_CAPITAL VK_KANA VK_$16 VK_JUNJA
0x1X VK_FINAL VK_KANJI VK_$1A VK_ESCAPE : Esc VK_CONVERT : 変換 VK_NONCONVERT : 無変換 VK_ACCEPT VK_MODCHANGE
0x2X VK_SPACE : Space VK_PRIOR : PgUp VK_NEXT : PgDn VK_END : End VK_HOME : Home VK_LEFT : ← VK_UP : ↑ VK_RIGHT : →
0x2X VK_DOWN : ↓ VK_SELECT VK_PRINT VK_EXECUTE VK_SNAPSHOT : Print Screen VK_INSERT : Ins VK_DELETE : Del VK_HELP
0x3X VK_0 VK_1 VK_2 VK_3 VK_4 VK_5 VK_6 VK_7
0x3X VK_8 VK_9 VK_$3A VK_$3B VK_$3C VK_$3D VK_$3E VK_$3F
0x4X VK_$40 VK_A VK_B VK_C VK_D VK_E VK_F VK_G
0x4X VK_H VK_I VK_J VK_K VK_L VK_M VK_N VK_O
0x5X VK_P VK_Q VK_R VK_S VK_T VK_U VK_V VK_W
0x5X VK_X VK_Y VK_Z VK_LWIN VK_RWIN VK_APPS VK_$5E VK_SLEEP
0x6X VK_NUMPAD0 VK_NUMPAD1 VK_NUMPAD2 VK_NUMPAD3 VK_NUMPAD4 VK_NUMPAD5 VK_NUMPAD6 VK_NUMPAD7
0x6X VK_NUMPAD8 VK_NUMPAD9 VK_MULTIPLY : numpad * VK_ADD : numpad + VK_SEPARATOR : numpad enter VK_SUBTRACT : numpad - VK_DECIMAL : numpad . VK_DIVIDE : numpad /
0x7X VK_F1 VK_F2 VK_F3 VK_F4 VK_F5 VK_F6 VK_F7 VK_F8
0x7X VK_F9 VK_F10 VK_F11 VK_F12 VK_F13 VK_F14 VK_F15 VK_F16
0x8X VK_F17 VK_F18 VK_F19 VK_F20 VK_F21 VK_F22 VK_F23 VK_F24
0x8X VK_$88 VK_$89 VK_$8A VK_$8B VK_$8C VK_$8D VK_$8E VK_$8F
0x9X VK_NUMLOCK : Num Lock VK_SCROLL : Scroll Lock VK_$92 VK_$93 VK_$94 VK_$95 VK_$96 VK_$97
0x9X VK_$98 VK_$99 VK_$9A VK_$9B VK_$9C VK_$9D VK_$9E VK_$9F
0xAX VK_LSHIFT VK_RSHIFT VK_LCONTROL VK_RCONTROL VK_LMENU VK_RMENU VK_BROWSER_BACK VK_BROWSER_FORWARD
0xAX VK_BROWSER_REFRESH VK_BROWSER_STOP VK_BROWSER_SERCH VK_BROWSER_FAVORITES VK_BROWSER_HOME VK_VOLUME_MUTE VK_VOLUME_DOWN VK_VOLUME_UP
0xBX VK_MEDIA_NEXT_TRACK VK_MEDIA_PREV_TRACK VK_MEDIA_STOP VK_MEDIA_PLAY_PAUSE VK_LAUNCH_MAIL VK_LAUNCH_MEDIA_SELECT VK_LAUNCH_APP1 VK_LAUNCH_APP2
0xBX VK_$B8 VK_$B9 VK_OEM_1 : [;: ] VK_OEM_PLUS : [+] VK_OEM_COMMA : [,] VK_OEM_MINUS : [-] VK_OEM_PERIOD : [.] VK_OEM_2 : [/?]
0xCX VK_OEM_3 : [`~ ] VK_$C1 VK_$C2 VK_$C3 VK_$C4 VK_$C5 VK_$C6 VK_$C7
0xCX VK_$C8 VK_$C9 VK_$CA VK_$CB VK_$CC VK_$CD VK_$CE VK_$CF
0xDX VK_$D0 VK_$D1 VK_$D2 VK_$D3 VK_$D4 VK_$D5 VK_$D6 VK_$D7
0xDX VK_$D8 VK_$D9 VK_$DA VK_OEM_4 : [{] VK_OEM_5 : [|] VK_OEM_6 : [}] VK_OEM_7 : ['] VK_OEM_8
0xEX VK_$E0 VK_OEM_AX VK_OEM_102 : [_] VK_ICO_HELP VK_ICO_00 VK_PROCESSKEY VK_ICO_CLEAR VK_PACKET
0xEX VK_$E8 VK_OEM_RESET VK_OEM_JUMP VK_OEM_PA1 VK_OEM_PA2 VK_OEM_PA3 VK_OEM_WSCTRL VK_OEM_CUSEL
0xFX VK_OEM_ATTN VK_OEM_FINISH VK_OEM_COPY VK_OEM_AUTO VK_OEM_ENLW VK_OEM_BACKTAB VK_ATTN VK_CRSEL
0xFX VK_EXSEL VK_EREOF VK_PLAY VK_ZOOM VK_NONAME VK_PA1 VK_OEM_CLEAR VK_$FF

Real ID, Alias ID Mappings
This table lists common mappings between Real IDs and their Alias IDs.

VK_RETURN, VK_ENTER VK_ESCAPE, VK_ESC VK_OEM_MINUS, VK_- VK_OEM_7, VK_^ VK_NUMPAD1, VK_N1 VK_NUMPAD2, VK_N2 VK_NUMPAD3, VK_N3 VK_NUMPAD4, VK_N4
VK_NUMPAD5, VK_N5 VK_NUMPAD6, VK_N6 VK_NUMPAD7, VK_N7 VK_NUMPAD8, VK_N8 VK_NUMPAD9, VK_N9 VK_NUMPAD0, VK_N0 VK_DECIMAL, VK_N. VK_ADD, VK_N+
VK_SUBTRACT, VK_N- VK_MULTIPLY, VK_N* VK_DIVIDE, VK_N/ VK_NUMLOCK, VK_NLK VK_OEM_5, VK_| VK_OEM_3, VK_@ VK_OEM_4, VK_[ VK_OEM_PLUS, VK_;
VK_OEM_1, VK_: VK_OEM_6, VK_] VK_OEM_COMMA, VK_, VK_OEM_PERIOD, VK_. VK_OEM_2, VK_/ VK_OEM_102, VK__ VK_CONTROL, VK_CTRL VK_MENU, VK_ALT
VK_CONVERT, VK_CNVT VK_NONCONVERT, VK_NONCNVT VK_PRIOR, VK_PGUP VK_NEXT, VK_PGDN VK_LEFT, VK_← VK_UP, VK_↑ VK_RIGHT, VK_→ VK_DOWN, VK_↓
VK_INSERT, VK_INS VK_DELETE, VK_DEL VK_SCROLL, VK_SLK VK_SNAPSHOT, VK_PRTSCRN VK_OEM_ATTN, VK_CLK VK_OEM_COPY, VK_KANA VK_OEM_ENLW, VK_ZEN VK_OEM_AUTO, VK_ZEN2
VK_PAUSE, VK_BRK VK_CLEAR, VK_CLS --- --- --- --- --- ---

Key Usage Examples: Checking Key Press State:
You can check if a key is pressed using its Real ID or by converting an Alias ID to a Real ID first:

if (KeyB.keystate[KeyB.indexof(KeyB.ToReal("VK_N1"))]) { // Check if numpad 1 key is down
    // ... your code here
}
  • Managing Aliases:
    • Add an alias:
      KeyB.addAlias(["VK_A", "MOVE_LEFT"]); // 'A' key can now be referred to as "MOVE_LEFT"
    • Delete an alias:
      KeyB.delAlias("MOVE_LEFT"); // Removes the "MOVE_LEFT" alias
  • Managing Unity Aliases:
    • Add a Unity Alias:
      KeyB.addUnityAlias(["MOVE_FORWARD", "VK_W", "VK_UP"]); // Both 'W' key and Up arrow key contribute to "MOVE_FORWARD"
    • Delete a Unity Alias:
      KeyB.delUnityAlias("MOVE_FORWARD");
    • Check Unity Alias press state:
      if (KeyB.isPressUnityAlias("MOVE_FORWARD")) { ... } // Returns true if either 'W' or Up arrow is pressed.
    • Get Unity Alias ID from an Alias ID:
      var unityID = KeyB.UnityAlias("VK_W");

These functions allow you to process multiple physical keys as a single logical input,
enhancing the flexibility and maintainability of your input handling.

How Keyboard Monitoring Works
At its core, N6LKeyBoard detects key presses by trapping the internal index number corresponding to a key's Real ID.
For instance, pressing the space key ultimately triggers a check against its Real ID,
VK_SPACE, and its associated internal index number (typically 0x20).

Here's a breakdown of the key press detection process:

1.Physical Key Press:
The user presses a key on the keyboard, like the spacebar.
2.System/Browser Event:
The operating system or web browser generates an event for this key press.
This event contains the raw key code information, which N6LKeyBoard understands as a Real ID like VK_SPACE.
3.Event Capture:
N6LKeyBoard intercepts this browser event. It then translates the raw key code into its corresponding Real ID
and its unique internal index number (e.g., 0x20 for VK_SPACE).
4.State Update:
N6LKeyBoard updates its internal KeyB.keystate property. For example, KeyB.keystate[0x20] is set to true
to mark the space key as pressed. This crucial step typically happens within the library's event handler,
such as document.onkeydown = function(e) { KeyB.keystate[e.keyCode] = true; ... }.
5.Application Use:
Your application code can then query this state to react to the key press.
You'd typically check it using if (KeyB.keystate[KeyB.indexof("VK_SPACE")]) { ... }.

While N6LKeyBoard offers Alias IDs and Unity Alias IDs for more abstract and flexible input handling
(allowing you to check if (KeyB.isPressUnityAlias("JUMP")) instead of a specific key),
these advanced features build directly upon this fundamental mechanism of monitoring Real IDs
and their corresponding index numbers.

Preventing Conflicts with Browser Input The N6LKeyBoard.setenable(b) method plays a critical role in preventing conflicts between N6LKeyBoard's direct key monitoring
and standard browser input fields like text boxes (<input type="text"> or <textarea>).
When N6LKeyBoard is enabled (setenable(true)), it actively captures key events,
which can interfere with the browser's default text input behavior.
By calling N6LKeyBoard.setenable(false), you temporarily disable N6LKeyBoard's event capturing.
This allows the browser to handle input to text boxes without interference.
You can then re-enable N6LKeyBoard's monitoring when direct keyboard control is needed again,
for instance, during gameplay.

Future Implementation Idea: A Unified N6LInput System

Back to Table of contents

While N6LKeyBoard currently provides robust and flexible management for keyboard input,
an exciting future direction for NAS6LIB could be to expand this into a more comprehensive N6LInput system.
This unified system would aim to abstract away the specific input device, allowing applications to respond
to user actions regardless of whether they come from a keyboard, mouse, gamepad, or touch screen.

The Appeal of a Unified Input System
A single, unified N6LInput layer would offer significant advantages:

  • True Device Agnosticism:
    Application logic wouldn't need to distinguish between input sources. An "ATTACK" action could be triggered
    by a keyboard key, a mouse click, or a gamepad button, all handled through a consistent API.
  • Simplified Application Logic:
    Developers could query a single system for action states, like N6LInput.isActionActive("JUMP"), dramatically simplifying code.
  • Enhanced Customization:
    It would enable powerful user-definable key and button remapping, allowing complex bindings across multiple devices.
  • Easier Cross-Platform Development:
    Differences in input handling across PCs, mobile devices, and consoles could be absorbed by the input layer itself.

The High Technical Hurdles
However, realizing such a comprehensive N6LInput system presents considerable technical challenges:

1.Diverse Device APIs:
Each input device (keyboard, mouse, touch, gamepad) has its own unique set of events, properties,
and interaction paradigms that must be integrated and normalized.
This includes dealing with browser-specific event models, touch gestures, and gamepad polling.
2.Event Normalization and Conflict Resolution:
Raw input events from different sources need to be converted into a common, understandable format.
More critically, handling conflicts and setting priorities
when the same action can be triggered by multiple devices requires intricate logic.
3.Delicate Mouse and Touch Handling:
Unlike keyboard events where return false; can often safely prevent default browser actions,
completely intercepting mouse or touch events can cripple basic web page functionality
(e.g., link clicks, scrolling, text selection). A unified system would need highly granular control
to only prevent defaults where absolutely necessary, without breaking core browser interactions.
4.Complex State Management:
Beyond simple boolean states for key presses, a unified system would need to efficiently manage continuous data
like mouse coordinates, scroll wheel delta, gamepad stick axes, and multi-touch positions and gestures.
5.Advanced Mapping System:
Building a robust internal structure and public API for users to map any physical input (e.g., Gamepad0_ButtonA)
to any logical action (e.g., FIRE_WEAPON) while supporting modifiers (e.g., Shift + Left Click)
is a significant design and implementation task.
6.Performance Optimization:
For real-time applications, processing a constant stream of diverse input data and updating states
without introducing performance bottlenecks requires careful optimization.

While the ambition for a unified N6LInput system is compelling, the extensive complexities involved underscore
why N6LKeyBoard remains a specialized yet powerful solution focused primarily on keyboard input.
Moving forward, any such expansion would require significant development effort and meticulous attention to detail.


* FAQ

FAQ Regarding Three.js Version Upgrades Beyond R148

Back to Table of contents

Q: After upgrading Three.js beyond R148,

my previously working JavaScript code is now throwing errors.

A: Three.js underwent significant changes with R148 (released in December 2022),
primarily driving the full adoption of ES Modules (ESM). This is a fundamental shift:
any JavaScript file that uses the Three.js library or its add-ons must now be loaded using <script type="module"> in your HTML.
This change means that code relying on the global scope, which was common in older versions,
will no longer work by default. Functions and variables defined within a module are not automatically exposed globally.

Below are common errors resulting from this change and their solutions.

1. Changes in Import Path Resolution
ESM uses import statements for module loading. With Three.js R148 and later, path resolution, especially for the core library, has become stricter.

Solution
Before importing, it's highly recommended to define module paths using an importmap within your HTML. This allows for cleaner and more concise import statements in your JavaScript files, making your code easier to read and maintain.

<script type="importmap">
  {
    "imports": {
      "three": "./javascripts/threejs/build/three.module.js",
      "three/addons/": "./javascripts/threejs/examples/jsm/"
    }
  }
</script>

With this importmap, you can now import modules simply in your JavaScript files:

import * as THREE from 'three'; // Resolves to "./javascripts/threejs/build/three.module.js"
import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; // Resolves to "./javascripts/threejs/examples/jsm/controls/OrbitControls.js"

Note on importmap support: importmap is a relatively new Web standard. While widely supported by modern browsers,
some older browser versions might not support it. In such cases, you may need to use a module bundler
(like Vite, Webpack, or Rollup) or specify direct relative/absolute URLs in your import statements.
For larger or more complex projects, adopting a module bundler
is generally recommended for better dependency management and optimization.

2. Deprecation of Inline Scripts
In a <script type="module"> environment, directly embedding JavaScript functions within HTML attributes like onclick
or onload is no longer recommended. Functions defined within a module are not exposed to the global scope,
meaning calling them directly from HTML attributes will lead to "function not defined" errors.

Solution
Remove inline script attributes from your HTML elements and register event handlers within your JavaScript module using addEventListener.
This is the modern and robust way to handle DOM events.

Before (HTML):

<input type='button' value='BTN' id='IDBTN' onclick='BTNFUNC();'>

After (HTML):

<input type='button' value='BTN' id='IDBTN'>

JavaScript (within <script type="module"> or a separate .js file):

// Execute initialization code after the DOM is fully loaded
window.addEventListener("DOMContentLoaded", init);

function init() {
  // Get the button element
  const ELMBTN = document.getElementById('IDBTN');

  // If the button exists, add an event listener
  if (ELMBTN) {
    ELMBTN.addEventListener('click', () => {
      BTNFUNC(); // Call the function defined within or imported into the module
    });
  }

  // Add other initialization code here (e.g., Three.js setup)
}

// Definition of BTNFUNC (must be within this module or a properly imported module)
function BTNFUNC() {
  console.log("Button clicked!");
}

FAQ about modifying X3DomXXX.js

The unload event has been deprecated, so edit X3DomXXX.js
and search for 'unload' and replace it with 'pagehide'.
This should avoid the warning and work as originally designed.
This fix is ​​in x3dom.reload()
and changes the deprecated 'unload' event handler to the alternative 'pagehide'.


Back to Table of contents
Back to NAS6LIB Repository [Links outside the wiki]

Clone this wiki locally