Bilanced simulation to make knots not untangle by themself
This commit is contained in:
+67
-58
@@ -1,137 +1,146 @@
|
||||
export const Vec2 = {
|
||||
Mutate: {
|
||||
add(target, [x, y]) {
|
||||
target[0] += x
|
||||
target[1] += y
|
||||
target[0] += x;
|
||||
target[1] += y;
|
||||
|
||||
return target
|
||||
return target;
|
||||
},
|
||||
scale(target, factor) {
|
||||
target[0] *= factor
|
||||
target[1] *= factor
|
||||
target[0] *= factor;
|
||||
target[1] *= factor;
|
||||
|
||||
return target
|
||||
return target;
|
||||
},
|
||||
normalize(target) {
|
||||
const norm = Vec2.norm2([x, y])
|
||||
target[0] /= norm
|
||||
target[1] /= norm
|
||||
const norm = Vec2.norm2([x, y]);
|
||||
target[0] /= norm;
|
||||
target[1] /= norm;
|
||||
|
||||
return target
|
||||
return target;
|
||||
},
|
||||
set(target, [x, y]) {
|
||||
target[0] = x
|
||||
target[1] = y
|
||||
return target
|
||||
target[0] = x;
|
||||
target[1] = y;
|
||||
return target;
|
||||
},
|
||||
},
|
||||
perpendicular([x, y]) {
|
||||
return [-y, x]
|
||||
return [-y, x];
|
||||
},
|
||||
distance2(v1, v2) {
|
||||
return Vec2.norm2(Vec2.sub(v1, v2))
|
||||
return Vec2.norm2(Vec2.sub(v1, v2));
|
||||
},
|
||||
add([x1, y1], [x2, y2]) {
|
||||
return [x1 + x2, y1 + y2]
|
||||
return [x1 + x2, y1 + y2];
|
||||
},
|
||||
sub([x1, y1], [x2, y2]) {
|
||||
return [x1 - x2, y1 - y2]
|
||||
return [x1 - x2, y1 - y2];
|
||||
},
|
||||
dot([x1, y1], [x2, y2]) {
|
||||
return x1 * x2 + y1 * y2
|
||||
return x1 * x2 + y1 * y2;
|
||||
},
|
||||
scale([x, y], factor) {
|
||||
return [x * factor, y * factor]
|
||||
return [x * factor, y * factor];
|
||||
},
|
||||
norm2([x, y]) {
|
||||
return Math.sqrt(x ** 2 + y ** 2)
|
||||
return Math.sqrt(x ** 2 + y ** 2);
|
||||
},
|
||||
normalize([x, y]) {
|
||||
const norm = Vec2.norm2([x, y])
|
||||
return [x / norm, y / norm]
|
||||
const norm = Vec2.norm2([x, y]);
|
||||
return [x / norm, y / norm];
|
||||
},
|
||||
|
||||
findNearest(curve, pt) {
|
||||
let index = -1
|
||||
let minDist = +Infinity
|
||||
let index = -1;
|
||||
let minDist = +Infinity;
|
||||
|
||||
for (let i = 0; i < curve.length; i++) {
|
||||
const d = Vec2.distance2(curve[i], pt)
|
||||
const d = Vec2.distance2(curve[i], pt);
|
||||
|
||||
if (d < minDist) {
|
||||
index = i
|
||||
minDist = d
|
||||
index = i;
|
||||
minDist = d;
|
||||
}
|
||||
}
|
||||
|
||||
return [index, minDist]
|
||||
return [index, minDist];
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
export const Vec3 = {
|
||||
Mutate: {
|
||||
add(target, [x, y, z]) {
|
||||
target[0] += x
|
||||
target[1] += y
|
||||
target[2] += z
|
||||
target[0] += x;
|
||||
target[1] += y;
|
||||
target[2] += z;
|
||||
|
||||
return target
|
||||
return target;
|
||||
},
|
||||
scale(target, factor) {
|
||||
target[0] *= factor
|
||||
target[1] *= factor
|
||||
target[2] *= factor
|
||||
target[0] *= factor;
|
||||
target[1] *= factor;
|
||||
target[2] *= factor;
|
||||
|
||||
return target
|
||||
return target;
|
||||
},
|
||||
normalize(target) {
|
||||
const norm = Vec3.norm2(target)
|
||||
target[0] /= norm
|
||||
target[1] /= norm
|
||||
target[2] /= norm
|
||||
const norm = Vec3.norm2(target);
|
||||
target[0] /= norm;
|
||||
target[1] /= norm;
|
||||
target[2] /= norm;
|
||||
|
||||
return target
|
||||
return target;
|
||||
},
|
||||
},
|
||||
withMaxLength(v, max) {
|
||||
const d = Vec3.norm2(v);
|
||||
|
||||
if (d > max) {
|
||||
return Vec3.scale(v, max / d);
|
||||
}
|
||||
|
||||
return v;
|
||||
},
|
||||
distance2(v1, v2) {
|
||||
return Vec3.norm2(Vec3.sub(v1, v2))
|
||||
return Vec3.norm2(Vec3.sub(v1, v2));
|
||||
},
|
||||
add([x1, y1, z1], [x2, y2, z2]) {
|
||||
return [x1 + x2, y1 + y2, z1 + z2]
|
||||
return [x1 + x2, y1 + y2, z1 + z2];
|
||||
},
|
||||
sub([x1, y1, z1], [x2, y2, z2]) {
|
||||
return [x1 - x2, y1 - y2, z1 - z2]
|
||||
return [x1 - x2, y1 - y2, z1 - z2];
|
||||
},
|
||||
dot([x1, y1, z1], [x2, y2, z2]) {
|
||||
return x1 * x2 + y1 * y2 + z1 * z2
|
||||
return x1 * x2 + y1 * y2 + z1 * z2;
|
||||
},
|
||||
scale([x, y, z], factor) {
|
||||
return [x * factor, y * factor, z * factor]
|
||||
return [x * factor, y * factor, z * factor];
|
||||
},
|
||||
norm2([x, y, z]) {
|
||||
return Math.sqrt(x ** 2 + y ** 2 + z ** 2)
|
||||
return Math.sqrt(x ** 2 + y ** 2 + z ** 2);
|
||||
},
|
||||
normalize([x, y, z]) {
|
||||
const norm = Vec3.norm2([x, y, z])
|
||||
return [x / norm, y / norm, z / norm]
|
||||
const norm = Vec3.norm2([x, y, z]);
|
||||
return [x / norm, y / norm, z / norm];
|
||||
},
|
||||
findNearest(curve, pt) {
|
||||
let index = -1
|
||||
let minDist = +Infinity
|
||||
let index = -1;
|
||||
let minDist = +Infinity;
|
||||
|
||||
for (let i = 0; i < curve.length; i++) {
|
||||
const d = Vec3.distance2(curve[i], pt)
|
||||
const d = Vec3.distance2(curve[i], pt);
|
||||
|
||||
if (d < minDist) {
|
||||
index = i
|
||||
minDist = d
|
||||
index = i;
|
||||
minDist = d;
|
||||
}
|
||||
}
|
||||
|
||||
return [index, minDist]
|
||||
return [index, minDist];
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
export function clamp(min, value, max) {
|
||||
return Math.min(max, Math.max(min, value))
|
||||
return Math.min(max, Math.max(min, value));
|
||||
}
|
||||
|
||||
@@ -1,171 +1,310 @@
|
||||
import { useEffect, useRef, useState } from 'preact/hooks'
|
||||
import { enforceDistance } from '../../lib/math/constraint.js'
|
||||
import { resampleCurve } from '../../lib/math/curves.js'
|
||||
import { Vec2, Vec3 } from '../../lib/math/math.js'
|
||||
import { MODE_DRAW, MODE_FLIP, MODE_DRAG } from '../pages.jsx'
|
||||
import { useEffect, useRef, useState } from "preact/hooks";
|
||||
import { enforceDistance } from "../../lib/math/constraint.js";
|
||||
import { resampleCurve } from "../../lib/math/curves.js";
|
||||
import { Vec2, Vec3 } from "../../lib/math/math.js";
|
||||
import { MODE_DRAW, MODE_FLIP, MODE_DRAG, MODE_BUTTONS } from "../pages.jsx";
|
||||
|
||||
function mod(i, modulus) {
|
||||
const r = i % modulus
|
||||
return r < 0 ? r + modulus : r
|
||||
const r = i % modulus;
|
||||
return r < 0 ? r + modulus : r;
|
||||
}
|
||||
|
||||
function createSimulation3d(positions, velocities, accelerations) {
|
||||
// function createSimulation3d(positions, velocities, accelerations) {
|
||||
// return {
|
||||
// positions,
|
||||
// velocities,
|
||||
// accelerations,
|
||||
|
||||
// update(newAccelerations) {
|
||||
// const n = this.positions.length
|
||||
// const newPositions = Array.from({ length: n }, () => [])
|
||||
// const newVelocities = Array.from({ length: n }, () => [])
|
||||
|
||||
// for (let i = 0; i < n; i++) {
|
||||
// newPositions[i] = Vec3.add(Vec3.add(this.positions[i], this.velocities[i]), Vec3.scale(this.accelerations[i], 0.5))
|
||||
// newVelocities[i] = Vec3.add(this.velocities[i], Vec3.scale(Vec3.add(this.accelerations[i], newAccelerations[i]), 0.5))
|
||||
// }
|
||||
|
||||
// this.positions = newPositions
|
||||
// this.velocities = newVelocities
|
||||
// this.accelerations = newAccelerations
|
||||
// },
|
||||
// }
|
||||
// }
|
||||
|
||||
function createSimulation3dMaxDisplacement(
|
||||
positions,
|
||||
velocities,
|
||||
accelerations,
|
||||
maxDisplacement
|
||||
) {
|
||||
return {
|
||||
positions,
|
||||
velocities,
|
||||
accelerations,
|
||||
|
||||
update(newAccelerations) {
|
||||
const n = this.positions.length
|
||||
const newPositions = Array.from({ length: n }, () => [])
|
||||
const newVelocities = Array.from({ length: n }, () => [])
|
||||
const n = this.positions.length;
|
||||
const newPositions = Array.from({ length: n }, () => []);
|
||||
const newVelocities = Array.from({ length: n }, () => []);
|
||||
|
||||
for (let i = 0; i < n; i++) {
|
||||
newPositions[i] = Vec3.add(Vec3.add(this.positions[i], this.velocities[i]), Vec3.scale(this.accelerations[i], 0.5))
|
||||
newVelocities[i] = Vec3.add(this.velocities[i], Vec3.scale(Vec3.add(this.accelerations[i], newAccelerations[i]), 0.5))
|
||||
newPositions[i] = Vec3.add(
|
||||
this.positions[i],
|
||||
Vec3.withMaxLength(
|
||||
Vec3.add(
|
||||
this.velocities[i],
|
||||
Vec3.scale(this.accelerations[i], 0.5)
|
||||
),
|
||||
maxDisplacement
|
||||
)
|
||||
);
|
||||
newVelocities[i] = Vec3.add(
|
||||
this.velocities[i],
|
||||
Vec3.scale(
|
||||
Vec3.add(this.accelerations[i], newAccelerations[i]),
|
||||
0.5
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
this.positions = newPositions
|
||||
this.velocities = newVelocities
|
||||
this.accelerations = newAccelerations
|
||||
this.positions = newPositions;
|
||||
this.velocities = newVelocities;
|
||||
this.accelerations = newAccelerations;
|
||||
},
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const NODE_BALL_RADIUS = 30
|
||||
const SEGMENT_LENGTH = 10
|
||||
const NODE_BALL_RADIUS = 30;
|
||||
const SEGMENT_LENGTH = 10;
|
||||
|
||||
const PASSTHROUGH_MIN_INDICES = NODE_BALL_RADIUS / SEGMENT_LENGTH + 1
|
||||
const PASSTHROUGH_MIN_INDICES = NODE_BALL_RADIUS / SEGMENT_LENGTH + 1;
|
||||
|
||||
const INVERT_CROSSING_RADIUS = SEGMENT_LENGTH * 3
|
||||
const INVERT_CROSSING_RADIUS = SEGMENT_LENGTH * 3;
|
||||
|
||||
class KnotSimulation {
|
||||
constructor(knotRef, modeRef) {
|
||||
/** ref allo stato esterno che maneggia anche Preact */
|
||||
this.knotRef = knotRef
|
||||
this.modeRef = modeRef
|
||||
this.knotRef = knotRef;
|
||||
this.modeRef = modeRef;
|
||||
|
||||
/** percorso temporaneo utilizzato mentre si disegna sul layer */
|
||||
this.ghostPath = null
|
||||
this.ghostPath = null;
|
||||
|
||||
/** position where to place the black hole while right-clicking the canvas */
|
||||
this.draggingIndex = null
|
||||
this.draggingIndex = null;
|
||||
|
||||
/** particle simulation that holds positions, velocities and accelerations */
|
||||
this.particleSimulation = null
|
||||
this.particleSimulation = null;
|
||||
}
|
||||
|
||||
onMouseDown(x, y, buttons) {
|
||||
this.mousePosition = [x, y]
|
||||
this.mousePosition = [x, y];
|
||||
|
||||
if (buttons === 1) {
|
||||
switch (this.modeRef.current) {
|
||||
case MODE_DRAW:
|
||||
this.ghostPath = []
|
||||
break;
|
||||
case MODE_FLIP:
|
||||
const { positions } = this.particleSimulation
|
||||
switch (this.modeRef.current) {
|
||||
case MODE_BUTTONS:
|
||||
switch (buttons) {
|
||||
case 1:
|
||||
this.ghostPath = [];
|
||||
break;
|
||||
case 2:
|
||||
const [i] = Vec2.findNearest(
|
||||
this.particleSimulation.positions.map(([x, y]) => [
|
||||
x,
|
||||
y,
|
||||
]),
|
||||
this.mousePosition
|
||||
);
|
||||
this.draggingIndex = i;
|
||||
break;
|
||||
case 4:
|
||||
const { positions } = this.particleSimulation;
|
||||
|
||||
for (let i = 0; i < positions.length; i++) {
|
||||
if (
|
||||
Vec2.distance2([x, y], positions[i]) <
|
||||
INVERT_CROSSING_RADIUS
|
||||
) {
|
||||
positions[i][2] *= -1;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case MODE_DRAW:
|
||||
if (buttons === 1) {
|
||||
this.ghostPath = [];
|
||||
}
|
||||
break;
|
||||
case MODE_FLIP:
|
||||
if (buttons === 1) {
|
||||
const { positions } = this.particleSimulation;
|
||||
|
||||
for (let i = 0; i < positions.length; i++) {
|
||||
if (Vec2.distance2([x, y], positions[i]) < INVERT_CROSSING_RADIUS) {
|
||||
positions[i][2] *= -1
|
||||
if (
|
||||
Vec2.distance2([x, y], positions[i]) <
|
||||
INVERT_CROSSING_RADIUS
|
||||
) {
|
||||
positions[i][2] *= -1;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case MODE_DRAG:
|
||||
}
|
||||
break;
|
||||
case MODE_DRAG:
|
||||
if (buttons === 1) {
|
||||
const [i] = Vec2.findNearest(
|
||||
this.particleSimulation.positions.map(([x, y]) => [x, y]),
|
||||
this.mousePosition
|
||||
)
|
||||
this.draggingIndex = i
|
||||
break;
|
||||
}
|
||||
this.particleSimulation.positions.map(([x, y]) => [
|
||||
x,
|
||||
y,
|
||||
]),
|
||||
this.mousePosition
|
||||
);
|
||||
this.draggingIndex = i;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
onMouseDrag(x, y) {
|
||||
this.mousePosition = [x, y]
|
||||
this.mousePosition = [x, y];
|
||||
|
||||
if (this.ghostPath) {
|
||||
this.ghostPath.push([x, y])
|
||||
this.ghostPath.push([x, y]);
|
||||
}
|
||||
}
|
||||
|
||||
onMouseUp() {
|
||||
if (this.ghostPath) {
|
||||
this.ghostPath.push(this.ghostPath[0])
|
||||
this.ghostPath.push(this.ghostPath[0]);
|
||||
|
||||
const curve = resampleCurve(this.ghostPath, SEGMENT_LENGTH, SEGMENT_LENGTH)
|
||||
curve.pop()
|
||||
const curve = resampleCurve(
|
||||
this.ghostPath,
|
||||
SEGMENT_LENGTH,
|
||||
SEGMENT_LENGTH
|
||||
);
|
||||
curve.pop();
|
||||
|
||||
// convert the 2d curve to a 3d curve
|
||||
// this.setPositions(curve.map(([x, y], i) => [x, y, 0.01 * (Math.random() * 2 - 1)]))
|
||||
this.setPositions(curve.map(([x, y], i) => [x, y, 0.5 * i]))
|
||||
this.setPositions(curve.map(([x, y], i) => [x, y, 0.5 * i]));
|
||||
|
||||
this.ghostPath = null
|
||||
this.ghostPath = null;
|
||||
}
|
||||
|
||||
this.draggingIndex = null
|
||||
this.draggingIndex = null;
|
||||
}
|
||||
|
||||
setPositions(positions) {
|
||||
this.particleSimulation = createSimulation3d(
|
||||
this.particleSimulation = createSimulation3dMaxDisplacement(
|
||||
positions,
|
||||
Array.from({ length: positions.length }, () => [0, 0, 0]),
|
||||
Array.from({ length: positions.length }, () => [0, 0, 0])
|
||||
)
|
||||
Array.from({ length: positions.length }, () => [0, 0, 0]),
|
||||
NODE_BALL_RADIUS * 0.1
|
||||
);
|
||||
}
|
||||
|
||||
update() {
|
||||
if (!this.particleSimulation) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
const { positions } = this.particleSimulation
|
||||
const N = positions.length
|
||||
const { positions } = this.particleSimulation;
|
||||
const N = positions.length;
|
||||
|
||||
const newAccelerations = Array.from({ length: N }, () => [0, 0, 0])
|
||||
console.log(N);
|
||||
|
||||
const newAccelerations = Array.from({ length: N }, () => [0, 0, 0]);
|
||||
|
||||
for (let i = 0; i < N; i++) {
|
||||
Vec3.Mutate.add(newAccelerations[i], [0, 0, -0.1 * Math.sign(positions[i][2])])
|
||||
Vec3.Mutate.add(newAccelerations[i], [
|
||||
0,
|
||||
0,
|
||||
-0.1 * Math.sign(positions[i][2]),
|
||||
]);
|
||||
}
|
||||
|
||||
// dragging point
|
||||
if (this.draggingIndex !== null) {
|
||||
this.particleSimulation.positions[this.draggingIndex] = [...this.mousePosition, 0]
|
||||
}
|
||||
const RANGE = 2;
|
||||
for (
|
||||
let i = this.draggingIndex - RANGE;
|
||||
i <= this.draggingIndex + RANGE;
|
||||
i++
|
||||
) {
|
||||
const node = positions[mod(i, N)];
|
||||
const factor = 1 / (1 + 10 * Math.abs(i - this.draggingIndex));
|
||||
|
||||
// forzo la lunghezza delle barrette
|
||||
for (let i = 0; i < 4; i++) {
|
||||
for (let i = 0; i < N; i++) {
|
||||
const curr = positions.at(i)
|
||||
const next = positions.at((i + 1) % N)
|
||||
|
||||
const [pt1, pt2] = enforceDistance(curr, next, SEGMENT_LENGTH)
|
||||
|
||||
positions[i] = pt1
|
||||
positions[mod(i + 1, N)] = pt2
|
||||
Vec3.Mutate.add(
|
||||
newAccelerations[mod(i, N)],
|
||||
Vec3.Mutate.scale(
|
||||
Vec3.sub([...this.mousePosition, 0], node),
|
||||
factor
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// forzo la lunghezza delle barrette
|
||||
let globalLinkMaxError = +Infinity;
|
||||
let linkIterCount = 0;
|
||||
while (
|
||||
globalLinkMaxError > SEGMENT_LENGTH * 0.01 &&
|
||||
linkIterCount++ < 100
|
||||
) {
|
||||
for (let i = 0; i < N; i++) {
|
||||
const curr = positions.at(i);
|
||||
const next = positions.at((i + 1) % N);
|
||||
|
||||
const [pt1, pt2] = enforceDistance(curr, next, SEGMENT_LENGTH);
|
||||
|
||||
positions[i] = pt1;
|
||||
positions[mod(i + 1, N)] = pt2;
|
||||
}
|
||||
|
||||
globalLinkMaxError = 0;
|
||||
for (let i = 0; i < N; i++) {
|
||||
const curr = positions.at(i);
|
||||
const next = positions.at((i + 1) % N);
|
||||
|
||||
const d = Math.abs(Vec3.distance2(curr, next) - SEGMENT_LENGTH);
|
||||
if (d > globalLinkMaxError) {
|
||||
globalLinkMaxError = d;
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log(
|
||||
`[Link Constraint] Iterations: ${linkIterCount}, Error: ${globalLinkMaxError}`
|
||||
);
|
||||
|
||||
// raddrizzamento dei link
|
||||
for (let i = 0; i < N; i++) {
|
||||
const prev = positions.at((i - 1) % N)
|
||||
const curr = positions.at(i)
|
||||
const next = positions.at((i + 1) % N)
|
||||
const prev = positions.at((i - 1) % N);
|
||||
const curr = positions.at(i);
|
||||
const next = positions.at((i + 1) % N);
|
||||
|
||||
const baseForce = Vec3.scale(Vec3.add(Vec3.sub(prev, curr), Vec3.sub(next, curr)), 0.5)
|
||||
const jointFactor = 0.1
|
||||
const baseForce = Vec3.scale(
|
||||
Vec3.add(Vec3.sub(prev, curr), Vec3.sub(next, curr)),
|
||||
0.5
|
||||
);
|
||||
const jointFactor = 0.1;
|
||||
|
||||
Vec3.Mutate.add(positions.at((i - 1) % N), Vec3.scale(baseForce, -jointFactor / 2))
|
||||
Vec3.Mutate.add(positions[i], Vec3.scale(baseForce, jointFactor))
|
||||
Vec3.Mutate.add(positions.at((i + 1) % N), Vec3.scale(baseForce, -jointFactor / 2))
|
||||
Vec3.Mutate.add(
|
||||
positions.at((i - 1) % N),
|
||||
Vec3.scale(baseForce, -jointFactor / 2)
|
||||
);
|
||||
Vec3.Mutate.add(positions[i], Vec3.scale(baseForce, jointFactor));
|
||||
Vec3.Mutate.add(
|
||||
positions.at((i + 1) % N),
|
||||
Vec3.scale(baseForce, -jointFactor / 2)
|
||||
);
|
||||
}
|
||||
|
||||
// the error of the next constraint will be at most of 90%
|
||||
let globalNodeMinDistance = 0
|
||||
let iter = 0
|
||||
while (globalNodeMinDistance < NODE_BALL_RADIUS * 0.99 && iter++ < 100) {
|
||||
let globalNodeMinDistance = 0;
|
||||
let iter = 0;
|
||||
while (
|
||||
globalNodeMinDistance < NODE_BALL_RADIUS * 0.99 &&
|
||||
iter++ < 100
|
||||
) {
|
||||
// repulsione per ogni coppia
|
||||
for (let i = 0; i < N; i++) {
|
||||
for (let j = i + 1; j < N; j++) {
|
||||
@@ -174,21 +313,25 @@ class KnotSimulation {
|
||||
Math.abs(i - j + N) > PASSTHROUGH_MIN_INDICES &&
|
||||
Math.abs(i - j - N) > PASSTHROUGH_MIN_INDICES
|
||||
) {
|
||||
const p1 = positions[i]
|
||||
const p2 = positions[j]
|
||||
const p1 = positions[i];
|
||||
const p2 = positions[j];
|
||||
|
||||
const dist = Vec3.distance2(p1, p2)
|
||||
const dist = Vec3.distance2(p1, p2);
|
||||
if (dist <= NODE_BALL_RADIUS) {
|
||||
const [newPt1, newPt2] = enforceDistance(p1, p2, NODE_BALL_RADIUS)
|
||||
positions[i] = newPt1
|
||||
positions[j] = newPt2
|
||||
const [newPt1, newPt2] = enforceDistance(
|
||||
p1,
|
||||
p2,
|
||||
NODE_BALL_RADIUS
|
||||
);
|
||||
positions[i] = newPt1;
|
||||
positions[j] = newPt2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// estimating global previous constraint error
|
||||
globalNodeMinDistance = +Infinity
|
||||
globalNodeMinDistance = +Infinity;
|
||||
for (let i = 0; i < N; i++) {
|
||||
for (let j = i + 1; j < N; j++) {
|
||||
if (
|
||||
@@ -196,184 +339,202 @@ class KnotSimulation {
|
||||
Math.abs(i - j + N) > PASSTHROUGH_MIN_INDICES &&
|
||||
Math.abs(i - j - N) > PASSTHROUGH_MIN_INDICES
|
||||
) {
|
||||
const p1 = positions[i]
|
||||
const p2 = positions[j]
|
||||
const p1 = positions[i];
|
||||
const p2 = positions[j];
|
||||
|
||||
const dist = Vec3.distance2(p1, p2)
|
||||
const dist = Vec3.distance2(p1, p2);
|
||||
|
||||
if (dist < globalNodeMinDistance) {
|
||||
globalNodeMinDistance = dist
|
||||
globalNodeMinDistance = dist;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log(
|
||||
`[NAIC Constraint] Iterations: ${iter}, Error: ${globalNodeMinDistance}`
|
||||
);
|
||||
|
||||
console.log(`Reached solution in ${iter} iterations with global min distance ${globalNodeMinDistance}`)
|
||||
|
||||
this.particleSimulation.update(newAccelerations)
|
||||
this.particleSimulation.update(newAccelerations);
|
||||
|
||||
// attrito viscoso
|
||||
for (let i = 0; i < N; i++) {
|
||||
Vec3.Mutate.scale(this.particleSimulation.velocities[i], 0.5)
|
||||
Vec3.Mutate.scale(this.particleSimulation.velocities[i], 0.5);
|
||||
}
|
||||
|
||||
this.knotRef.current.points = this.particleSimulation.positions
|
||||
this.knotRef.current.points = this.particleSimulation.positions;
|
||||
}
|
||||
|
||||
/** @param {CanvasRenderingContext2D} g */
|
||||
render(g) {
|
||||
const w = g.canvas.width
|
||||
const h = g.canvas.height
|
||||
const w = g.canvas.width;
|
||||
const h = g.canvas.height;
|
||||
|
||||
g.clearRect(0, 0, w, h)
|
||||
g.clearRect(0, 0, w, h);
|
||||
|
||||
g.fillStyle = '#fff'
|
||||
g.fillRect(0, 0, w, h)
|
||||
g.fillStyle = "#fff";
|
||||
g.fillRect(0, 0, w, h);
|
||||
|
||||
g.lineWidth = 3
|
||||
g.lineCap = 'round'
|
||||
g.lineJoin = 'round'
|
||||
g.lineWidth = 3;
|
||||
g.lineCap = "round";
|
||||
g.lineJoin = "round";
|
||||
|
||||
g.strokeStyle = '#333'
|
||||
g.strokeStyle = "#333";
|
||||
if (this.ghostPath && this.ghostPath.length > 0) {
|
||||
g.strokeStyle = '#888'
|
||||
g.beginPath()
|
||||
g.strokeStyle = "#888";
|
||||
g.beginPath();
|
||||
{
|
||||
const [x0, y0] = this.ghostPath[0]
|
||||
g.moveTo(x0, y0)
|
||||
const [x0, y0] = this.ghostPath[0];
|
||||
g.moveTo(x0, y0);
|
||||
for (const [x, y] of this.ghostPath) {
|
||||
g.lineTo(x, y)
|
||||
g.lineTo(x, y);
|
||||
}
|
||||
}
|
||||
g.stroke()
|
||||
g.stroke();
|
||||
|
||||
g.save()
|
||||
g.save();
|
||||
{
|
||||
g.strokeStyle = '#080'
|
||||
g.lineWidth = 3
|
||||
g.setLineDash([6, 6])
|
||||
g.beginPath()
|
||||
g.moveTo(...this.ghostPath.at(-1))
|
||||
g.lineTo(...this.ghostPath.at(0))
|
||||
g.stroke()
|
||||
g.strokeStyle = "#080";
|
||||
g.lineWidth = 3;
|
||||
g.setLineDash([6, 6]);
|
||||
g.beginPath();
|
||||
g.moveTo(...this.ghostPath.at(-1));
|
||||
g.lineTo(...this.ghostPath.at(0));
|
||||
g.stroke();
|
||||
}
|
||||
g.restore()
|
||||
g.restore();
|
||||
}
|
||||
|
||||
if (!this.particleSimulation) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
const positions = this.particleSimulation.positions
|
||||
const particleCount = positions.length
|
||||
const positions = this.particleSimulation.positions;
|
||||
const particleCount = positions.length;
|
||||
|
||||
if (particleCount > 0) {
|
||||
const sortedZ = [...positions]
|
||||
.map(([, , z], i) => [i, z])
|
||||
.sort(([, z1], [, z2]) => z2 - z1)
|
||||
.map(([i]) => i)
|
||||
.map(([i]) => i);
|
||||
|
||||
g.strokeStyle = '#333'
|
||||
sortedZ.forEach(i => {
|
||||
g.strokeStyle = "#333";
|
||||
sortedZ.forEach((i) => {
|
||||
function toProjection([x, y, z]) {
|
||||
// return [x + z, y + z]
|
||||
return [x, y]
|
||||
return [x, y];
|
||||
}
|
||||
|
||||
const prev2 = toProjection(positions.at((i - 2) % particleCount))
|
||||
const prev = toProjection(positions.at((i - 1) % particleCount))
|
||||
const curr = toProjection(positions.at(i % particleCount))
|
||||
const next = toProjection(positions.at((i + 1) % particleCount))
|
||||
const next2 = toProjection(positions.at((i + 2) % particleCount))
|
||||
const prev2 = toProjection(
|
||||
positions.at((i - 2) % particleCount)
|
||||
);
|
||||
const prev = toProjection(
|
||||
positions.at((i - 1) % particleCount)
|
||||
);
|
||||
const curr = toProjection(positions.at(i % particleCount));
|
||||
const next = toProjection(
|
||||
positions.at((i + 1) % particleCount)
|
||||
);
|
||||
const next2 = toProjection(
|
||||
positions.at((i + 2) % particleCount)
|
||||
);
|
||||
|
||||
g.save()
|
||||
g.save();
|
||||
{
|
||||
g.strokeStyle = '#fff'
|
||||
g.lineWidth = 12
|
||||
g.lineJoin = 'round'
|
||||
g.lineCap = 'butt'
|
||||
g.strokeStyle = "#fff";
|
||||
g.lineWidth = 12;
|
||||
g.lineJoin = "round";
|
||||
g.lineCap = "butt";
|
||||
|
||||
g.beginPath()
|
||||
g.moveTo(...prev)
|
||||
g.lineTo(...curr)
|
||||
g.lineTo(...next)
|
||||
g.stroke()
|
||||
g.beginPath();
|
||||
g.moveTo(...prev);
|
||||
g.lineTo(...curr);
|
||||
g.lineTo(...next);
|
||||
g.stroke();
|
||||
}
|
||||
g.restore()
|
||||
g.restore();
|
||||
|
||||
g.save()
|
||||
g.save();
|
||||
{
|
||||
g.strokeStyle = '#333'
|
||||
g.lineWidth = 3
|
||||
g.lineCap = 'round'
|
||||
g.lineJoin = 'round'
|
||||
g.strokeStyle = "#333";
|
||||
g.lineWidth = 3;
|
||||
g.lineCap = "round";
|
||||
g.lineJoin = "round";
|
||||
|
||||
g.beginPath()
|
||||
g.moveTo(...prev2)
|
||||
g.moveTo(...prev)
|
||||
g.lineTo(...curr)
|
||||
g.lineTo(...next)
|
||||
g.lineTo(...next2)
|
||||
g.stroke()
|
||||
g.beginPath();
|
||||
g.moveTo(...prev2);
|
||||
g.moveTo(...prev);
|
||||
g.lineTo(...curr);
|
||||
g.lineTo(...next);
|
||||
g.lineTo(...next2);
|
||||
g.stroke();
|
||||
}
|
||||
g.restore()
|
||||
})
|
||||
g.restore();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const KnotLayer = ({ knotRef, modeRef }) => {
|
||||
const canvasRef = useRef(null)
|
||||
const [knotSim] = useState(() => new KnotSimulation(knotRef, modeRef))
|
||||
const canvasRef = useRef(null);
|
||||
const [knotSim] = useState(() => new KnotSimulation(knotRef, modeRef));
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('resize', () => {
|
||||
// trigger repaint and get a new context
|
||||
})
|
||||
}, [])
|
||||
window.addEventListener("resize", () => {
|
||||
const $canvas = canvasRef.current;
|
||||
$canvas.width = $canvas.offsetWidth;
|
||||
$canvas.height = $canvas.offsetHeight;
|
||||
$canvas.graphicsContext = $canvas.getContext("2d");
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
let simTimerHandle
|
||||
let simTimerHandle;
|
||||
|
||||
if (canvasRef.current) {
|
||||
const $canvas = canvasRef.current
|
||||
$canvas.width = $canvas.offsetWidth
|
||||
$canvas.height = $canvas.offsetHeight
|
||||
$canvas.graphicsContext = $canvas.getContext('2d')
|
||||
const $canvas = canvasRef.current;
|
||||
$canvas.width = $canvas.offsetWidth;
|
||||
$canvas.height = $canvas.offsetHeight;
|
||||
$canvas.graphicsContext = $canvas.getContext("2d");
|
||||
|
||||
simTimerHandle = setInterval(() => {
|
||||
knotSim.update()
|
||||
requestAnimationFrame(() => knotSim.render($canvas.graphicsContext))
|
||||
}, 1000 / 60)
|
||||
knotSim.update();
|
||||
requestAnimationFrame(() =>
|
||||
knotSim.render($canvas.graphicsContext)
|
||||
);
|
||||
}, 1000 / 120);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (simTimerHandle) {
|
||||
clearInterval(simTimerHandle)
|
||||
clearInterval(simTimerHandle);
|
||||
}
|
||||
}
|
||||
}, [canvasRef.current])
|
||||
};
|
||||
}, [canvasRef.current]);
|
||||
|
||||
return (
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
onPointerDown={e => {
|
||||
knotSim.onMouseDown(e.offsetX, e.offsetY, e.buttons)
|
||||
onPointerDown={(e) => {
|
||||
knotSim.onMouseDown(e.offsetX, e.offsetY, e.buttons);
|
||||
}}
|
||||
onPointerMove={e => {
|
||||
onPointerMove={(e) => {
|
||||
if (canvasRef.current && e.buttons > 0) {
|
||||
knotSim.onMouseDrag(e.offsetX, e.offsetY, e.buttons)
|
||||
requestAnimationFrame(() => knotSim.render(canvasRef.current.graphicsContext))
|
||||
knotSim.onMouseDrag(e.offsetX, e.offsetY, e.buttons);
|
||||
requestAnimationFrame(() =>
|
||||
knotSim.render(canvasRef.current.graphicsContext)
|
||||
);
|
||||
}
|
||||
}}
|
||||
onContextMenu={e => e.preventDefault()}
|
||||
onPointerUp={e => {
|
||||
onContextMenu={(e) => e.preventDefault()}
|
||||
onPointerUp={(e) => {
|
||||
if (canvasRef.current) {
|
||||
knotSim.onMouseUp()
|
||||
requestAnimationFrame(() => knotSim.render(canvasRef.current.graphicsContext))
|
||||
knotSim.onMouseUp();
|
||||
requestAnimationFrame(() =>
|
||||
knotSim.render(canvasRef.current.graphicsContext)
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
@@ -13,13 +13,14 @@ function filterClasses(...classes) {
|
||||
return classes.filter(Boolean).join(' ')
|
||||
}
|
||||
|
||||
export const MODE_BUTTONS = 'buttons'
|
||||
export const MODE_DRAW = 'draw'
|
||||
export const MODE_FLIP = 'flip'
|
||||
export const MODE_DRAG = 'drag'
|
||||
|
||||
export const PageKnotEditor = ({}) => {
|
||||
const knot1 = useRef({ points: [] })
|
||||
const [mode, setMode] = useState(MODE_DRAW)
|
||||
const [mode, setMode] = useState(MODE_BUTTONS)
|
||||
|
||||
const modeRef = useRef(mode)
|
||||
modeRef.current = mode
|
||||
@@ -36,6 +37,9 @@ export const PageKnotEditor = ({}) => {
|
||||
<div class="label">Tools</div>
|
||||
<div class="content">
|
||||
<div class="compound-select">
|
||||
<button class={filterClasses("icon", mode === MODE_BUTTONS && 'selected')} onClick={() => setMode(MODE_BUTTONS)}>
|
||||
<span class="material-symbols-outlined">mouse</span>
|
||||
</button>
|
||||
<button class={filterClasses("icon", mode === MODE_DRAW && 'selected')} onClick={() => setMode(MODE_DRAW)}>
|
||||
<span class="material-symbols-outlined">gesture</span>
|
||||
</button>
|
||||
|
||||
@@ -230,6 +230,10 @@ main {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
filter: invert(100%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
"name": "frontend",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite --clearScreen false",
|
||||
"dev": "vite --host --clearScreen false",
|
||||
"build": "vite build"
|
||||
},
|
||||
"author": "aziis98 <antonio.delucreziis@gmail.com>",
|
||||
|
||||
Reference in New Issue
Block a user