代码: 全选
Here are the specific parts of the `EjSolid` class that need to be modified, with all code and comments in English as requested.
### 1. Update `constructor()`
Add the new advanced sort setting to `this.config` and the test panel states to `this.state`.
```javascript
// Modify this.state inside constructor()
this.state = {
rotMatrix: [1, 0, 0, 0, 1, 0, 0, 0, 1],
lastSvg: '',
isDragging: false,
lastX: 0,
lastY: 0,
objData: `...`, // (Keep original objData string)
parsedModel: null,
rawJsonData: this.container.textContent.trim(),
// Add test state
testMode: { active: false, face1: null, face2: null, selecting: 0 },
renderedPolysOrder: [],
currentPolys: []
};
// Modify this.config inside constructor()
this.config = {
sides: [3, 4, 5, 6, 8, 10],
defaultColors: { 3: '#00b4d8', 4: '#48cae4', 5: '#90e0ef', 6: '#0077b6', 8: '#023e8a', 10: '#03045e' },
labels: { 3: 'Triangle', 4: 'Square', 5: 'Pentagon', 6: 'Hexagon', 8: 'Octagon', 10: 'Decagon' },
canvasSize: 480,
contentSize: 360,
opacity: 0.7,
strokeWidth: 1.5,
strokeColor: '#03045e',
perspective: false,
animate: false,
animTime: 7.0,
animAngle: 36,
frameInt: 0.25,
// Add advanced sort config
advancedSort: false
};
```
### 2. Update `renderDOM()`
Add the "Advanced Depth Sort" toggle and the "Depth Test" panel.
```javascript
// Inside renderDOM() for the main mode, replace the Visibility and Style sections with:
`<h2>Visibility</h2>
<div class="ejsolid-control-group">
<label><input type="checkbox" class="ejs-perspective" /> Perspective</label>
<label><input type="checkbox" class="ejs-adv-sort" /> Advanced Depth Sort</label>
</div>
<h2>Depth Test</h2>
<div class="ejsolid-control-group ejs-test-panel">
<div class="ejsolid-row" style="grid-template-columns: 1fr 1fr;">
<button type="button" class="ejs-select-f1" style="background: #ccc; color: #333; padding: 8px;">Select Face 1</button>
<button type="button" class="ejs-select-f2" style="background: #ccc; color: #333; padding: 8px;">Select Face 2</button>
</div>
<div class="ejs-test-output" style="font-size: 11px; font-family: monospace; background: #f4f4f4; padding: 10px; border-radius: 6px; min-height: 40px;">
Select two faces to test.
</div>
<button type="button" class="ejs-debug-btn" style="display: none; background: #ffaa00; margin-top: 5px;">Debug Error</button>
</div>
<h2>Style</h2>`
```
### 3. Update `cacheDOM()`
Cache the new DOM elements.
```javascript
// Add to the end of cacheDOM()
this.dom.advSort = this.container.querySelector('.ejs-adv-sort');
this.dom.selectF1 = this.container.querySelector('.ejs-select-f1');
this.dom.selectF2 = this.container.querySelector('.ejs-select-f2');
this.dom.testOutput = this.container.querySelector('.ejs-test-output');
this.dom.debugBtn = this.container.querySelector('.ejs-debug-btn');
```
### 4. Update `bindEvents()`
Bind the new functionality and click listeners for the depth test.
```javascript
// Add inside bindEvents() before the final JSON download section
// Advanced Sort Toggle
if (this.dom.advSort) {
this.dom.advSort.oninput = () => {
this.config.advancedSort = this.dom.advSort.checked;
this.draw();
};
}
// Depth Test UI Interactions
const resetSelectModes = () => {
this.state.testMode.selecting = 0;
if (this.dom.selectF1) this.dom.selectF1.style.boxShadow = 'none';
if (this.dom.selectF2) this.dom.selectF2.style.boxShadow = 'none';
};
if (this.dom.selectF1) {
this.dom.selectF1.onclick = () => {
resetSelectModes();
this.state.testMode.selecting = 1;
this.dom.selectF1.style.boxShadow = '0 0 0 3px #eb2563';
};
}
if (this.dom.selectF2) {
this.dom.selectF2.onclick = () => {
resetSelectModes();
this.state.testMode.selecting = 2;
this.dom.selectF2.style.boxShadow = '0 0 0 3px #eb2563';
};
}
if (this.dom.debugBtn) {
this.dom.debugBtn.onclick = () => this.runDebugCycle();
}
// SVG Click delegation for selecting faces
if (this.dom.svgContainer) {
this.dom.svgContainer.addEventListener('click', (e) => {
if (this.state.testMode.selecting > 0 && e.target.tagName.toLowerCase() === 'polygon') {
const id = e.target.getAttribute('id');
if (id) {
const fill = e.target.getAttribute('fill');
if (this.state.testMode.selecting === 1) {
this.state.testMode.face1 = id;
this.dom.selectF1.style.background = fill;
this.dom.selectF1.style.color = '#fff';
this.dom.selectF1.textContent = `F1: ${id}`;
} else {
this.state.testMode.face2 = id;
this.dom.selectF2.style.background = fill;
this.dom.selectF2.style.color = '#fff';
this.dom.selectF2.textContent = `F2: ${id}`;
}
resetSelectModes();
this.updateTestPanel();
}
}
});
}
```
### 5. Update `draw()`
Update vertex projections to retain 3D coordinates, expand polygon properties (isFront, normals), and implement the advanced sorting replacements.
```javascript
// Inside draw(), replace the projected variable mapping block:
const projected = vertices.map(v => {
const x = v[0] - cx,
y = v[1] - cy,
z = v[2] - cz;
const rx = x * this.state.rotMatrix[0] + y * this.state.rotMatrix[1] + z * this.state.rotMatrix[2];
const ry = x * this.state.rotMatrix[3] + y * this.state.rotMatrix[4] + z * this.state.rotMatrix[5];
const rz = x * this.state.rotMatrix[6] + y * this.state.rotMatrix[7] + z * this.state.rotMatrix[8];
const f = this.config.perspective ? 4 / (4 - rz) : 1;
// Keep pure 3D coords for plane normal math
return { x: rx * scale * f + offset, y: -ry * scale * f + offset, z: rz, x3d: rx, y3d: ry, z3d: rz };
});
const polys = [];
faces.forEach((fIdx, idx) => {
const pts = fIdx.map(i => projected[i]);
// Calculate 2D Area to determine if Front Facing
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;
// Calculate 3D Normal for the plane
let nx = 0, ny = 0, nz = 0;
for (let j = 0; j < pts.length; j++) {
let k = (j + 1) % pts.length;
nx += (pts[j].y3d - pts[k].y3d) * (pts[j].z3d + pts[k].z3d);
ny += (pts[j].z3d - pts[k].z3d) * (pts[j].x3d + pts[k].x3d);
nz += (pts[j].x3d - pts[k].x3d) * (pts[j].y3d + pts[k].y3d);
}
const nLen = Math.sqrt(nx * nx + ny * ny + nz * nz) || 1;
const normal = { x: nx / nLen, y: ny / nLen, z: nz / nLen };
// Plane Equation component D (Ax + By + Cz = D)
const planeD = normal.x * pts[0].x3d + normal.y * pts[0].y3d + normal.z * pts[0].z3d;
polys.push({
pts,
avgZ: pts.reduce((s, p) => s + p.z, 0) / pts.length,
sides: pts.length,
fIdx,
id: `ejs-${idx}`,
isFront,
normal,
planeD
});
});
// --- REPLACED SORTING LOGIC START ---
if (this.config.advancedSort) {
polys.sort((a, b) => {
// Rule 1: Front faces always prioritized (placed at end of array to render last / closest to lens)
if (a.isFront !== b.isFront) {
return a.isFront ? 1 : -1;
}
// Standard Z-sort for backfaces
if (!a.isFront) {
return a.avgZ - b.avgZ;
}
// Rule 2 & 3: For two front faces, check overlap and normal relations
if (this.check2DOverlap(a.pts, b.pts)) {
let aInFront = false;
let aBehind = false;
// Traverse points in overlapping region against B's plane
for (let i = 0; i < a.pts.length; i++) {
if (this.isPointInsidePoly2D(a.pts[i], b.pts)) {
let dist = b.normal.x * a.pts[i].x3d + b.normal.y * a.pts[i].y3d + b.normal.z * a.pts[i].z3d - b.planeD;
if (dist > 1e-4) aInFront = true;
if (dist < -1e-4) aBehind = true;
}
}
if (aInFront && !aBehind) return 1; // A is strictly in positive normal of B (Place A before/over B)
if (aBehind && !aInFront) return -1; // A is strictly in negative normal of B (Place A behind B)
}
return a.avgZ - b.avgZ; // Fallback
});
} else {
polys.sort((a, b) => a.avgZ - b.avgZ);
}
// Save state for the test panel
this.state.currentPolys = polys;
this.state.renderedPolysOrder = polys.map(p => p.id);
// --- REPLACED SORTING LOGIC END ---
```
### 6. Add Helper Methods
Add these new methods inside the `EjSolid` class (for instance, right after `check2DOverlap`). Make sure to call `this.updateTestPanel();` at the very end of `draw()` right after setting the innerHTML.
```javascript
// Inside draw(), right at the end of the method before the closing brace:
this.updateTestPanel();
```
```javascript
// --- New Methods to Add to the Class ---
/**
* Ray casting algorithm to check if a 2D point is inside a 2D polygon projection
*/
isPointInsidePoly2D(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;
}
/**
* Updates the debug panel with relations between Face 1 and Face 2
*/
updateTestPanel() {
if (!this.state.testMode.face1 || !this.state.testMode.face2 || !this.dom.testOutput) return;
const f1Id = this.state.testMode.face1;
const f2Id = this.state.testMode.face2;
const p1 = this.state.currentPolys.find(p => p.id === f1Id);
const p2 = this.state.currentPolys.find(p => p.id === f2Id);
if (!p1 || !p2) return;
const overlaps = this.check2DOverlap(p1.pts, p2.pts);
const i1 = this.state.renderedPolysOrder.indexOf(f1Id);
const i2 = this.state.renderedPolysOrder.indexOf(f2Id);
// Higher index means rendered later (appears in front)
const currentOrder = i1 > i2 ? `${f1Id} is placed before (on top of) ${f2Id}` : `${f2Id} is placed before (on top of) ${f1Id}`;
let normalTestRes = "No overlap / No vertices in region.";
let contradiction = false;
if (overlaps) {
let p1InFront = false, p1Behind = false;
for (let i = 0; i < p1.pts.length; i++) {
if (this.isPointInsidePoly2D(p1.pts[i], p2.pts)) {
let dist = p2.normal.x * p1.pts[i].x3d + p2.normal.y * p1.pts[i].y3d + p2.normal.z * p1.pts[i].z3d - p2.planeD;
if (dist > 1e-4) p1InFront = true;
if (dist < -1e-4) p1Behind = true;
}
}
if (p1InFront && !p1Behind) normalTestRes = `${f1Id} edges are in positive normal direction of ${f2Id}.`;
else if (p1Behind && !p1InFront) normalTestRes = `${f1Id} edges are in negative normal direction of ${f2Id}.`;
else if (p1InFront && p1Behind) normalTestRes = `${f1Id} intersects ${f2Id} (both pos and neg edges).`;
// Check if standard rendering array ordering contradicts normal geometry
if (p1InFront && !p1Behind && i1 < i2) contradiction = true;
if (p1Behind && !p1InFront && i1 > i2) contradiction = true;
}
this.dom.testOutput.innerHTML = `
<div style="margin-bottom: 4px;"><b>Faces:</b> ${f1Id}, ${f2Id}</div>
<div style="margin-bottom: 4px;"><b>Overlap:</b> ${overlaps ? 'Yes' : 'No'}</div>
<div style="margin-bottom: 4px;"><b>Current Order:</b> ${currentOrder}</div>
<div><b>Normal Test:</b> ${normalTestRes}</div>
`;
this.dom.debugBtn.style.display = contradiction ? 'block' : 'none';
}
/**
* Explanation triggered when sorting algorithm fails topological transitivity
*/
runDebugCycle() {
const f1 = this.state.testMode.face1;
const f2 = this.state.testMode.face2;
alert(`Debug Analysis:\n\nThe standard Array sorting algorithm contradicts the mathematical normal vector test for ${f1} and ${f2}.\n\nReason: Array.prototype.sort() strictly requires transitive relations (e.g. if A > B and B > C, then A must > C). In complex 3D rendering, polygons frequently form cyclic overlaps (A overlaps B, B overlaps C, C overlaps A), forcing the sorting engine to break rules to resolve the endless loop, causing sorting errors. A BSP Tree or polygon splitting is required to fix this completely.`);
}
```