Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions docs/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ export const examples: {
readonly physics: {
readonly createFoldConstraintRegistry: 'examples/foldSetup.ts';
readonly createCubicBarrier: 'examples/foldCubicBarrier.ts';
readonly computeFrozenStiffness: 'examples/foldStiffness.ts';
};
readonly performance: {
readonly debounce: 'examples/requestDedup.ts';
Expand Down Expand Up @@ -3313,6 +3314,24 @@ export interface CubicBarrierOptions {
}
export function createCubicBarrier(options?: CubicBarrierOptions): FoldConstraint;

/**
* Stiffness design principle helper for frozen Fold barriers.
* Use for: computing \bar\kappa using mass and Hessian terms.
* Import: physics/fold/stiffness.ts
*/
export interface StiffnessDesignInput {
gap: number;
effectiveMass: number;
direction: Vector3D;
hessian: Matrix3x3;
}
export interface StiffnessDesignOptions {
epsilon?: number;
min?: number;
max?: number;
}
export function computeFrozenStiffness(input: StiffnessDesignInput, options?: StiffnessDesignOptions): number;

export type FoldConstraintType =
| 'cubic-barrier'
| 'contact-barrier'
Expand Down
14 changes: 14 additions & 0 deletions examples/foldStiffness.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { computeFrozenStiffness } from '../src/index.js';

const stiffness = computeFrozenStiffness({
gap: -0.02,
effectiveMass: 0.5,
direction: { x: 0, y: 0, z: 1 },
hessian: [
[2, 0, 0],
[0, 2, 0],
[0, 0, 2],
],
});

console.log('stiffness', stiffness);
5 changes: 4 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ export const examples = {
physics: {
createFoldConstraintRegistry: 'examples/foldSetup.ts',
createCubicBarrier: 'examples/foldCubicBarrier.ts',
computeFrozenStiffness: 'examples/foldStiffness.ts',
},
performance: {
debounce: 'examples/requestDedup.ts',
Expand Down Expand Up @@ -1188,7 +1189,7 @@ export type { ClosestPairResult } from './geometry/closestPair.js';
// ⚙️ PHYSICS & FOLD BARRIERS
// ============================================================================

export { createFoldConstraintRegistry, createCubicBarrier } from './physics/fold/index.js';
export { createFoldConstraintRegistry, createCubicBarrier, computeFrozenStiffness } from './physics/fold/index.js';

export type {
CubicBarrierOptions,
Expand All @@ -1201,6 +1202,8 @@ export type {
FoldConstraintRegistry,
FoldSolverSettings,
FoldSystemState,
StiffnessDesignInput,
StiffnessDesignOptions,
} from './physics/fold/index.js';

// ============================================================================
Expand Down
1 change: 1 addition & 0 deletions src/physics/fold/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './types.js';
export * from './cubicBarrier.js';
export * from './stiffness.js';
114 changes: 114 additions & 0 deletions src/physics/fold/stiffness.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import type { Matrix3x3, Vector3D } from '../../types.js';

const DEFAULT_EPSILON = 1e-8;

export interface StiffnessDesignInput {
gap: number;
effectiveMass: number;
direction: Vector3D;
hessian: Matrix3x3;
}

export interface StiffnessDesignOptions {
epsilon?: number;
min?: number;
max?: number;
}

export function computeFrozenStiffness(
input: StiffnessDesignInput,
options: StiffnessDesignOptions = {}
): number {
validateInput(input);
const epsilon = options.epsilon ?? DEFAULT_EPSILON;

const gapMagnitude = Math.max(Math.abs(input.gap), epsilon);
const unit = normalise(input.direction);
if (!unit) {
return clamp(input.effectiveMass / (gapMagnitude * gapMagnitude), options.min, options.max);
}

const massContribution = input.effectiveMass / (gapMagnitude * gapMagnitude);
const projected = projectHessian(unit, input.hessian);
const stiffness = massContribution + projected;
return clamp(stiffness, options.min, options.max);
}

function validateInput(input: StiffnessDesignInput): void {
if (!isFiniteNumber(input.gap)) {
throw new TypeError('gap must be a finite number.');
}
if (!isFiniteNumber(input.effectiveMass)) {
throw new TypeError('effectiveMass must be a finite number.');
}
if (!isVector(input.direction)) {
throw new TypeError('direction must be a finite 3D vector.');
}
if (!isMatrix(input.hessian)) {
throw new TypeError('hessian must be a 3x3 matrix.');
}
}

function projectHessian(direction: Vector3D, hessian: Matrix3x3): number {
const hx = hessian[0];
const hy = hessian[1];
const hz = hessian[2];
if (!hx || !hy || !hz) {
throw new TypeError('hessian must be a 3x3 matrix.');
}
const vx = direction.x;
const vy = direction.y;
const vz = direction.z;
const hvx = hx[0] * vx + hx[1] * vy + hx[2] * vz;
const hvy = hy[0] * vx + hy[1] * vy + hy[2] * vz;
const hvz = hz[0] * vx + hz[1] * vy + hz[2] * vz;
return hvx * vx + hvy * vy + hvz * vz;
}

function normalise(vector: Vector3D): Vector3D | null {
const length = Math.hypot(vector.x, vector.y, vector.z);
if (length <= 0) {
return null;
}
return {
x: vector.x / length,
y: vector.y / length,
z: vector.z / length,
};
}

function clamp(value: number, min?: number, max?: number): number {
let result = value;
if (typeof min === 'number') {
result = Math.max(result, min);
}
if (typeof max === 'number') {
result = Math.min(result, max);
}
return result;
}

function isFiniteNumber(value: unknown): value is number {
return typeof value === 'number' && Number.isFinite(value);
}

function isVector(vector: Vector3D | undefined): vector is Vector3D {
return (
typeof vector?.x === 'number' && Number.isFinite(vector.x) &&
typeof vector?.y === 'number' && Number.isFinite(vector.y) &&
typeof vector?.z === 'number' && Number.isFinite(vector.z)
);
}

function isMatrix(matrix: Matrix3x3 | undefined): matrix is Matrix3x3 {
return (
Array.isArray(matrix) &&
matrix.length === 3 &&
matrix.every(
(row) =>
Array.isArray(row) &&
row.length === 3 &&
row.every((value) => typeof value === 'number' && Number.isFinite(value))
)
);
}
2 changes: 2 additions & 0 deletions tests/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ describe('package entry point', () => {
expect(examples.spatial.raycastBvh).toBe('examples/bvh.ts');
expect(examples.physics.createFoldConstraintRegistry).toBe('examples/foldSetup.ts');
expect(examples.physics.createCubicBarrier).toBe('examples/foldCubicBarrier.ts');
expect(examples.physics.computeFrozenStiffness).toBe('examples/foldStiffness.ts');
});

it('provides strong typing for example categories and names', () => {
Expand Down Expand Up @@ -201,6 +202,7 @@ describe('package entry point', () => {
expectTypeOf<ExampleName<'physics'>>().toEqualTypeOf<
| 'createFoldConstraintRegistry'
| 'createCubicBarrier'
| 'computeFrozenStiffness'
>();

expectTypeOf<ExampleName<'ai'>>().toEqualTypeOf<
Expand Down
39 changes: 39 additions & 0 deletions tests/stiffness.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { describe, expect, it } from 'vitest';

import { computeFrozenStiffness } from '../src/physics/fold/stiffness.js';

describe('computeFrozenStiffness', () => {
it('matches design principle formula for basic inputs', () => {
const result = computeFrozenStiffness({
gap: 0.1,
effectiveMass: 2,
direction: { x: 0, y: 0, z: 1 },
hessian: [
[1, 0, 0],
[0, 1, 0],
[0, 0, 3],
],
});

const expected = 2 / (0.1 * 0.1) + 3;
expect(result).toBeCloseTo(expected, 6);
});

it('clamps values when bounds provided', () => {
const result = computeFrozenStiffness(
{
gap: 0,
effectiveMass: 1,
direction: { x: 0, y: 0, z: 0 },
hessian: [
[0, 0, 0],
[0, 0, 0],
[0, 0, 0],
],
},
{ max: 1e5 }
);

expect(result).toBeLessThanOrEqual(1e5);
});
});