// /* ... your existing coordinate transformations (pts2D, pts3D, normal, depth, etc.) ... */
// /* ... your existing properties ... */
代码: 全选
那段範例代碼的意思是告訴您:**「保留您原本代碼中在 `faces.forEach` 或 `faces.map` 裡面處理坐標轉換、計算 3D 點、法向量和深度(avgZ)的那些公式不變,只需要在回傳的物件中,確保加上一個 `id` 屬性即可。」**
在您提供的完整原始碼 `ejsolid195993.js` 中,您實際使用的變數名稱是 `faces.forEach((fIdx, idx) => { ... polys.push({ ... }) })`。
為了完美達成您的四個需求(區分正反面、冒泡法循環驗證、加入 id、加測試按鈕),請將原代碼中 **`faces.forEach((fIdx, idx) => { ... });` 開始,一直到 `this.dom.svgContainer.innerHTML = svg; ...` 之前**的所有排序與動畫資料處理邏輯,替換為以下完整的英文實作代碼。
請用以下代碼替換您原文件中 **`faces.forEach((fIdx, idx) => {`** 往下一直到 **`this.state.lastSvg = svg;` 之前** 的整段區域:
```javascript
// === Requirement 3: Construct polygons with custom sequential IDs ===
faces.forEach((fIdx, idx) => {
const pts = fIdx.map(i => projected[i]);
// Calculate 3D points in camera space to find precise planes and normals
const pts3D = fIdx.map(i => {
const v = vertices[i];
const dx = v[0] - cx, dy = v[1] - cy, dz = v[2] - cz;
return {
x: dx * rot[0] + dy * rot[1] + dz * rot[2],
y: dx * rot[3] + dy * rot[4] + dz * rot[5],
z: dx * rot[6] + dy * rot[7] + dz * rot[8]
};
});
// Calculate 3D normal vector using Newell's method
let nx = 0, ny = 0, nz = 0;
for (let j = 0; j < pts3D.length; j++) {
const next = (j + 1) % pts3D.length;
nx += (pts3D[j].y - pts3D[next].y) * (pts3D[j].z + pts3D[next].z);
ny += (pts3D[j].z - pts3D[next].z) * (pts3D[j].x + pts3D[next].x);
nz += (pts3D[j].x - pts3D[next].x) * (pts3D[j].y + pts3D[next].y);
}
const len = Math.sqrt(nx * nx + ny * ny + nz * nz);
const normal3D = len > 0 ? { x: nx / len, y: ny / len, z: nz / len } : { x: 0, y: 0, z: 1 };
polys.push({
pts,
pts3D,
normal3D,
avgZ: pts.reduce((s, p) => s + p.z, 0) / pts.length,
sides: pts.length,
fIdx,
id: `ejs-${idx}` // Unique string identifier matched by evaluation handlers
});
});
// Determine basic winding order for rendering styles
polys.forEach(p => {
let cp = 0;
for (let j = 0; j < p.pts.length; j++) {
let k = (j + 1) % p.pts.length;
cp += p.pts[j].x * p.pts[k].y - p.pts[k].x * p.pts[j].y;
}
p.isFront = cp < 0;
});
// === Requirement 1: Separate into front-facing and back-facing arrays ===
const backFaces = polys.filter(p => !p.isFront);
const frontFaces = polys.filter(p => p.isFront);
// Baseline fallback depth sort for backfaces (furthest first)
backFaces.sort((a, b) => a.avgZ - b.avgZ);
// Baseline initial sorting for front faces based on geometric Z depth center
frontFaces.sort((a, b) => a.avgZ - b.avgZ);
// Helpers cloned inside local context for edge projection calculations
const isPointInPolyLocal = (pt, polyPts) => {
let inside = false;
for (let i = 0, j = polyPts.length - 1; i < polyPts.length; j = i++) {
const xi = polyPts[i].x, yi = polyPts[i].y;
const xj = polyPts[j].x, yj = polyPts[j].y;
const intersect = ((yi > pt.y) !== (yj > pt.y))
&& (pt.x < (xj - xi) * (pt.y - yi) / (yj - yi) + xi);
if (intersect) inside = !inside;
}
return inside;
};
const lineIntersectsLocal = (p1, p2, p3, p4) => {
const ccw = (A, B, C) => (C.y - A.y) * (B.x - A.x) > (B.y - A.y) * (C.x - A.x);
return (ccw(p1, p3, p4) !== ccw(p2, p3, p4)) && (ccw(p1, p2, p3) !== ccw(p1, p2, p4));
};
const edgeProjectsLocal = (p1, p2, poly) => {
if (isPointInPolyLocal(p1, poly.pts) || isPointInPolyLocal(p2, poly.pts)) return true;
for (let i = 0; i < poly.pts.length; i++) {
const q1 = poly.pts[i];
const q2 = poly.pts[(i + 1) % poly.pts.length];
if (lineIntersectsLocal(p1, p2, q1, q2)) return true;
}
return false;
};
// === Requirement 2: Bubble sort to enforce Face A is in front of Face B based on normals ===
let swapped;
let iterationCount = 0;
const maxIterations = 200; // Safeguard loop boundaries against infinite cyclic traps
do {
swapped = false;
for (let i = 0; i < frontFaces.length - 1; i++) {
let faceA = frontFaces[i];
let faceB = frontFaces[i + 1];
// Only evaluate or swap if there is a real 2D overlapping projection area
if (this.check2DOverlap(faceA.pts, faceB.pts)) {
let shouldSwap = false;
// Test if any edge of faceA projects onto faceB and lies in the direction of Normal B
for (let j = 0; j < faceA.pts.length; j++) {
const p1_2d = faceA.pts[j], p2_2d = faceA.pts[(j + 1) % faceA.pts.length];
if (edgeProjectsLocal(p1_2d, p2_2d, faceB)) {
const p1_3d = faceA.pts3D[j], p2_3d = faceA.pts3D[(j + 1) % faceA.pts3D.length];
const midX = (p1_3d.x + p2_3d.x) / 2;
const midY = (p1_3d.y + p2_3d.y) / 2;
const midZ = (p1_3d.z + p2_3d.z) / 2;
const q = faceB.pts3D[0];
const dot = (midX - q.x) * faceB.normal3D.x + (midY - q.y) * faceB.normal3D.y + (midZ - q.z) * faceB.normal3D.z;
if (dot > 1e-4) {
// Face A has an edge in the direction of Normal B, meaning Face A MUST be rendered AFTER (in front of) Face B
shouldSwap = true;
break;
}
}
}
if (shouldSwap) {
frontFaces[i] = faceB;
frontFaces[i + 1] = faceA;
swapped = true;
}
}
}
iterationCount++;
} while (swapped && iterationCount < maxIterations);
// Merge lists together so backfaces draw first, followed by verified topologically sorted frontfaces
polys = [...backFaces, ...frontFaces];
this.state.currentPolys = polys;
const doAnim = this.config.animate;
const splitOpacity = doAnim && op < 1;
const animPolysData = [];
if (doAnim) {
const duration = this.config.animTime;
const angleDeg = this.config.animAngle;
const frameInterval = this.config.frameInt;
const totalFrames = Math.round(duration / frameInterval);
const alphaRad = (angleDeg * Math.PI) / 180;
const axisX = Math.sin(alphaRad), axisY = Math.cos(alphaRad);
let startAssigned = false;
polys.forEach(p => {
animPolysData.push({
pointsValues: [],
sets: [],
frontSets: [],
backSets: [],
lastCp: 0,
currentVis: false,
fIdx: p.fIdx,
id: p.id
});
});
for (let i = 0; i <= totalFrames; i++) {
const angle = (i / totalFrames) * 2 * Math.PI;
const c = Math.cos(angle), s = Math.sin(angle), t = 1 - c;
const rAnim = [
t * axisX * axisX + c, t * axisX * axisY, s * axisY,
t * axisX * axisY, t * axisY * axisY + c, -s * axisX,
-s * axisY, s * axisX, c
];
const curRot = [
rAnim[0] * this.state.rotMatrix[0] + rAnim[1] * this.state.rotMatrix[3] + rAnim[2] * this.state.rotMatrix[6],
rAnim[0] * this.state.rotMatrix[1] + rAnim[1] * this.state.rotMatrix[4] + rAnim[2] * this.state.rotMatrix[7],
rAnim[0] * this.state.rotMatrix[2] + rAnim[1] * this.state.rotMatrix[5] + rAnim[2] * this.state.rotMatrix[8],
rAnim[3] * this.state.rotMatrix[0] + rAnim[4] * this.state.rotMatrix[3] + rAnim[5] * this.state.rotMatrix[6],
rAnim[3] * this.state.rotMatrix[1] + rAnim[4] * this.state.rotMatrix[4] + rAnim[5] * this.state.rotMatrix[7],
rAnim[3] * this.state.rotMatrix[2] + rAnim[4] * this.state.rotMatrix[5] + rAnim[5] * this.state.rotMatrix[8],
rAnim[6] * this.state.rotMatrix[0] + rAnim[7] * this.state.rotMatrix[3] + rAnim[8] * this.state.rotMatrix[6],
rAnim[6] * this.state.rotMatrix[1] + rAnim[7] * this.state.rotMatrix[4] + rAnim[8] * this.state.rotMatrix[7],
rAnim[6] * this.state.rotMatrix[2] + rAnim[7] * this.state.rotMatrix[5] + rAnim[8] * this.state.rotMatrix[8]
];
const frameProjected = vertices.map(v => {
const x = v[0] - cx, y = v[1] - cy, z = v[2] - cz;
const rx = x * curRot[0] + y * curRot[1] + z * curRot[2];
const ry = x * curRot[3] + y * curRot[4] + z * curRot[5];
const rz = x * curRot[6] + y * curRot[7] + z * curRot[8];
const f = this.config.perspective ? 4 / (4 - rz) : 1;
return { x: rx * scale * f + offset, y: -ry * scale * f + offset };
});
animPolysData.forEach(animP => {
const pts = animP.fIdx.map(idx => frameProjected[idx]);
let cp = 0;
for (let j = 0; j < pts.length; j++) {
let k = (j + 1) % pts.length;
cp += pts[j].x * pts[k].y - pts[k].x * pts[j].y;
}
const isFront = cp < 0;
animP.pointsValues.push(pts.map(pt => `${pt.x.toFixed(2)},${pt.y.toFixed(2)}`).join(' '));
if (i === 0) {
animP.currentVis = isFront;
animP.lastCp = cp;
const visStr = isFront ? 'visible' : 'hidden';
const invVisStr = isFront ? 'hidden' : 'visible';
if (!startAssigned) {
animP.sets.push(`<set id="start" attributeName="visibility" to="${visStr}" begin="0;start.begin+${duration.toFixed(1)}s" />`);
animP.frontSets.push(`<set id="start" attributeName="visibility" to="${visStr}" begin="0;start.begin+${duration.toFixed(1)}s" />`);
animP.backSets.push(`<set attributeName="visibility" to="${invVisStr}" begin="0;start.begin+${duration.toFixed(1)}s" />`);
startAssigned = true;
} else {
animP.sets.push(`<set attributeName="visibility" to="${visStr}" begin="start.begin" />`);
animP.frontSets.push(`<set attributeName="visibility" to="${visStr}" begin="start.begin" />`);
animP.backSets.push(`<set attributeName="visibility" to="${invVisStr}" begin="start.begin" />`);
}
} else {
if (isFront !== animP.currentVis) {
animP.currentVis = isFront;
const visStr = isFront ? 'visible' : 'hidden';
const invVisStr = isFront ? 'hidden' : 'visible';
let fraction = Math.abs(animP.lastCp - cp) > 1e-6 ? Math.abs(animP.lastCp / (animP.lastCp - cp)) : 0.5;
let exactTime = ((i - 1 + fraction) * frameInterval).toFixed(3);
animP.sets.push(`<set attributeName="visibility" to="${visStr}" begin="start.begin+${exactTime}s" />`);
animP.frontSets.push(`<set attributeName="visibility" to="${visStr}" begin="start.begin+${exactTime}s" />`);
animP.backSets.push(`<set attributeName="visibility" to="${invVisStr}" begin="start.begin+${exactTime}s" />`);
}
animP.lastCp = cp;
}
});
}
}
let svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${size} ${size}" width="${size}" height="${size}" preserveAspectRatio="xMidYMid meet">\n`;
const groupAttr = splitOpacity ? ` visibility="hidden"` : ``;
svg += ` <g stroke-linejoin="round"${groupAttr}>\n`;
polys.forEach((p, i) => {
const pStr = p.pts.map(pt => `${pt.x.toFixed(2)},${pt.y.toFixed(2)}`).join(' ');
let fill = this.config.defaultColors[p.sides] || '#ccc';
if (p.id === this.state.selectedPolyA && this.dom.colorTestA) {
fill = this.dom.colorTestA.value;
} else if (p.id === this.state.selectedPolyB && this.dom.colorTestB) {
fill = this.dom.colorTestB.value;
}
const isFrontAttr = p.isFront ? 'true' : 'false';
const polyIdAttr = ` id="${p.id}" data-front="${isFrontAttr}"`;
if (doAnim) {
const animP = animPolysData[i];
const valuesStr = animP.pointsValues.join(';');
const durationStr = this.config.animTime.toFixed(1);
svg += ` <polygon${polyIdAttr} points="${pStr}" fill="${fill}" fill-opacity="${op}" stroke="${strokeColor}" stroke-width="${sw}">\n`;
svg += ` <animate attributeName="points" values="${valuesStr}" dur="${durationStr}s" begin="start.begin" repeatCount="indefinite" />\n`;
if (!splitOpacity) {
animP.sets.forEach(setTag => { svg += ` ${setTag}\n`; });
}
svg += ` </polygon>\n`;
} else {
svg += ` <polygon${polyIdAttr} points="${pStr}" fill="${fill}" fill-opacity="${op}" stroke="${strokeColor}" stroke-width="${sw}" />\n`;
}
});
svg += ` </g>\n`;
if (splitOpacity) {
let backSvg = ` <g id="back" stroke-linejoin="round">\n`;
let frontSvg = ` <g id="front" stroke-linejoin="round">\n`;
polys.forEach((p, i) => {
const animP = animPolysData[i];
backSvg += ` <use href="#${animP.id}">\n`;
animP.backSets.forEach(setTag => { backSvg += ` ${setTag}\n`; });
backSvg += ` </use>\n`;
frontSvg += ` <use href="#${animP.id}">\n`;
animP.frontSets.forEach(setTag => { frontSvg += ` ${setTag}\n`; });
frontSvg += ` </use>\n`;
});
backSvg += ` </g>\n`;
frontSvg += ` </g>\n`;
svg += backSvg + frontSvg;
}
svg += `</svg>`;
// === Requirement 4: Dynamically append verification logging button if absent ===
if (!document.getElementById('ejs-console-verify-btn')) {
const logBtn = document.createElement('button');
logBtn.id = 'ejs-console-verify-btn';
logBtn.innerText = 'Console Verify Sorting';
logBtn.type = 'button';
logBtn.style.cssText = 'margin-top: 10px; width: 100%; padding: 10px; background: #0077b6; color: #fff; border: none; border-radius: 6px; cursor: pointer; font-weight: bold;';
logBtn.onclick = () => {
console.log('--- Geometry Separation & Verification Dump ---');
console.log(`Total Front Faces: ${frontFaces.length}`);
console.log(`Total Back Faces: ${backFaces.length}`);
frontFaces.forEach((fA, idxA) => {
for (let idxB = idxA + 1; idxB < frontFaces.length; idxB++) {
let fB = frontFaces[idxB];
if (this.check2DOverlap(fA.pts, fB.pts)) {
let aHasEdgeOnNormalB = false;
for (let j = 0; j < fA.pts.length; j++) {
const p1_2d = fA.pts[j], p2_2d = fA.pts[(j + 1) % fA.pts.length];
if (edgeProjectsLocal(p1_2d, p2_2d, fB)) {
const p1_3d = fA.pts3D[j], p2_3d = fA.pts3D[(j + 1) % fA.pts3D.length];
const midX = (p1_3d.x + p2_3d.x) / 2;
const midY = (p1_3d.y + p2_3d.y) / 2;
const midZ = (p1_3d.z + p2_3d.z) / 2;
const q = fB.pts3D[0];
const dot = (midX - q.x) * fB.normal3D.x + (midY - q.y) * fB.normal3D.y + (midZ - q.z) * fB.normal3D.z;
if (dot > 1e-4) {
aHasEdgeOnNormalB = true;
break;
}
}
}
console.log(`Pairwise Test -> Face A ID: ${fA.id} | Face B ID: ${fB.id}`);
console.log(` - 2D Intersecting Overlap: True`);
console.log(` - Face A has edge projecting along Normal B: ${aHasEdgeOnNormalB}`);
console.log(` - Render Layer Order: Face ${fB.id} draws before Face ${fA.id} (A is in front)`);
}
}
});
};
const leftPanel = this.container.querySelector('.ejsolid-left');
if (leftPanel) {
leftPanel.appendChild(logBtn);
}
}
```