452 lines
22 KiB
JavaScript
452 lines
22 KiB
JavaScript
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]);
|
|
}
|