452 lines
22 KiB
JavaScript
Raw Permalink Normal View History

2026-08-18 16:06:28 +08:00
 function anglesForBladeCount(count) {
var safeCount = SUPPORTED_BLADE_COUNTS.includes(Number(count)) ? Number(count) : 11;
var half = (safeCount - 1) / 2;
return Array.from({
length: safeCount
}, (_, index) => (index - half) * BLADE_ANGLE_STEP);
}
function syncActiveBladeConfigs() {
var start = Math.floor((COMMON_ANGLES.length - state.bladeCount) / 2);
state.bladeConfigs = state.bladeConfigSlots.slice(start, start + state.bladeCount);
}
function outlineExtents(angles) {
var minX = Infinity
, maxX = -Infinity
, minY = Infinity
, maxY = -Infinity;
angles.forEach( (angle) => {
FAN.outline.forEach( ([x,y]) => {
var point = rotatePoint(x, y, angle);
minX = Math.min(minX, point[0]);
maxX = Math.max(maxX, point[0]);
minY = Math.min(minY, point[1]);
maxY = Math.max(maxY, point[1]);
}
);
}
);
return {
minX,
maxX,
minY,
maxY
};
}
function updateActiveBounds() {
var full = outlineExtents(anglesForBladeCount(11));
var active = outlineExtents(FAN.angles);
var lowerPadding = full.minY - FAN.yMin;
var upperPadding = FAN.yMax - full.maxY;
state.activeBounds = {
xMin: FAN.xMin,
xMax: FAN.xMax,
yMin: active.minY - lowerPadding,
yMax: active.maxY + upperPadding
};
}
function modelBounds() {
return state.activeBounds || {
xMin: FAN.xMin,
xMax: FAN.xMax,
yMin: FAN.yMin,
yMax: FAN.yMax
};
}
function widthAt(radius) {
if (FAN.headArc && radius >= FAN.headArc.startRadius) {
var offset = radius - FAN.headArc.centerRadius;
return Math.sqrt(Math.max(0, FAN.headArc.radius ** 2 - offset ** 2));
}
var p = FAN.profile;
for (let i = 0; i < p.length - 1; i++) {
if (radius >= p[i][0] && radius <= p[i + 1][0]) {
var t = (radius - p[i][0]) / (p[i + 1][0] - p[i][0]);
return p[i][1] + (p[i + 1][1] - p[i][1]) * t;
}
}
return radius < p[0][0] ? p[0][1] : p[p.length - 1][1];
}
function rotatePoint(x, y, angleDegrees) {
var a = deg(angleDegrees)
, c = Math.cos(a)
, s = Math.sin(a);
return [x * c - y * s, x * s + y * c];
}
function decodeBase64(base64) {
var binary = atob(base64);
var bytes = new Uint8Array(binary.length);
var chunk = 1 << 15;
for (let start = 0; start < binary.length; start += chunk) {
var end = Math.min(start + chunk, binary.length);
for (let i = start; i < end; i++)
bytes[i] = binary.charCodeAt(i);
}
return bytes;
}
function parseBinarySTL(base64) {
var bytes = decodeBase64(base64);
var view = new DataView(bytes.buffer,bytes.byteOffset,bytes.byteLength);
var count = view.getUint32(80, true);
if (84 + count * 50 > bytes.byteLength)
throw new Error('内置骨架 STL 数据不完整');
var positions = new Float32Array(count * 9);
var normals = new Float32Array(count * 9);
var offset = 84;
for (let t = 0; t < count; t++,
offset += 50) {
var nx = view.getFloat32(offset, true)
, ny = view.getFloat32(offset + 4, true)
, nz = view.getFloat32(offset + 8, true);
var out = t * 9;
for (let v = 0; v < 3; v++) {
var source = offset + 12 + v * 12;
positions[out + v * 3] = view.getFloat32(source, true);
positions[out + v * 3 + 1] = view.getFloat32(source + 4, true);
positions[out + v * 3 + 2] = view.getFloat32(source + 8, true);
normals[out + v * 3] = nx;
normals[out + v * 3 + 1] = ny;
normals[out + v * 3 + 2] = nz;
}
}
return {
positions,
normals,
count
};
}
function assembleFrameFromTemplates() {
if (!state.frameTemplates)
throw new Error('扇叶骨架模板尚未载入');
// 叶片编号按预览画面从右向左排列:第 1 片位于右端,最后一片位于左端。
// 首尾护骨是非对称件,必须与画面方位一致,否则导出后会装反。
var roles = FAN.angles.map( (angle, index) => index === 0 ? 'right' : (index === FAN.angles.length - 1 ? 'left' : 'inner'));
var totalLength = roles.reduce( (sum, role) => sum + state.frameTemplates[role].positions.length, 0);
var positions = new Float32Array(totalLength);
var normals = new Float32Array(totalLength);
var output = 0;
roles.forEach( (role, bladeIndex) => {
var template = state.frameTemplates[role];
var radians = deg(FAN.angles[bladeIndex]);
var cosine = Math.cos(radians)
, sine = Math.sin(radians);
for (let index = 0; index < template.positions.length; index += 3) {
var x = template.positions[index]
, y = template.positions[index + 1];
positions[output + index] = x * cosine - y * sine;
positions[output + index + 1] = x * sine + y * cosine;
positions[output + index + 2] = template.positions[index + 2];
var nx = template.normals[index]
, ny = template.normals[index + 1];
normals[output + index] = nx * cosine - ny * sine;
normals[output + index + 1] = nx * sine + ny * cosine;
normals[output + index + 2] = template.normals[index + 2];
}
output += template.positions.length;
}
);
return {
positions,
normals,
count: totalLength / 9
};
}
function rebuildFrameFromTemplates() {
var frame = assembleFrameFromTemplates();
state.frameOriginalPositions = frame.positions;
state.frameOriginalNormals = frame.normals;
state.framePositions = frame.positions;
state.frameNormals = frame.normals;
state.frameRemovedTriangles = 0;
state.frameWelds = null;
prepareFrameWelds();
}
function removeDetachedPivotParts(mesh) {
var triangleCount = mesh.count;
if (triangleCount < 2)
return {
...mesh,
removedTriangles: 0,
componentCount: 1
};
var parent = new Int32Array(triangleCount);
var rank = new Uint8Array(triangleCount);
for (let triangle = 0; triangle < triangleCount; triangle++)
parent[triangle] = triangle;
var find = (item) => {
var root = item;
while (parent[root] !== root)
root = parent[root];
while (parent[item] !== item) {
var next = parent[item];
parent[item] = root;
item = next;
}
return root;
}
;
var union = (a, b) => {
var rootA = find(a)
, rootB = find(b);
if (rootA === rootB)
return;
if (rank[rootA] < rank[rootB])
[rootA,rootB] = [rootB, rootA];
parent[rootB] = rootA;
if (rank[rootA] === rank[rootB])
rank[rootA]++;
}
;
var vertexIds = new Int32Array(triangleCount * 3);
var vertexTable = new Map();
var vertexCount = 0;
for (let triangle = 0; triangle < triangleCount; triangle++) {
var triangleOffset = triangle * 9;
for (let vertex = 0; vertex < 3; vertex++) {
var offset = triangleOffset + vertex * 3;
var key = `${Math.round(mesh.positions[offset] * 10000)},${Math.round(mesh.positions[offset + 1] * 10000)},${Math.round(mesh.positions[offset + 2] * 10000)}`;
var vertexId = vertexTable.get(key);
if (vertexId === undefined) {
vertexId = vertexCount++;
vertexTable.set(key, vertexId);
}
vertexIds[triangle * 3 + vertex] = vertexId;
}
}
var edgeOwners = new Map();
for (let triangle = 0; triangle < triangleCount; triangle++) {
var offset = triangle * 3;
var ids = [vertexIds[offset], vertexIds[offset + 1], vertexIds[offset + 2]];
for (const [aIndex,bIndex] of [[0, 1], [1, 2], [2, 0]]) {
var a = ids[aIndex]
, b = ids[bIndex];
var key = a < b ? `${a}:${b}` : `${b}:${a}`;
var owner = edgeOwners.get(key);
if (owner === undefined)
edgeOwners.set(key, triangle);
else
union(triangle, owner);
}
}
var componentSizes = new Map();
var largestRoot = 0;
var largestSize = 0;
for (let triangle = 0; triangle < triangleCount; triangle++) {
var root = find(triangle);
var size = (componentSizes.get(root) || 0) + 1;
componentSizes.set(root, size);
if (size > largestSize) {
largestSize = size;
largestRoot = root;
}
}
if (componentSizes.size === 1)
return {
...mesh,
removedTriangles: 0,
componentCount: 1
};
var positions = new Float32Array(largestSize * 9);
var normals = new Float32Array(largestSize * 9);
var outputOffset = 0;
for (let triangle = 0; triangle < triangleCount; triangle++) {
if (find(triangle) !== largestRoot)
continue;
var inputOffset = triangle * 9;
positions.set(mesh.positions.subarray(inputOffset, inputOffset + 9), outputOffset);
normals.set(mesh.normals.subarray(inputOffset, inputOffset + 9), outputOffset);
outputOffset += 9;
}
return {
positions,
normals,
count: largestSize,
removedTriangles: triangleCount - largestSize,
componentCount: componentSizes.size
};
}
function triangleNormal(a, b, c) {
var ux = b[0] - a[0]
, uy = b[1] - a[1]
, uz = b[2] - a[2];
var vx = c[0] - a[0]
, vy = c[1] - a[1]
, vz = c[2] - a[2];
var nx = uy * vz - uz * vy;
var ny = uz * vx - ux * vz;
var nz = ux * vy - uy * vx;
var len = Math.hypot(nx, ny, nz) || 1;
return [nx / len, ny / len, nz / len];
}
function createMeshWriter(triangleCapacity) {
return {
positions: new Float32Array(triangleCapacity * 9),
normals: new Float32Array(triangleCapacity * 9),
offset: 0
};
}
function pushTri(mesh, a, b, c) {
var n = triangleNormal(a, b, c);
var offset = mesh.offset;
mesh.positions[offset] = a[0];
mesh.positions[offset + 1] = a[1];
mesh.positions[offset + 2] = a[2];
mesh.positions[offset + 3] = b[0];
mesh.positions[offset + 4] = b[1];
mesh.positions[offset + 5] = b[2];
mesh.positions[offset + 6] = c[0];
mesh.positions[offset + 7] = c[1];
mesh.positions[offset + 8] = c[2];
for (let vertex = 0; vertex < 3; vertex++) {
mesh.normals[offset] = n[0];
mesh.normals[offset + 1] = n[1];
mesh.normals[offset + 2] = n[2];
offset += 3;
}
mesh.offset = offset;
}
function pushTriFacing(mesh, a, b, c, upward) {
var normal = triangleNormal(a, b, c);
var facesUp = normal[2] >= 0;
if (facesUp === upward)
pushTri(mesh, a, b, c);
else
pushTri(mesh, a, c, b);
}
function triangulatePairPolygon(pairs) {
var cross2D = (a, b, c) => {
var pa = a.bottom
, pb = b.bottom
, pc = c.bottom;
return (pb[0] - pa[0]) * (pc[1] - pa[1]) - (pb[1] - pa[1]) * (pc[0] - pa[0]);
}
;
var signedArea = pairs.reduce( (sum, pair, index) => {
var next = pairs[(index + 1) % pairs.length];
return sum + pair.bottom[0] * next.bottom[1] - next.bottom[0] * pair.bottom[1];
}
, 0);
var indices = pairs.map( (pair, index) => index);
if (signedArea < 0)
indices.reverse();
var triangles = [];
var pointInside = (point, a, b, c) => {
var c1 = cross2D(a, b, point);
var c2 = cross2D(b, c, point);
var c3 = cross2D(c, a, point);
return c1 >= -1e-8 && c2 >= -1e-8 && c3 >= -1e-8;
}
;
while (indices.length > 3) {
var clipped = false;
for (let position = 0; position < indices.length; position++) {
var previous = indices[(position + indices.length - 1) % indices.length];
var current = indices[position];
var next = indices[(position + 1) % indices.length];
if (cross2D(pairs[previous], pairs[current], pairs[next]) <= 1e-8)
continue;
var containsPoint = indices.some( (candidate) => candidate !== previous && candidate !== current && candidate !== next && pointInside(pairs[candidate], pairs[previous], pairs[current], pairs[next]));
if (containsPoint)
continue;
triangles.push([pairs[previous], pairs[current], pairs[next]]);
indices.splice(position, 1);
clipped = true;
break;
}
if (!clipped)
throw new Error('扇叶根部轮廓三角化失败');
}
triangles.push(indices.map( (index) => pairs[index]));
return triangles;
}
function prepareFrameWelds() {
if (!state.framePositions || state.frameWelds)
return;
var positions = state.framePositions;
var normals = state.frameNormals;
var used = new Set();
var welds = [];
for (let bladeIndex = 0; bladeIndex < FAN.angles.length; bladeIndex++) {
var target = rotatePoint(-(FAN.rootTipRadius + 1.3), 0, FAN.angles[bladeIndex]);
var bestTriangle = -1;
var bestScore = Infinity;
for (let triangle = 0; triangle < positions.length; triangle += 9) {
var triangleIndex = triangle / 9;
if (used.has(triangleIndex))
continue;
var z0 = positions[triangle + 2]
, z1 = positions[triangle + 5]
, z2 = positions[triangle + 8];
if (Math.abs(z0 - .1) > .0005 || Math.abs(z1 - .1) > .0005 || Math.abs(z2 - .1) > .0005)
continue;
var cx = (positions[triangle] + positions[triangle + 3] + positions[triangle + 6]) / 3;
var cy = (positions[triangle + 1] + positions[triangle + 4] + positions[triangle + 7]) / 3;
var score = (cx - target[0]) ** 2 + (cy - target[1]) ** 2;
if (score < bestScore) {
bestScore = score;
bestTriangle = triangleIndex;
}
}
if (bestTriangle < 0)
continue;
used.add(bestTriangle);
var offset = bestTriangle * 9;
welds[bladeIndex] = [[positions[offset], positions[offset + 1], positions[offset + 2]], [positions[offset + 3], positions[offset + 4], positions[offset + 5]], [positions[offset + 6], positions[offset + 7], positions[offset + 8]]];
}
var filteredPositions = [];
var filteredNormals = [];
for (let triangle = 0; triangle < positions.length; triangle += 9) {
if (used.has(triangle / 9))
continue;
for (let index = 0; index < 9; index++) {
filteredPositions.push(positions[triangle + index]);
filteredNormals.push(normals[triangle + index]);
}
}
state.framePositions = new Float32Array(filteredPositions);
state.frameNormals = new Float32Array(filteredNormals);
state.frameWelds = welds;
}
function matchTriangleLoop(frameLoop, reliefLoop) {
var permutations = [[0, 1, 2], [1, 2, 0], [2, 0, 1], [0, 2, 1], [2, 1, 0], [1, 0, 2]];
var best = permutations[0];
var bestScore = Infinity;
for (const permutation of permutations) {
var score = 0;
for (let index = 0; index < 3; index++) {
var frame = frameLoop[index];
var relief = reliefLoop[permutation[index]];
score += (frame[0] - relief[0]) ** 2 + (frame[1] - relief[1]) ** 2 + (frame[2] - relief[2]) ** 2;
}
if (score < bestScore) {
bestScore = score;
best = permutation;
}
}
return best.map( (index) => reliefLoop[index]);
}