正在開發新遊戲:星•球

分享身边的点滴趣事
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5653
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 179 次
被圈友点赞: 207 次
联系:

Re: 正在開發新遊戲:星•球

帖子 ejsoon »

代码: 全选

Here are the necessary corrections. The inability to click the faces is caused by the hardcoded `pointer-events: none` CSS rules protecting the canvas for the drag-to-rotate functionality. We can fix this by dynamically overriding the `pointer-events` inline styles when a selection mode is active.

Additionally, the sorting issue was caused by polygons completely enclosing smaller ones, causing the simple vertex point-check to fail. We need a robust 3-way check (A inside B, B inside A, and crossing edges) to fully evaluate the overlap areas as requested.

### 1. Replace the SVG Click & Button Events in `bindEvents()`

Replace the previous depth test UI interactions inside your `bindEvents()` with this updated logic. It correctly toggles `pointer-events` so the polygons become clickable.

```javascript
    // --- REPLACE the Depth Test UI Interactions in bindEvents() ---
    
    const setSelectMode = (mode) => {
      this.state.testMode.selecting = mode;
      
      // Update Button Styles
      if (this.dom.selectF1) this.dom.selectF1.style.boxShadow = mode === 1 ? '0 0 0 3px #eb2563' : 'none';
      if (this.dom.selectF2) this.dom.selectF2.style.boxShadow = mode === 2 ? '0 0 0 3px #eb2563' : 'none';
      
      // Override pointer-events so faces can actually be clicked
      if (this.dom.svgContainer) {
        this.dom.svgContainer.style.cursor = mode > 0 ? 'crosshair' : 'grab';
        const svg = this.dom.svgContainer.querySelector('svg');
        if (svg) {
          svg.style.pointerEvents = mode > 0 ? 'auto' : 'none';
          const polys = svg.querySelectorAll('polygon');
          polys.forEach(el => { el.style.pointerEvents = mode > 0 ? 'auto' : 'none'; });
        }
      }
    };

    if (this.dom.selectF1) this.dom.selectF1.onclick = () => setSelectMode(1);
    if (this.dom.selectF2) this.dom.selectF2.onclick = () => setSelectMode(2);

    if (this.dom.debugBtn) {
      this.dom.debugBtn.onclick = () => this.runDebugCycle();
    }

    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') || e.target.style.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}`;
            }
            // Reset mode after successful click
            setSelectMode(0);
            this.updateTestPanel();
          }
        }
      });
    }

```

### 2. Add the Edge Intersection Helper Method

Add this completely new helper method directly beneath `isPointInsidePoly2D(...)` in your class to analyze the crossing edges:

```javascript
  /**
   * Calculates precise 2D edge intersections between two polygons.
   * Required to detect faces that form a cross/star shape where vertices aren't enclosed.
   */
  get2DEdgeIntersections(polyA, polyB) {
    const intersections = [];
    for (let i = 0; i < polyA.length; i++) {
      const p1 = polyA[i], p2 = polyA[(i + 1) % polyA.length];
      for (let j = 0; j < polyB.length; j++) {
        const p3 = polyB[j], p4 = polyB[(j + 1) % polyB.length];
        
        const denom = (p1.x - p2.x) * (p3.y - p4.y) - (p1.y - p2.y) * (p3.x - p4.x);
        if (Math.abs(denom) < 1e-6) continue; // Parallel edges
        
        const tA = ((p1.x - p3.x) * (p3.y - p4.y) - (p1.y - p3.y) * (p3.x - p4.x)) / denom;
        const tB = -((p1.x - p2.x) * (p1.y - p3.y) - (p1.y - p2.y) * (p1.x - p3.x)) / denom;
        
        if (tA >= 0 && tA <= 1 && tB >= 0 && tB <= 1) {
          intersections.push({ 
            x: p1.x + tA * (p2.x - p1.x), 
            y: p1.y + tA * (p2.y - p1.y), 
            tA, tB, pA1: p1, pA2: p2, pB1: p3, pB2: p4 
          });
        }
      }
    }
    return intersections;
  }

```

### 3. Replace the Sorting Logic in `draw()`

Replace the advanced sort block in `draw()` to include the reverse check and edge intersection check.

```javascript
    // --- REPLACED SORTING LOGIC START ---
    if (this.config.advancedSort) {
      polys.sort((a, b) => {
        // Rule 1: Distinguish Front and Back faces
        if (a.isFront !== b.isFront) {
          return a.isFront ? 1 : -1; // Front faces render after (on top of) back faces
        }

        // Standard Z-sort for backfaces
        if (!a.isFront) {
          return a.avgZ - b.avgZ;
        }

        // Rule 2 & 3: For overlapping front faces, deeply check geometry
        if (this.check2DOverlap(a.pts, b.pts)) {
          let aInFront = false;
          let aBehind = false;

          // Check 1: A's vertices inside B's 2D projection
          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;
            }
          }

          // Check 2: B's vertices inside A's 2D projection (crucial for large polygons enclosing small ones)
          for (let i = 0; i < b.pts.length; i++) {
            if (this.isPointInsidePoly2D(b.pts[i], a.pts)) {
              let dist = a.normal.x * b.pts[i].x3d + a.normal.y * b.pts[i].y3d + a.normal.z * b.pts[i].z3d - a.planeD;
              if (dist > 1e-4) aBehind = true; // B is in front of A -> A is behind B
              if (dist < -1e-4) aInFront = true;
            }
          }

          // Check 3: Explicitly evaluate crossing edges in the overlapping region
          const intersections = this.get2DEdgeIntersections(a.pts, b.pts);
          for (let i = 0; i < intersections.length; i++) {
            const I = intersections[i];
            const zA = I.pA1.z3d + I.tA * (I.pA2.z3d - I.pA1.z3d);
            const zB = I.pB1.z3d + I.tB * (I.pB2.z3d - I.pB1.z3d);
            if (zA - zB > 1e-4) aInFront = true;
            if (zB - zA > 1e-4) aBehind = true;
          }

          if (aInFront && !aBehind) return 1; // A strictly in front, place A after B
          if (aBehind && !aInFront) return -1; // A strictly behind, place A before B
        }
        return a.avgZ - b.avgZ; // Fallback
      });
    } else {
      polys.sort((a, b) => a.avgZ - b.avgZ);
    }

    this.state.currentPolys = polys;
    this.state.renderedPolysOrder = polys.map(p => p.id);
    // --- REPLACED SORTING LOGIC END ---

```

### 4. Update the Debug Test Panel

Finally, replace `updateTestPanel()` to make sure the debug readouts use the same robust 3-way evaluation logic.

```javascript
  /**
   * 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 geometry interactions.";
    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;
        }
      }
      for (let i = 0; i < p2.pts.length; i++) {
        if (this.isPointInsidePoly2D(p2.pts[i], p1.pts)) {
          let dist = p1.normal.x * p2.pts[i].x3d + p1.normal.y * p2.pts[i].y3d + p1.normal.z * p2.pts[i].z3d - p1.planeD;
          if (dist > 1e-4) p1Behind = true; 
          if (dist < -1e-4) p1InFront = true;
        }
      }
      const intersections = this.get2DEdgeIntersections(p1.pts, p2.pts);
      for (let i = 0; i < intersections.length; i++) {
        const I = intersections[i];
        const z1 = I.pA1.z3d + I.tA * (I.pA2.z3d - I.pA1.z3d);
        const z2 = I.pB1.z3d + I.tB * (I.pB2.z3d - I.pB1.z3d);
        if (z1 - z2 > 1e-4) p1InFront = true;
        if (z2 - z1 > 1e-4) p1Behind = true;
      }

      if (p1InFront && !p1Behind) normalTestRes = `${f1Id} is geometrically IN FRONT OF ${f2Id}.`;
      else if (p1Behind && !p1InFront) normalTestRes = `${f1Id} is geometrically BEHIND ${f2Id}.`;
      else if (p1InFront && p1Behind) normalTestRes = `${f1Id} INTERSECTS ${f2Id} in 3D.`;

      // 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 Array Order:</b> ${currentOrder}</div>
      <div><b>Normal Test:</b> ${normalTestRes}</div>
    `;

    this.dom.debugBtn.style.display = contradiction ? 'block' : 'none';
  }

```
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5653
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 179 次
被圈友点赞: 207 次
联系:

Re: 正在開發新遊戲:星•球

帖子 ejsoon »

改進:
增加一個測試欄到Visibility下方,裡面有兩個選取面的按鈕,當點擊後將能選取一個面,被選取的面的顏色將跟按鈕顏色相同。

當有兩個面選擇後,測試欄將實時顯示:這兩個面id,是否重疊(check2DOverlap函式已寫好),當前的前後關係。

只需給出所要修改的地方,所有的代碼和注釋都要使用英文。



gemini fast:

代码: 全选

To add the face selection and 2D overlap/sorting test feature beneath the "Visibility" section, you need to update three parts of the `EjSolid` class: the HTML structure in `renderDOM()`, the event listeners and element caching in `bindEvents()` and `cacheDOM()`, and the rendering/drawing pipeline in `draw()`.

Here are the specific sections that need to be modified:

---

### 1. Update `renderDOM()`

Add the new test panel HTML structure inside the `else` block (main mode) directly under the "Visibility" section.

```javascript
// Locate this block inside renderDOM() -> else statement:
// <h2>Visibility</h2>
// <div class="ejsolid-control-group">
//     <label><input type="checkbox" class="ejs-perspective" /> Perspective</label>
// </div>

// CHANGE TO:
<h2>Visibility</h2>
<div class="ejsolid-control-group">
    <label><input type="checkbox" class="ejs-perspective" /> Perspective</label>
</div>
<h2>Overlap Test</h2>
<div class="ejsolid-control-group" style="font-size: 13px; display: flex; flex-direction: column; gap: 8px;">
    <div style="display: flex; gap: 10px;">
        <button type="button" class="ejs-select-face-1" style="flex: 1; padding: 6px; border-radius: 4px; font-size: 12px; background: #e2e8f0; color: #334155;">Select Face 1</button>
        <button type="button" class="ejs-select-face-2" style="flex: 1; padding: 6px; border-radius: 4px; font-size: 12px; background: #e2e8f0; color: #334155;">Select Face 2</button>
    </div>
    <div class="ejs-test-result" style="background: #f1f5f9; padding: 8px; border-radius: 6px; font-family: monospace; line-height: 1.4; color: #475569;">
        Face 1: None<br>
        Face 2: None<br>
        Overlap: --<br>
        Order: --
    </div>
</div>

```

---

### 2. Update `cacheDOM()` & Initialize Test States

Cache the new DOM controls and define tracking states in the constructor or `cacheDOM()` block to preserve selected faces.

```javascript
// Add these lines at the end of the cacheDOM() method:
this.dom.btnSelectFace1 = this.container.querySelector('.ejs-select-face-1');
this.dom.btnSelectFace2 = this.container.querySelector('.ejs-select-face-2');
this.dom.testResult = this.container.querySelector('.ejs-test-result');

// Initialize state tracking variables for the selection
this.state.testSelection = {
  activeSlot: null, // 1 or 2 when waiting for a click, null otherwise
  face1: null,      // Stores full polygon object or null
  face2: null       // Stores full polygon object or null
};
this.config.testColors = {
  1: '#ef4444',     // Red for Face 1
  2: '#22c55e'      // Green for Face 2
};

```

---

### 3. Update `bindEvents()`

Implement the selection toggle states for the buttons. This sets up which slot is actively listening for a generic canvas polygon click.

```javascript
// Add these event hooks into the bindEvents() method:
if (this.dom.btnSelectFace1) {
  this.dom.btnSelectFace1.onclick = (e) => {
    e.stopPropagation();
    if (this.state.testSelection.activeSlot === 1) {
      this.state.testSelection.activeSlot = null;
      this.dom.btnSelectFace1.style.background = '#e2e8f0';
    } else {
      this.state.testSelection.activeSlot = 1;
      this.dom.btnSelectFace1.style.background = this.config.testColors[1];
      if (this.dom.btnSelectFace2) this.dom.btnSelectFace2.style.background = '#e2e8f0';
    }
  };
}

if (this.dom.btnSelectFace2) {
  this.dom.btnSelectFace2.onclick = (e) => {
    e.stopPropagation();
    if (this.state.testSelection.activeSlot === 2) {
      this.state.testSelection.activeSlot = null;
      this.dom.btnSelectFace2.style.background = '#e2e8f0';
    } else {
      this.state.testSelection.activeSlot = 2;
      this.dom.btnSelectFace2.style.background = this.config.testColors[2];
      if (this.dom.btnSelectFace1) this.dom.btnSelectFace1.style.background = '#e2e8f0';
    }
  };
}

```

---

### 4. Update `draw()` Pipeline

Modify the rendering pipeline to capture clicks on specific faces, dynamically override face fill colors based on selection, and run live SAT calculation evaluations.

```javascript
// --- STEP A: Place this right BEFORE the "polys.sort((a, b) => a.avgZ - b.avgZ);" line ---
// This aligns data references before the depth sorting shifts indices
if (this.state.testSelection.face1) {
  const updatedF1 = polys.find(p => p.id === this.state.testSelection.face1.id);
  if (updatedF1) this.state.testSelection.face1 = updatedF1;
}
if (this.state.testSelection.face2) {
  const updatedF2 = polys.find(p => p.id === this.state.testSelection.face2.id);
  if (updatedF2) this.state.testSelection.face2 = updatedF2;
}


// --- STEP B: Place this right BEFORE the "let svg = `<svg ...`" generation layout ---
// Process overlap and execution calculations if both elements are locked
if (this.dom.testResult) {
  const f1 = this.state.testSelection.face1;
  const f2 = this.state.testSelection.face2;
  
  let f1Text = f1 ? f1.id : 'None';
  let f2Text = f2 ? f2.id : 'None';
  let overlapText = '--';
  let orderText = '--';

  if (f1 && f2) {
    const isOverlapping = this.check2DOverlap(f1.pts, f2.pts);
    overlapText = isOverlapping ? 'True' : 'False';
    
    // Find current render indexes within sorted arrays to identify painter order stack paths
    const idx1 = polys.findIndex(p => p.id === f1.id);
    const idx2 = polys.findIndex(p => p.id === f2.id);
    
    if (idx1 !== -1 && idx2 !== -1) {
      orderText = idx1 < idx2 ? `${f1.id} behind ${f2.id}` : `${f2.id} behind ${f1.id}`;
    }
  }
  
  this.dom.testResult.innerHTML = `Face 1: ${f1Text}<br>Face 2: ${f2Text}<br>Overlap: ${overlapText}<br>Order: ${orderText}`;
}


// --- STEP C: Modify inside the "polys.forEach((p, i) => { ..." loop ---
// Replace the color extraction definition assignment:
// const fill = this.config.defaultColors[p.sides] || '#ccc';

// CHANGE TO:
let fill = this.config.defaultColors[p.sides] || '#ccc';
if (this.state.testSelection.face1 && p.id === this.state.testSelection.face1.id) {
  fill = this.config.testColors[1];
} else if (this.state.testSelection.face2 && p.id === this.state.testSelection.face2.id) {
  fill = this.config.testColors[2];
}


// --- STEP D: Add cursor interactions inside the standard loop generation template ---
// Replace the static polygon strings attributes output mappings inside the loop:
// svg += `    <polygon${polyIdAttr} points="${pStr}" fill="${fill}" ... />\n`;

// CHANGE TO:
// Inject pointer-events active configurations and inline click method overrides directly into elements
const interactiveAttrs = ` pointer-events="auto" cursor="pointer" onclick="window.ejsInstance.selectFaceHandler('${p.id}')"`;

if (doAnim) {
  // ... inside animation loop setup blocks:
  svg += `    <polygon${polyIdAttr} points="${pStr}" fill="${fill}" fill-opacity="${op}" stroke="${strokeColor}" stroke-width="${sw}"${interactiveAttrs}>\n`;
  // ...
} else {
  svg += `    <polygon${polyIdAttr} points="${pStr}" fill="${fill}" fill-opacity="${op}" stroke="${strokeColor}" stroke-width="${sw}"${interactiveAttrs} />\n`;
}

```

---

### 5. Add Global Selector Method and Window Binding

Add this tracking method at the bottom of the script or inside `initApp()` so that inline SVG `onclick` actions can successfully route selections back to the class instance.

```javascript
// Inside the constructor, expose this instance globally to handle SVG string onclick markers securely
window.ejsInstance = this;

// Add this method to the EjSolid class prototype structure:
selectFaceHandler(polygonId) {
  const slot = this.state.testSelection.activeSlot;
  if (!slot) return; // Ignore if no selection button is currently active

  // Locate targeted geometry directly from unmutated dataset references
  const targetFace = this.state.parsedModel.faces.map((fIdx, idx) => ({ id: `ejs-${idx}` })).find(p => p.id === polygonId);
  
  if (targetFace) {
    if (slot === 1) {
      this.state.testSelection.face1 = targetFace;
      this.dom.btnSelectFace1.style.background = '#e2e8f0';
    } else if (slot === 2) {
      this.state.testSelection.face2 = targetFace;
      this.dom.btnSelectFace2.style.background = '#e2e8f0';
    }
    this.state.testSelection.activeSlot = null; // Reset selection tool focus state
    this.draw(); // Trigger redraw updates
  }
}

```
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5653
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 179 次
被圈友点赞: 207 次
联系:

Re: 正在開發新遊戲:星•球

帖子 ejsoon »

分析所上傳代碼。當選取了兩個面,判斷兩個面的邊是否處於另一個面的法向量方位,用的是哪些函式?


gemini thinking:

代码: 全选

根據您上傳的程式碼,當選取了兩個面(Face A 與 Face B)之後,用來判斷「**邊是否處於另一個面的法向量方位**」的核心方法是 **`evaluateIntersection()`**。

在這個方法內部,除了直接進行 3D 點與法向量的內積(Dot Product)計算外,還定義並呼叫了數個區域輔助函式(Local Helpers)來共同完成空間與投影的幾何判斷。

以下是具體涉及的函式及其作用:

### 1. 主核心方法:`evaluateIntersection()`

這是觸發選取測試時的主要類別方法。它負責協調整個判斷流程:

* 從快取的 `this.state.currentPolys` 中撈出選取面的 3D 幾何資料(如頂點 `pts3D` 和法向量 `normal3D`)。
* 計算邊的 3D 中點(`midX`, `midY`, `midZ`)。
* **內積計算(Inline 實作)**:在該方法內直接透過以下數學式計算中點到平面的向量與法向量的內積:
```javascript
const dot = (midX - q.x) * dataB.normal3D.x + (midY - q.y) * dataB.normal3D.y + (midZ - q.z) * dataB.normal3D.z;

```


並依據 `dot > 0`(順著法向量)或 `dot < 0`(逆著法向量)來判斷並輸出 `In direction of Normal` 或 `Opposite to Normal`。

---

### 2. 區域輔助函式(定義於 `evaluateIntersection` 內部)

在執行上述法向量方位判斷前,程式必須先確認「A 面的邊是否有投影到 B 面上」(反之亦然)。這部分依賴以下三個在 `evaluateIntersection()` 內部宣告的箭頭函式:

* **`edgeProjects(p1, p2, poly)`**
* **作用**:用來判斷某條 2D 邊(由 `p1` 到 `p2`)是否投影覆蓋或交會於目標多邊形(`poly`)的投影範圍內。
* **行為**:內部會進一步呼叫 `isPointInPoly` 和 `lineIntersects`。


* **`isPointInPoly(pt, polyPts)`**
* **作用**:使用射線法(Ray-casting algorithm)判斷一個 2D 點是否在多邊形內部。
* **用途**:被 `edgeProjects` 呼叫,用來檢查邊的端點是否落在另一個面的投影內。


* **`lineIntersects(p1, p2, p3, p4)`**
* **作用**:使用外積(CCW, Counter-Clockwise)演算法判斷兩條 2D 線段(`p1->p2` 與 `p3->p4`)是否在二維平面上相交。
* **用途**:被 `edgeProjects` 呼叫,用來檢查兩個面的邊緣是否在投影畫面上產生交點。



---

### 總結流程

當您選取兩個面時,執行的幾何判斷鏈為:
`evaluateIntersection()` $\rightarrow$ 呼叫 `edgeProjects()` $\rightarrow$ 呼叫 `isPointInPoly()` 與 `lineIntersects()` 確認有投影重疊 $\rightarrow$ 回到 `evaluateIntersection()` 執行 **3D 內積(`dot`)** 判定法向量方位。
附件
ejsolid195993.js
(59.66 KiB) 已下载 5 次
上次由 ejsoon 在 2026年 5月 19日 10:08,总共编辑 1 次。
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5653
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 179 次
被圈友点赞: 207 次
联系:

Re: 正在開發新遊戲:星•球

帖子 ejsoon »

現在我想把這個判定方法應用到196.js中。
在196.js中的Visibility內新增一個切換按鈕,當切換時,正面的排序將替換成195993的「選擇兩個面之後,判定每條邊在另一個面的法向量方位是正向還是反向」的方法。要遍歷每一條邊,用冒泡法而不要用array.sort。

當原本已經有面被判定為在另一個面後方,但是後來又排到前面,應在console中輸出原因。

代码: 全选

為了將 `195993.js` 的「選取兩面後判定每條邊在另一個面的法向量方位」幾何演算法整合至 `196.js` 的排序與可見性(Visibility)機制中,我們需要進行以下四個步驟的修改與擴充:

1. **在 UI 介面的 Visibility 區域新增切換按鈕。**
2. **移植並建立 3D 法向量方位與投影的幾何判定核心函式。**
3. **實作自訂的冒泡排序法(Bubble Sort)取代原本的 `Array.prototype.sort`。**
4. **加入「逆轉排序偵測機制」,當最終排序與直接幾何判定發生衝突時(即原本在後方的面被排到前面),於主控台(Console)輸出詳細原因。**

以下是具體的實作與代碼整合指南:

---

### 步驟一:在 UI 中新增切換按鈕(Visibility 區塊)

請在 `196.js` 中初始化或建構控制面板(如 `buildUI`、`createDOM` 或相關渲染側邊欄的方法)的 **Visibility 區塊**內,加入以下 HTML 與事件綁定:

```javascript
// 1. 在 state 初始化中加入預設狀態 (通常在 constructor 內)
this.state.useNormalEdgeSort = false;

// 2. 在建立 Visibility UI 的地方插入此 HTML 標籤
// 範例:尋找含有 'backfaceCulling' 或 'Visibility' 的 UI 渲染位置
const visibilityContainer = this.container.querySelector('.visibility-controls-group') || ...; 

const toggleHtml = `
  <label style="display:flex; align-items:center; gap:5px; margin-top:5px;">
    <input type="checkbox" id="toggleNormalEdgeSort">
    使用法向量邊緣冒泡排序
  </label>
`;
// 插入至 UI 中
if (visibilityContainer) {
  visibilityContainer.insertAdjacentHTML('beforeend', toggleHtml);
}

// 3. 綁定事件監聽器
const normalEdgeToggle = this.container.querySelector('#toggleNormalEdgeSort');
if (normalEdgeToggle) {
  normalEdgeToggle.checked = this.state.useNormalEdgeSort;
  normalEdgeToggle.addEventListener('change', (e) => {
    this.state.useNormalEdgeSort = e.target.checked;
    // 觸發重新計算與渲染
    if (typeof this.render === 'function') this.render();
    else if (typeof this.update === 'function') this.update();
  });
}

```

---

### 步驟二:移植與封裝 195993.js 的幾何判定函式

將此組幾何工具函式新增至 `EjSolid` 類別的方法中,用來精準計算「面 A 的邊是否處於面 B 的法向量正/反向」:

```javascript
// 判斷點是否在 2D 多邊形內 (射線法)
_isPointInPoly(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;
}

// 判斷兩條 2D 線段是否相交 (CCW 外積法)
_lineIntersects(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);
}

// 判斷邊是否投影重疊於目標多邊形
_edgeProjects(p1, p2, poly) {
  if (this._isPointInPoly(p1, poly.pts2D) || this._isPointInPoly(p2, poly.pts2D)) return true;
  for (let i = 0; i < poly.pts2D.length; i++) {
    const p3 = poly.pts2D[i];
    const p4 = poly.pts2D[(i + 1) % poly.pts2D.length];
    if (this._lineIntersects(p1, p2, p3, p4)) return true;
  }
  return false;
}

/**
 * 核心:直接判定兩個面的幾何前後關係
 * 回傳: 'behind' (A在B後方), 'in_front' (A在B前方), 'unknown' (無交集或無法判定)
 */
_evaluatePairwiseRelation(polyA, polyB) {
  let votesA = 0; // 正數代表 A 在 B 的法向量正向,負數代表反向

  // 1. 遍歷面 A 的每一條邊,對照面 B 的法向量
  for (let i = 0; i < polyA.pts2D.length; i++) {
    const p1_2d = polyA.pts2D[i];
    const p2_2d = polyA.pts2D[(i + 1) % polyA.pts2D.length];

    if (this._edgeProjects(p1_2d, p2_2d, polyB)) {
      const p1_3d = polyA.pts3D[i];
      const p2_3d = polyA.pts3D[(i + 1) % polyA.pts3D.length];
      
      // 計算 3D 中點
      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;

      // 面 B 的參考點與法向量
      const q = polyB.pts3D[0];
      const dot = (midX - q.x) * polyB.normal3D.x + 
                  (midY - q.y) * polyB.normal3D.y + 
                  (midZ - q.z) * polyB.normal3D.z;

      if (dot > 1e-5) votesA++;       // 處於法向量正向方位 (前方)
      else if (dot < -1e-5) votesA--;  // 處於法向量反向方位 (後方)
    }
  }

  // 2. 對稱檢查:遍歷面 B 的每一條邊,對照面 A 的法向量
  let votesB = 0;
  for (let i = 0; i < polyB.pts2D.length; i++) {
    const p1_2d = polyB.pts2D[i];
    const p2_2d = polyB.pts2D[(i + 1) % polyB.pts2D.length];

    if (this._edgeProjects(p1_2d, p2_2d, polyA)) {
      const p1_3d = polyB.pts3D[i];
      const p2_3d = polyB.pts3D[(i + 1) % polyB.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 = polyA.pts3D[0];
      const dot = (midX - q.x) * polyA.normal3D.x + 
                  (midY - q.y) * polyA.normal3D.y + 
                  (midZ - q.z) * polyA.normal3D.z;

      if (dot > 1e-5) votesB++;       // B 在 A 的正向 (意味著 A 在 B 的後方)
      else if (dot < -1e-5) votesB--;  // B 在 A 的反向 (意味著 A 在 B 的前方)
    }
  }

  // 綜合投票結果:
  // 如果 votesA > 0 且 votesB < 0,皆指向「A 在 B 前方」
  const netScore = votesA - votesB;
  if (netScore > 0) return 'in_front';
  if (netScore < 0) return 'behind';
  return 'unknown';
}

```

---

### 步驟三與四:實作冒泡排序法與逆轉衝突輸出

在 `EjSolid` 中新增自訂的冒泡排序方法。排序完成後,我們會進行全檢,抓出「**直接判定為 behind(後方),但最終排序位置卻在前方(Index 較大)**」的衝突面,並將原因輸出至 console。

```javascript
bubbleSortByNormalEdge(polys) {
  const n = polys.length;
  let swapped;

  // 1. 執行標準冒泡排序
  for (let i = 0; i < n - 1; i++) {
    swapped = false;
    for (let j = 0; j < n - i - 1; j++) {
      const polyA = polys[j];
      const polyB = polys[j + 1];

      // 進行直接幾何判定
      const relation = this._evaluatePairwiseRelation(polyA, polyB);
      let shouldSwap = false;

      if (relation === 'in_front') {
        // polyA 在前方,應該往後移(繪圖順序在後,Index 應較大),故需要交換
        shouldSwap = true;
      } else if (relation === 'behind') {
        shouldSwap = false;
      } else {
        // 'unknown' 狀況下,退回原本 196.js 的預設排序基礎 (例如 zAvg 深度)
        shouldSwap = polyA.zAvg < polyB.zAvg; 
      }

      if (shouldSwap) {
        const temp = polys[j];
        polys[j] = polys[j + 1];
        polys[j + 1] = temp;
        swapped = true;
      }
    }
    // 若某一輪完全沒有交換,代表排序已完成,提早結束
    if (!swapped) break;
  }

  // 2. 逆轉偵測機制:排序結束後,全面比對是否有面被強行排到了前面
  for (let i = 0; i < polys.length; i++) {
    for (let j = i + 1; j < polys.length; j++) {
      const pA = polys[i]; // 最終排序在後方的面 (繪圖時先畫)
      const pB = polys[j]; // 最終排序在前方的面 (繪圖時後畫,會遮擋 pA)

      // 檢查 pB 是否在直接判定中「原本被判定在 pA 後方」
      const directRel = this._evaluatePairwiseRelation(pB, pA);
      
      if (directRel === 'behind') {
        console.warn(
          `%c[排序位置逆轉告警] 發現面幾何衝突!\n` +
          `原因:在直接兩面評估中,面 (ID/Index: ${pB.index || j}) 被判定處於面 (ID/Index: ${pA.index || i}) 的【後方方位】。\n` +
          `但在經過冒泡排序傳遞傳播後,面 ${pB.index || j} 最終被排到了【前面】(最終索引 ${j} > ${i})。\n` +
          `這通常是由於 3D 空間中存在三個或多個面互相重疊的「循環遮擋 (Cyclic Overlap)」或非遞移性幾何關係所導致。`,
          "color: #ff9900; font-weight: bold;"
        );
      }
    }
  }
}

```

---

### 步驟五:整合進 196.js 的核心渲染流程

找到 `ejsolid196.js` 中對面進行排序的程式碼片段(通常位於 `render()`、`process()` 或 `sortPolygons()` 函式中,一般會看到類似 `this.state.currentPolys.sort(...)` 的寫法)。

將其修改為:

```javascript
// 尋找原本類似這一行的程式碼:
// this.state.currentPolys.sort((a, b) => b.zAvg - a.zAvg);

// 將其替換為條件分支控制:
if (this.state.useNormalEdgeSort) {
  // 當開啟切換按鈕時,使用 195993 的法向量邊緣判定搭配冒泡排序
  this.bubbleSortByNormalEdge(this.state.currentPolys);
} else {
  // 未開啟時,維持 196.js 原本的快速排序或深度排序演算法
  this.state.currentPolys.sort((a, b) => {
    // 這裡請保留您 196.js 原本內建的完整 sort 比較邏輯
    return b.zAvg - a.zAvg; 
  });
}

```

如此一來,當使用者啟用該按鈕時,系統將會拋棄原有的 `Array.sort`,改採**冒泡法遍歷每一條投影交會邊**進行精確計票排序;一旦遇到複雜幾何體產生的排序位置逆轉(例如經典的三角循環互遮結構),便會精準在瀏覽器的 `console` 中輸出詳細的追蹤成因報告。
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5653
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 179 次
被圈友点赞: 207 次
联系:

Re: 正在開發新遊戲:星•球

帖子 ejsoon »

改進:
當測試出現以下信息:
Status: A is Front, B is Front
Projection: Edges project onto each other (Overlap)
Projected Edges of A:
• Edge 1-2: Opposite to Normal B
• Edge 2-3: Opposite to Normal B
• Edge 3-0: Opposite to Normal B
Projected Edges of B:
• Edge 0-1: In direction of Normal A
Order: Face A is IN FRONT OF Face B

這是一個矛盾的結果,因為面A的三條邊都處於面B的法向量的反向,而面B的一條邊處於面A法向量的正向,因此面B應該在面A的前面,但是現在卻是面A處在前面。

每當出現這種矛盾的結果,將在測試框的下方,新增一個debug按鈕,當按下時,將(在按原來的邏輯排序後)輸出產生錯誤的原因。

只需給出所要修改的地方,所有的代碼和注釋都要使用英文。

代码: 全选

改進:
當測試出現以下信息:
Status: A is Front, B is Front
Projection: Edges project onto each other (Overlap)
Projected Edges of A:
• Edge 1-2: Opposite to Normal B
• Edge 2-3: Opposite to Normal B
• Edge 3-0: Opposite to Normal B
Projected Edges of B:
• Edge 0-1: In direction of Normal A
Order: Face A is IN FRONT OF Face B

這是一個矛盾的結果,因為面A的三條邊都處於面B的法向量的反向,而面B的一條邊處於面A法向量的正向,因此面B應該在面A的前面,但是現在卻是面A處在前面。

每當出現這種矛盾的結果,將在測試框的下方,新增一個debug按鈕,當按下時,將(在按原來的邏輯排序後)輸出產生錯誤的原因。

只需給出所要修改的地方,所有的代碼和注釋都要使用英文。
附件
ejsolid195993196.js
(60.06 KiB) 已下载 5 次
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5653
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 179 次
被圈友点赞: 207 次
联系:

Re: 正在開發新遊戲:星•球

帖子 ejsoon »

genini thinking:


代码: 全选

To add the contradiction detection mechanism and the dynamic debug button to the testing panel, replace the existing `evaluateIntersection()` method inside the `EjSolid` class with the following refactored version:

```javascript
  /**
   * Helper function to evaluate real-time testing of Face A and Face B projections.
   * Analyzes normal directions, tracking contradictions to reveal sorting bugs.
   */
  evaluateIntersection() {
    if (!this.state.selectedPolyA || !this.state.selectedPolyB || !this.dom.testResult) return;

    const polyA = this.dom.svgContainer.querySelector(`#${this.state.selectedPolyA}`);
    const polyB = this.dom.svgContainer.querySelector(`#${this.state.selectedPolyB}`);

    if (!polyA || !polyB) {
      this.dom.testResult.innerHTML = 'Result: One or both faces are hidden.';
      return;
    }

    // 1. Determine if faces are Front or Back
    const isAFront = polyA.getAttribute('data-front') === 'true' ? 'Front' : 'Back';
    const isBFront = polyB.getAttribute('data-front') === 'true' ? 'Front' : 'Back';

    // 2. Evaluate 2D Edge Projection (Overlap)
    const parsePoints = pointsStr => {
      return pointsStr
        .trim()
        .split(/\s+/)
        .map(pair => {
          const [x, y] = pair.split(',').map(Number);
          return { x, y };
        });
    };

    const ptsA = parsePoints(polyA.getAttribute('points'));
    const ptsB = parsePoints(polyB.getAttribute('points'));

    const overlaps = this.check2DOverlap(ptsA, ptsB);
    const overlapText = overlaps ? 'Edges project onto each other (Overlap)' : 'No projection overlap';

    // Local helpers for edge projection logic
    const isPointInPoly = (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 lineIntersects = (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 edgeProjects = (p1, p2, poly) => {
      if (isPointInPoly(p1, poly.pts) || isPointInPoly(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 (lineIntersects(p1, p2, q1, q2)) return true;
      }
      return false;
    };

    // 3. Analyze all projecting edges and their respective normal vector positions
    let edgesAText = '';
    let edgesBText = '';

    // Tracking geometric indicators for layer depth ordering
    let aImpliesAInFront = false;
    let aImpliesBInFront = false;
    let bImpliesBInFront = false;
    let bImpliesAInFront = false;

    if (this.state.currentPolys) {
      const dataA = this.state.currentPolys.find(p => p.id === this.state.selectedPolyA);
      const dataB = this.state.currentPolys.find(p => p.id === this.state.selectedPolyB);

      if (dataA && dataB) {
        // Output conditions for projecting edges of Face A onto Face B
        for (let i = 0; i < dataA.pts.length; i++) {
          const p1_2d = dataA.pts[i],
            p2_2d = dataA.pts[(i + 1) % dataA.pts.length];
          if (edgeProjects(p1_2d, p2_2d, dataB)) {
            const p1_3d = dataA.pts3D[i],
              p2_3d = dataA.pts3D[(i + 1) % dataA.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 = dataB.pts3D[0];
            const dot =
              (midX - q.x) * dataB.normal3D.x + (midY - q.y) * dataB.normal3D.y + (midZ - q.z) * dataB.normal3D.z;

            let status = '';
            if (Math.abs(dot) <= 1e-4) {
              status = 'On Plane B';
            } else {
              status = dot > 0 ? 'In direction of Normal B' : 'Opposite to Normal B';
              if (dot > 0) aImpliesAInFront = true;
              else aImpliesBInFront = true;
            }
            edgesAText += `<div style="padding-left: 10px; font-size: 12px; color: #555;">• Edge ${i}-${(i + 1) % dataA.pts.length}: ${status}</div>`;
          }
        }

        // Output conditions for projecting edges of Face B onto Face A
        for (let i = 0; i < dataB.pts.length; i++) {
          const p1_2d = dataB.pts[i],
            p2_2d = dataB.pts[(i + 1) % dataB.pts.length];
          if (edgeProjects(p1_2d, p2_2d, dataA)) {
            const p1_3d = dataB.pts3D[i],
              p2_3d = dataB.pts3D[(i + 1) % dataB.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 = dataA.pts3D[0];
            const dot =
              (midX - q.x) * dataA.normal3D.x + (midY - q.y) * dataA.normal3D.y + (midZ - q.z) * dataA.normal3D.z;

            let status = '';
            if (Math.abs(dot) <= 1e-4) {
              status = 'On Plane A';
            } else {
              status = dot > 0 ? 'In direction of Normal A' : 'Opposite to Normal A';
              if (dot > 0) bImpliesBInFront = true;
              else bImpliesAInFront = true;
            }
            edgesBText += `<div style="padding-left: 10px; font-size: 12px; color: #555;">• Edge ${i}-${(i + 1) % dataB.pts.length}: ${status}</div>`;
          }
        }
      }
    }

    if (!edgesAText) edgesAText = '<div style="padding-left: 10px; font-size: 12px; color: #999;">None</div>';
    if (!edgesBText) edgesBText = '<div style="padding-left: 10px; font-size: 12px; color: #999;">None</div>';

    // 4. Determine Render Order (Painter's Algorithm)
    const polygons = Array.from(this.dom.svgContainer.querySelectorAll('polygon'));
    const indexA = polygons.indexOf(polyA);
    const indexB = polygons.indexOf(polyB);

    let orderText = 'Same layer';
    if (indexA > indexB) {
      orderText = 'Face A is IN FRONT OF Face B';
    } else if (indexB > indexA) {
      orderText = 'Face B is IN FRONT OF Face A';
    }

    // 5. Evaluate Ordering Contradiction and Formulate Debug Reasoning
    const geomSaysAInFront = aImpliesAInFront || bImpliesAInFront;
    const geomSaysBInFront = aImpliesBInFront || bImpliesBInFront;
    const actualAInFront = indexA > indexB;
    const actualBInFront = indexB > indexA;

    let contradictionReason = '';
    if (geomSaysAInFront && !geomSaysBInFront && actualBInFront) {
      contradictionReason = 'Reason for error: Geometric edge-plane tests assert that Face A is positioned closer to the camera than Face B. However, the sorting routine left Face B on top. This is a manifestation of the single-pass Bubble Sort failing to correctly converge on non-transitive configurations.';
    } else if (geomSaysBInFront && !geomSaysAInFront && actualAInFront) {
      contradictionReason = 'Reason for error: Geometric edge-plane tests assert that Face B is positioned closer to the camera than Face A. However, the sorting routine left Face A on top. This is a manifestation of the single-pass Bubble Sort failing to correctly converge on non-transitive configurations.';
    } else if (geomSaysAInFront && geomSaysBInFront) {
      contradictionReason = 'Reason for error: Mutual occlusion loop detected! Face A contains points blocking Face B, and Face B contains points blocking Face A. This forms a geometric overlapping cycle or an intersecting layout that standard depth sorting cannot correct without splitting the face polygons.';
    }

    // Update the UI panel with the formatted edge results and conditional debug button
    this.dom.testResult.innerHTML = `
      <div style="margin-bottom: 5px; color: #333;"><strong>Status:</strong> A is ${isAFront}, B is ${isBFront}</div>
      <div style="margin-bottom: 5px; color: #333;"><strong>Projection:</strong> ${overlapText}</div>
      <div style="margin-bottom: 3px; color: #2a9d8f;"><strong>Projected Edges of A:</strong></div>
      ${edgesAText}
      <div style="margin-top: 5px; margin-bottom: 3px; color: #2a9d8f;"><strong>Projected Edges of B:</strong></div>
      ${edgesBText}
      <div style="margin-top: 5px; color: #0077b6;"><strong>Order:</strong> ${orderText}</div>
      ${contradictionReason ? `
        <button id="ejs-debug-btn" style="margin-top: 12px; padding: 6px 12px; background: #eb2563; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 11px; width: 100%; font-weight: 600;">Debug Sorting Error</button>
        <div id="ejs-debug-reason" style="margin-top: 8px; padding: 10px; background: #fff0f3; border: 1px solid #ffccd5; font-size: 11px; color: #eb2563; display: none; border-radius: 6px; line-height: 1.4;"></div>
      ` : ''}
    `;

    // Attach dynamic click tracking handler if a debug interface was attached
    if (contradictionReason) {
      const debugBtn = this.dom.testResult.querySelector('#ejs-debug-btn');
      const debugReasonDiv = this.dom.testResult.querySelector('#ejs-debug-reason');
      if (debugBtn && debugReasonDiv) {
        debugBtn.onclick = () => {
          if (debugReasonDiv.style.display === 'none') {
            debugReasonDiv.textContent = contradictionReason;
            debugReasonDiv.style.display = 'block';
          } else {
            debugReasonDiv.style.display = 'none';
          }
        };
      }
    }
  }

```
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5653
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 179 次
被圈友点赞: 207 次
联系:

Re: 正在開發新遊戲:星•球

帖子 ejsoon »

改進:
當測試出現以下信息:
Status: A is Front, B is Front
Projection: Edges project onto each other (Overlap)
Projected Edges of A:
• Edge 1-2: Opposite to Normal B
• Edge 2-3: Opposite to Normal B
• Edge 3-0: Opposite to Normal B
Projected Edges of B:
• Edge 0-1: In direction of Normal A
Order: Face A is IN FRONT OF Face B

這是一個矛盾的結果,因為面A的三條邊都處於面B的法向量的反向,而面B的一條邊處於面A法向量的正向,因此面B應該在面A的前面,但是現在卻是面A處在前面。

每當出現這種矛盾的結果,將在測試框的下方,新增一個debug按鈕,當按下時,將(在按原來的邏輯排序後)輸出產生錯誤的原因。

只需給出所要修改的地方,所有的代碼和注釋都要使用英文。




在代碼中修正這個排序錯誤。

只需給出所要修改的地方,所有的代碼和注釋都要使用英文。


代码: 全选

在 `draw()` 方法中,利用 Newell's Algorithm 進行深度排序時,其平面測試(Plane Tests)邏輯存在排序錯誤。

原本的程式碼在檢查投影邊與平面的相對位置時,只要遇到第一個不共面的邊(`Math.abs(dot) > 1e-4`)就會直接執行 `break`。這會導致如果第一個被檢測的邊剛好位於平面後方(`dot < 0`),程式就會過早終止迴圈並判定 `shouldSwap = false`(不需要交換),從而忽略了該多邊形其他可能位於平面前方、真正需要進行交換的邊。

為了修正這個錯誤,必須完整遍歷多邊形的所有邊,確認其整體的空間相對關係後,再決定是否需要交換。以下是需要修改的程式碼區塊(程式碼與注釋皆已調整為英文):

### 修改前的程式碼區塊:

```javascript
// 1. Test the relative position of projected edges of Face A to the normal vector of Face B
for (let k = 0; k < a.pts.length; k++) {
  const p1_2d = a.pts[k],
    p2_2d = a.pts[(k + 1) % a.pts.length];
  if (edgeProjects(p1_2d, p2_2d, b)) {
    const p1_3d = a.pts3D[k],
      p2_3d = a.pts3D[(k + 1) % a.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 = b.pts3D[0];
    const dot = (midX - q.x) * b.normal3D.x + (midY - q.y) * b.normal3D.y + (midZ - q.z) * b.normal3D.z;

    if (Math.abs(dot) > 1e-4) {
      // dot > 0 means Face A is in front of Face B (A occludes B), so A must be drawn later.
      // The original order is [a, b] (a drawn first, b drawn later), so they need to be swapped to [b, a].
      shouldSwap = dot > 0;
      foundRelation = true;
      break;
    }
  }
}

// 2. If no conclusive relation is found from Face A, test the projected edges of Face B relative to the normal vector of Face A
if (!foundRelation) {
  for (let k = 0; k < b.pts.length; k++) {
    const p1_2d = b.pts[k],
      p2_2d = b.pts[(k + 1) % b.pts.length];
    if (edgeProjects(p1_2d, p2_2d, a)) {
      const p1_3d = b.pts3D[k],
        p2_3d = b.pts3D[(k + 1) % b.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 = a.pts3D[0];
      const dot = (midX - q.x) * a.normal3D.x + (midY - q.y) * a.normal3D.y + (midZ - q.z) * a.normal3D.z;

      if (Math.abs(dot) > 1e-4) {
        // dot > 0 means Face B is in front of Face A (B occludes A), so B must be drawn later.
        // The original order is [a, b] where B is already drawn later, so NO swap is needed when dot > 0; swap when dot < 0.
        shouldSwap = dot < 0;
        foundRelation = true;
        break;
      }
    }
  }
}

```

### 修改後的程式碼區塊:

```javascript
// 1. Test the relative position of projected edges of Face A to the normal vector of Face B
let anyAInFront = false;
let anyABehind = false;
for (let k = 0; k < a.pts.length; k++) {
  const p1_2d = a.pts[k],
    p2_2d = a.pts[(k + 1) % a.pts.length];
  if (edgeProjects(p1_2d, p2_2d, b)) {
    const p1_3d = a.pts3D[k],
      p2_3d = a.pts3D[(k + 1) % a.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 = b.pts3D[0];
    const dot = (midX - q.x) * b.normal3D.x + (midY - q.y) * b.normal3D.y + (midZ - q.z) * b.normal3D.z;

    if (dot > 1e-4) anyAInFront = true;
    if (dot < -1e-4) anyABehind = true;
  }
}

// Evaluate the complete plane relation for Face A
if (anyAInFront) {
  shouldSwap = true;
  foundRelation = true;
} else if (anyABehind) {
  shouldSwap = false;
  foundRelation = true;
}

// 2. If no conclusive relation is found from Face A, test the projected edges of Face B relative to the normal vector of Face A
if (!foundRelation) {
  let anyBInFront = false;
  let anyBBehind = false;
  for (let k = 0; k < b.pts.length; k++) {
    const p1_2d = b.pts[k],
      p2_2d = b.pts[(k + 1) % b.pts.length];
    if (edgeProjects(p1_2d, p2_2d, a)) {
      const p1_3d = b.pts3D[k],
        p2_3d = b.pts3D[(k + 1) % b.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 = a.pts3D[0];
      const dot = (midX - q.x) * a.normal3D.x + (midY - q.y) * a.normal3D.y + (midZ - q.z) * a.normal3D.z;

      if (dot > 1e-4) anyBInFront = true;
      if (dot < -1e-4) anyBBehind = true;
    }
  }

  // Evaluate the complete plane relation for Face B
  if (anyBBehind) {
    shouldSwap = true;
    foundRelation = true;
  } else if (anyBInFront) {
    shouldSwap = false;
    foundRelation = true;
  }
}

```
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5653
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 179 次
被圈友点赞: 207 次
联系:

Re: 正在開發新遊戲:星•球

帖子 ejsoon »

在一個陣列中,遍歷每一項:

當某一項被選取時,它將跟所有的項先判定是否發生干涉(判定函式已寫好),只跟有干涉的項對比大小,小的放在前面,不涉的不對比。最終得到結果是一個排列好的陣列,前面的項對比處於後面的項,要麼沒發生干涉,要麼一定在比較之後更小。

代码: 全选

這個需求非常經典,本質上是一個**客製化排序**的問題。

你所描述的規則(只跟有干涉的項比較大小,沒干涉的就不管),在電腦科學中被稱為**偏序關係(Partial Order)**。因為不是任意兩項都可以比較大小,所以傳統的快速排序(Quick Sort)或合併排序(Merge Sort)在這裡**不適用**,直接使用它們可能會導致排序結果混亂,甚至陷入無窮迴圈。

要完美解決這個問題,最核心、最穩定的做法是使用**拓撲排序(Topological Sort)**。

---

## 💡 解題思路:圖論與拓撲排序

我們可以把陣列中的每一項看作是圖(Graph)裡面的**節點(Node)**:

1. **建立關係(連線):** 遍歷陣列,讓每一項跟其他項進行干涉判定。如果 $A$ 和 $B$ 發生干涉,且 $A < B$,代表 $A$ 必須排在 $B$ 的前面。我們就建立一條從 $A$ 指向 $B$ 的有向邊($A \rightarrow B$)。
2. **計算入度(In-degree):** 統計每個節點有多少條「被指向」的線。入度為 0 代表目前沒有任何比它小、且跟它有干涉的項擋在前面。
3. **拓撲排序:** * 每次挑選入度為 0 的節點放入結果陣列中(因為它現在最安全,可以直接排在前面)。
* 將這個節點從圖中移除,並把它指向的其他節點的入度減 1。
* 重複此步驟,直到所有節點都被排出。



這樣做能保證:**只要兩者有干涉,小的絕對在前面;沒干涉的,順序隨緣,但絕對不會破壞有干涉項的相對順序。**

---

## 🛠️ 程式碼實作 (以 JavaScript 為例)

假設你已經寫好的干涉判定函式叫做 `isInterference(a, b)`,且物件本身可以用 `<` 或自訂邏輯比較大小。

```javascript
function partialOrderSort(arr, isInterference) {
    const n = arr.length;
    
    // 1. 初始化鄰接表 (Graph) 與 入度統計表 (In-degree)
    const graph = Array.from({ length: n }, () => []);
    const inDegree = new Array(n).fill(0);
    
    // 2. 兩兩比較,建立有向圖
    for (let i = 0; i < n; i++) {
        for (let j = i + 1; j < n; j++) {
            // 判定是否發生干涉
            if (isInterference(arr[i], arr[j])) {
                // 有干涉,對比大小(這裡假設元素可以直接比大小,或你可以換成自訂的數值比較)
                if (arr[i] < arr[j]) {
                    graph[i].push(j); // i 應該在 j 前面 (i -> j)
                    inDegree[j]++;
                } else if (arr[i] > arr[j]) {
                    graph[j].push(i); // j 應該在 i 前面 (j -> i)
                    inDegree[i]++;
                }
                // 如果相等,可以不連線,保持原本相對順序
            }
        }
    }
    
    // 3. 將所有入度為 0 的節點放入佇列 (Queue)
    const queue = [];
    for (let i = 0; i < n; i++) {
        if (inDegree[i] === 0) {
            queue.push(i);
        }
    }
    
    const result = [];
    
    // 4. 開始拓撲排序
    while (queue.length > 0) {
        // 彈出一個沒有任何限制(或限制已被解除)的節點
        const curr = queue.shift();
        result.push(arr[curr]);
        
        // 遍歷這個節點指向的所有鄰居
        for (const neighbor of graph[curr]) {
            inDegree[neighbor]--; // 擋在鄰居前面的山頭少了一個
            if (inDegree[neighbor] === 0) {
                queue.push(neighbor); // 如果鄰居也沒有任何阻礙了,加入佇列
            }
        }
    }
    
    // 檢查是否有環(合理情況下,單純的大小比較不會有環,除非你的大小關係或干涉邏輯會互咬)
    if (result.length !== n) {
        throw new Error("偵測到循環依賴,無法完成排序!");
    }
    
    return result;
}

```

---

## 📝 實測範例

假設我們有一組數字,**干涉規則是:「只有當兩個數都是偶數,或者都是奇數時,才算干涉(同奇同偶才比較大小)」**。

```javascript
// 測試用的干涉判定:同奇同偶才干涉
function myInterference(a, b) {
    return (a % 2) === (b % 2);
}

const input = [9, 4, 1, 8, 2, 3];
const output = partialOrderSort(input, myInterference);

console.log("排序後結果:", output); 
// 可能的輸出之一: [1, 2, 3, 4, 9, 8] 

```

### 為什麼這個結果是對的?

我們來檢驗你提出的最終要求:

* **有干涉的項(同為偶數):** `2`, `4`, `8` 的相對順序是 `2 -> 4 -> 8`(小的確實在前面)。
* **有干涉的項(同為奇數):** `1`, `3`, `9` 的相對順序是 `1 -> 3 -> 9`(小的確實在前面)。
* **沒干涉的項(一奇一偶):** 例如 `2` 和 `3`,它們在陣列中的前後順序完全不影響結果,因為它們不干涉,不需要對比。

這個演算法的時間複雜度是 $O(N^2)$(因為建立圖需要兩兩判定干涉),對於幾百到幾千筆資料的陣列來說,執行速度都是瞬間完成,非常適合你的情境!
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5653
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 179 次
被圈友点赞: 207 次
联系:

Re: 正在開發新遊戲:星•球

帖子 ejsoon »

判定法向量方位:

代码: 全选

/**
 * Evaluates whether Face A is positioned in front of Face B based on normal vectors.
 * It iterates through all edges and returns true or false immediately upon finding a definitive relation.
 * If no definitive relation is found, it logs a warning listing the coplanar edges and returns null.
 * * @param {Object} faceA - The first face object containing pts, pts3D, and normal3D.
 * @param {Object} faceB - The second face object containing pts, pts3D, and normal3D.
 * @param {Function} edgeProjects - The helper function that checks 2D projection overlap.
 * @returns {boolean|null} true if Face A is in front, false if Face A is behind, or null if inconclusive.
 */
function checkEdgeNormalRelation(faceA, faceB, edgeProjects) {
  const coplanarEdgesA = [];
  const coplanarEdgesB = [];

  // ==========================================
  // Step 1: Evaluate projecting edges of Face A against Face B's plane
  // ==========================================
  for (let i = 0; i < faceA.pts.length; i++) {
    const p1_2d = faceA.pts[i];
    const p2_2d = faceA.pts[(i + 1) % faceA.pts.length];
    
    if (edgeProjects(p1_2d, p2_2d, faceB)) {
      const p1_3d = faceA.pts3D[i];
      const p2_3d = faceA.pts3D[(i + 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 (Math.abs(dot) > 1e-4) {
        return dot > 0; // true if front, false if behind
      } else {
        coplanarEdgesA.push(i);
      }
    }
  }

  // ==========================================
  // Step 2: Evaluate projecting edges of Face B against Face A's plane
  // ==========================================
  for (let i = 0; i < faceB.pts.length; i++) {
    const p1_2d = faceB.pts[i];
    const p2_2d = faceB.pts[(i + 1) % faceB.pts.length];
    
    if (edgeProjects(p1_2d, p2_2d, faceA)) {
      const p1_3d = faceB.pts3D[i];
      const p2_3d = faceB.pts3D[(i + 1) % faceB.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 = faceA.pts3D[0];
      
      const dot = (midX - q.x) * faceA.normal3D.x + 
                  (midY - q.y) * faceA.normal3D.y + 
                  (midZ - q.z) * faceA.normal3D.z;

      if (Math.abs(dot) > 1e-4) {
        return dot <= 0; // false if dot > 0 (A is behind), true if dot < 0 (A is front)
      } else {
        coplanarEdgesB.push(i);
      }
    }
  }

  // ==========================================
  // Step 3: Handle inconclusive cases
  // ==========================================
  if (coplanarEdgesA.length > 0) {
    console.warn(`Which edges of Face A are completely on Face B: Indices [${coplanarEdgesA.join(', ')}]`);
  }
  if (coplanarEdgesB.length > 0) {
    console.warn(`Which edges of Face B are completely on Face A: Indices [${coplanarEdgesB.join(', ')}]`);
  }
  if (coplanarEdgesA.length === 0 && coplanarEdgesB.length === 0) {
    console.warn("No overlapping projection edges found between Face A and Face B.");
  }

  return null;
}
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5653
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 179 次
被圈友点赞: 207 次
联系:

Re: 正在開發新遊戲:星•球

帖子 ejsoon »

將代碼中的polys.sort((a, b) => a.avgZ - b.avgZ)替換為一個函式:
一,將所有的polys分為正面和反面,正面將置於反面的前面。(這裡的前面指的是最靠近當前視角的面,也就是svg最後渲染的面)

二,反面仍用原來的方法排序。

三,正面將用拓撲排序(Topological Sort)。只跟有干涉的面比較大小,沒干涉的就不管。

首先用check2DOverlap判定哪些面之間存在干涉,之後用checkEdgeNormalRelation來判定哪些面應該在前。

把這個函式放到class的最後,前面如有必要可加上某些屬性。

只需給出所要修改的地方,所有的代碼和注釋都要使用英文。

代码: 全选

將代碼中的polys.sort((a, b) => a.avgZ - b.avgZ)替換為一個函式:
一,將所有的polys分為正面和反面,正面將置於反面的前面。(這裡的前面指的是最靠近當前視角的面,也就是svg最後渲染的面)

二,反面仍用原來的方法排序。

三,正面將用拓撲排序(Topological Sort)。只跟有干涉的面比較大小,沒干涉的就不管。

首先用check2DOverlap判定哪些面之間存在干涉,之後用checkEdgeNormalRelation來判定哪些面應該在前。

把這個函式放到class的最後,前面如有必要可加上某些屬性。

只需給出所要修改的地方,所有的代碼和注釋都要使用英文。
附件
ejsolid1968.js
(55.84 KiB) 已下载 7 次
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5653
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 179 次
被圈友点赞: 207 次
联系:

Re: 正在開發新遊戲:星•球

帖子 ejsoon »

gemini pro:

代码: 全选

Here are the necessary modifications to implement the topological sorting mechanism.

### 1. Modification in the `draw()` method

Replace the section where `projected`, `polys`, and `polys.sort()` are defined to inject the 3D data and use the new sorting method.

**Replace this block:**

```javascript
    // Project vertices for the current static frame
    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;
      return { x: rx * scale * f + offset, y: -ry * scale * f + offset, z: rz };
    });

    const polys = [];
    faces.forEach((fIdx, idx) => {
      const pts = fIdx.map(i => projected[i]);
      polys.push({
        pts,
        avgZ: pts.reduce((s, p) => s + p.z, 0) / pts.length,
        sides: pts.length,
        fIdx,
        id: `ejs-${idx}`
      });
    });

    polys.sort((a, b) => a.avgZ - b.avgZ);

```

**With this code:**

```javascript
    // Project vertices for the current static frame
    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;
      // Add rx, ry, rz to the projection for 3D normal and depth calculations
      return { x: rx * scale * f + offset, y: -ry * scale * f + offset, z: rz, rx, ry, rz };
    });

    let polys = [];
    faces.forEach((fIdx, idx) => {
      const pts = fIdx.map(i => projected[i]);
      const pts3D = fIdx.map(i => ({ x: projected[i].rx, y: projected[i].ry, z: projected[i].rz }));

      // Determine if it is a front face via cross product
      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 vector for Edge Normal Relations
      let normal3D = { x: 0, y: 0, z: 0 };
      if (pts3D.length >= 3) {
        const v1 = { x: pts3D[1].x - pts3D[0].x, y: pts3D[1].y - pts3D[0].y, z: pts3D[1].z - pts3D[0].z };
        const v2 = { x: pts3D[2].x - pts3D[0].x, y: pts3D[2].y - pts3D[0].y, z: pts3D[2].z - pts3D[0].z };
        normal3D = {
          x: v1.y * v2.z - v1.z * v2.y,
          y: v1.z * v2.x - v1.x * v2.z,
          z: v1.x * v2.y - v1.y * v2.x
        };
        const len = Math.sqrt(normal3D.x * normal3D.x + normal3D.y * normal3D.y + normal3D.z * normal3D.z);
        if (len > 0) {
          normal3D.x /= len; normal3D.y /= len; normal3D.z /= len;
        }
      }

      polys.push({
        pts,
        pts3D,
        normal3D,
        isFront,
        avgZ: pts.reduce((s, p) => s + p.z, 0) / pts.length,
        sides: pts.length,
        fIdx,
        id: `ejs-${idx}`
      });
    });

    // Replace basic sort with advanced topological sort
    polys = this.sortPolygonsTopological(polys);

```

---

### 2. Add the New Sorting Function

Add the following method directly to the very end of the `EjSolid` class (right before the class's closing bracket `}`).

```javascript
  /**
   * Advanced sorting mechanism that separates faces into back and front.
   * Back faces are sorted by average Z-depth.
   * Front faces are sorted using a Topological Sort based on 2D overlap and edge relations.
   * @param {Array} polygons - The array of prepared polygon objects.
   * @returns {Array} The fully ordered array.
   */
  sortPolygonsTopological(polygons) {
    const frontFaces = [];
    const backFaces = [];

    // 1. Separate into front and back faces
    polygons.forEach(poly => {
      if (poly.isFront) {
        frontFaces.push(poly);
      } else {
        backFaces.push(poly);
      }
    });

    // 2. Sort back faces using the original Z-average method
    backFaces.sort((a, b) => a.avgZ - b.avgZ);

    // 3. Sort front faces using Topological Sort (Kahn's Algorithm)
    const graph = new Map();
    const inDegree = new Map();

    // Initialize graph maps
    frontFaces.forEach(f => {
      graph.set(f, []);
      inDegree.set(f, 0);
    });

    // Build the dependency graph based on interference
    for (let i = 0; i < frontFaces.length; i++) {
      for (let j = i + 1; j < frontFaces.length; j++) {
        const faceA = frontFaces[i];
        const faceB = frontFaces[j];

        // Check if faces interfere with each other in the 2D projection
        if (this.check2DOverlap(faceA.pts, faceB.pts)) {
          const isAInFront = this.checkEdgeNormalRelation(faceA, faceB);
          
          if (isAInFront === true) {
            // Face A is in front, so Face B must be drawn before Face A. (Edge: B -> A)
            graph.get(faceB).push(faceA);
            inDegree.set(faceA, inDegree.get(faceA) + 1);
          } else if (isAInFront === false) {
            // Face B is in front, so Face A must be drawn before Face B. (Edge: A -> B)
            graph.get(faceA).push(faceB);
            inDegree.set(faceB, inDegree.get(faceB) + 1);
          } else {
            // Inconclusive relations (e.g. strict edge alignment or coplanar without strict projection overlap).
            // Prevent topological cycles by falling back to average Z-depth for this specific pair.
            if (faceA.avgZ < faceB.avgZ) {
              graph.get(faceA).push(faceB);
              inDegree.set(faceB, inDegree.get(faceB) + 1);
            } else {
              graph.get(faceB).push(faceA);
              inDegree.set(faceA, inDegree.get(faceA) + 1);
            }
          }
        }
      }
    }

    // Execute Topological Sort
    const queue = [];
    inDegree.forEach((degree, node) => {
      if (degree === 0) queue.push(node);
    });

    const sortedFrontFaces = [];
    while (queue.length > 0) {
      const current = queue.shift();
      sortedFrontFaces.push(current);

      const neighbors = graph.get(current);
      neighbors.forEach(neighbor => {
        inDegree.set(neighbor, inDegree.get(neighbor) - 1);
        if (inDegree.get(neighbor) === 0) {
          queue.push(neighbor);
        }
      });
    }

    // Fallback for cyclic dependencies (intersecting faces that Kahn's algorithm leaves behind)
    if (sortedFrontFaces.length !== frontFaces.length) {
      console.warn("Cyclic dependencies detected in front faces. Falling back to avgZ for remaining cyclic polygons.");
      const remainingFaces = frontFaces.filter(f => !sortedFrontFaces.includes(f));
      remainingFaces.sort((a, b) => a.avgZ - b.avgZ);
      sortedFrontFaces.push(...remainingFaces);
    }

    // Return back faces followed by front faces
    return backFaces.concat(sortedFrontFaces);
  }

```
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5653
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 179 次
被圈友点赞: 207 次
联系:

Re: 正在開發新遊戲:星•球

帖子 ejsoon »

在左欄Visibility下方添加一個測試欄:
增加一個「檢測正面」按鈕,當點擊時,它將把每個正面列出,並隨機填色。

列表中包含:id,顏色,所有與它干涉的面及前後關係。

點擊顏色可更換。

當點擊其中一個「前後關係」,它將給出兩個面中每一條有干涉的邊與另一個面的法向量方位。

在「檢測正面」按鈕的右邊有一個「reset」按鈕,當點擊時將取消檢測。

只需給出所要修改的地方,所有的代碼和注釋都要使用英文。

代码: 全选

在左欄Visibility下方添加一個測試欄:
增加一個「檢測正面」按鈕,當點擊時,它將把每個正面列出,並隨機填色。

列表中包含:id,顏色,所有與它干涉的面及前後關係。

點擊顏色可更換。

當點擊其中一個「前後關係」,它將給出兩個面中每一條有干涉的邊與另一個面的法向量方位。

在「檢測正面」按鈕的右邊有一個「reset」按鈕,當點擊時將取消檢測。

只需給出所要修改的地方,所有的代碼和注釋都要使用英文。
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5653
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 179 次
被圈友点赞: 207 次
联系:

Re: 正在開發新遊戲:星•球

帖子 ejsoon »

gemini pro:

代码: 全选

Here are the modifications required to implement the "Test Front Faces" feature.

### 1. Update State Variables

In the `constructor(containerSelector)`, add `testMode` and `testColors` to `this.state`.

**Modify `this.state` initialization:**

```javascript
    this.state = {
      rotMatrix: [1, 0, 0, 0, 1, 0, 0, 0, 1],
      lastSvg: '',
      isDragging: false,
      lastX: 0,
      lastY: 0,
      objData: `...`, // (Original objData content remains unchanged)
      parsedModel: null,
      rawJsonData: this.container.textContent.trim(),
      testMode: false, // New: Tracks if the test mode is active
      testColors: {}   // New: Stores random colors assigned to front faces
    };

```

### 2. Update DOM Structure

In the `renderDOM()` method, locate the "Visibility" section in the main mode and insert the new test UI right below it.

**Replace this block:**

```javascript
                <h2>Visibility</h2>
                <div class="ejsolid-control-group">
                    <label><input type="checkbox" class="ejs-perspective" /> Perspective</label>
                </div>
                <h2>Style</h2>

```

**With this code:**

```javascript
                <h2>Visibility</h2>
                <div class="ejsolid-control-group">
                    <label><input type="checkbox" class="ejs-perspective" /> Perspective</label>
                </div>
                
                <h2>Test Front Faces</h2>
                <div class="ejsolid-control-group">
                    <div class="ejsolid-row" style="grid-template-columns: 1fr 1fr; gap: 10px;">
                        <button type="button" class="ejs-test-front-btn" style="padding: 8px 15px; border-radius: 6px;">Test Front Faces</button>
                        <button type="button" class="ejs-reset-test-btn" style="padding: 8px 15px; border-radius: 6px; background-color: #6c757d;">Reset</button>
                    </div>
                    <div class="ejs-test-results" style="max-height: 250px; overflow-y: auto; font-size: 12px; display: none; flex-direction: column; gap: 10px; padding: 5px; border: 1px solid #ddd; border-radius: 6px; background: #fafafa;"></div>
                </div>
                
                <h2>Style</h2>

```

### 3. Cache the New Elements

In the `cacheDOM()` method, add the new buttons and results container to `this.dom`.

**Add inside `this.dom = { ... };` :**

```javascript
      testFrontBtn: this.container.querySelector('.ejs-test-front-btn'),
      resetTestBtn: this.container.querySelector('.ejs-reset-test-btn'),
      testResults: this.container.querySelector('.ejs-test-results'),

```

### 4. Bind Events for the Buttons

In the `bindEvents()` method, add the click listeners to activate and reset the test mode.

**Add this code (e.g., right below the SVG container drag handlers):**

```javascript
    // Test Front Faces Events
    if (this.dom.testFrontBtn) {
      this.dom.testFrontBtn.onclick = () => {
        this.state.testMode = true;
        this.state.testColors = {}; // Reset colors on fresh test
        this.dom.testResults.style.display = 'flex';
        this.draw();
      };
    }

    if (this.dom.resetTestBtn) {
      this.dom.resetTestBtn.onclick = () => {
        this.state.testMode = false;
        this.dom.testResults.style.display = 'none';
        this.dom.testResults.innerHTML = '';
        this.draw();
      };
    }

```

### 5. Apply the Random Colors & Trigger Render in `draw()`

In the `draw()` method, apply the generated random colors if the test mode is active, and call the rendering function after the topological sort.

**First change inside `draw()` (Applying Colors):**
Find how `fill` is assigned within the `polys.forEach` loop:

```javascript
    polys.forEach((p, i) => {
      const pStr = p.pts.map(pt => `${pt.x.toFixed(2)},${pt.y.toFixed(2)}`).join(' ');
      const fill = this.config.defaultColors[p.sides] || '#ccc';
      const polyIdAttr = splitOpacity ? ` id="${p.id}"` : ``;

```

**Replace with:**

```javascript
    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';
      
      // Override with random test color if active
      if (this.state.testMode && p.isFront && this.state.testColors[p.id]) {
        fill = this.state.testColors[p.id];
      }
      
      const polyIdAttr = splitOpacity ? ` id="${p.id}"` : ``;

```

**Second change inside `draw()` (Triggering List Update):**
Find where the topological sort happens:

```javascript
    // Replace basic sort with advanced topological sort
    polys = this.sortPolygonsTopological(polys);

```

**Insert this immediately after:**

```javascript
    // Call render test results if test mode is active
    if (this.state.testMode) {
      this.renderTestResults(polys.filter(p => p.isFront));
    }

```

### 6. Add Helper Methods

Add these two new methods at the bottom of the `EjSolid` class (right before the final `}`).

```javascript
  /**
   * Renders the test results list for front faces.
   * @param {Array} frontFaces - Array of faces evaluated as front.
   */
  renderTestResults(frontFaces) {
    this.dom.testResults.innerHTML = '';
    
    // Assign random colors if not already assigned
    frontFaces.forEach(f => {
      if (!this.state.testColors[f.id]) {
        this.state.testColors[f.id] = '#' + Math.floor(Math.random() * 16777215).toString(16).padStart(6, '0');
      }
    });

    frontFaces.forEach(fA => {
      const item = document.createElement('div');
      item.style.borderBottom = '1px solid #ccc';
      item.style.paddingBottom = '8px';
      
      // Header: ID and Color Box
      const header = document.createElement('div');
      header.style.display = 'flex';
      header.style.alignItems = 'center';
      header.style.gap = '10px';
      
      const title = document.createElement('strong');
      title.textContent = `Face ID: ${fA.id}`;
      
      const colorBox = document.createElement('div');
      colorBox.style.width = '20px';
      colorBox.style.height = '20px';
      colorBox.style.borderRadius = '4px';
      colorBox.style.backgroundColor = this.state.testColors[fA.id];
      colorBox.style.cursor = 'pointer';
      colorBox.style.border = '1px solid #999';
      colorBox.title = 'Click to change color';
      
      colorBox.onclick = () => {
        this.state.testColors[fA.id] = '#' + Math.floor(Math.random() * 16777215).toString(16).padStart(6, '0');
        colorBox.style.backgroundColor = this.state.testColors[fA.id];
        this.draw(); // Re-render to show new color
      };
      
      header.appendChild(title);
      header.appendChild(colorBox);
      item.appendChild(header);

      // Interferences list
      const relList = document.createElement('ul');
      relList.style.margin = '5px 0 0 0';
      relList.style.paddingLeft = '20px';
      relList.style.listStyleType = 'square';

      frontFaces.forEach(fB => {
        if (fA.id === fB.id) return;
        
        if (this.check2DOverlap(fA.pts, fB.pts)) {
          const isAInFront = this.checkEdgeNormalRelation(fA, fB);
          let relStr = 'Inconclusive with';
          let colorStr = '#666';
          
          if (isAInFront === true) {
             relStr = 'In front of';
             colorStr = '#2a9d8f';
          } else if (isAInFront === false) {
             relStr = 'Behind';
             colorStr = '#e76f51';
          }
          
          const relItem = document.createElement('li');
          const relLabel = document.createElement('span');
          relLabel.innerHTML = `<span style="color:${colorStr}; font-weight:600; cursor:pointer; text-decoration:underline;">${relStr} ${fB.id}</span>`;
          
          const detailsDiv = document.createElement('div');
          detailsDiv.style.display = 'none';
          detailsDiv.style.fontSize = '11px';
          detailsDiv.style.color = '#555';
          detailsDiv.style.background = '#eee';
          detailsDiv.style.padding = '5px';
          detailsDiv.style.marginTop = '4px';
          detailsDiv.style.borderRadius = '4px';
          detailsDiv.style.fontFamily = 'monospace';
          
          relLabel.onclick = () => {
            if (detailsDiv.style.display === 'none') {
               detailsDiv.innerHTML = this.getEdgeNormalDetails(fA, fB);
               detailsDiv.style.display = 'block';
            } else {
               detailsDiv.style.display = 'none';
            }
          };
          
          relItem.appendChild(relLabel);
          relItem.appendChild(detailsDiv);
          relList.appendChild(relItem);
        }
      });
      
      if (relList.children.length === 0) {
        const noInt = document.createElement('div');
        noInt.style.color = '#999';
        noInt.style.fontStyle = 'italic';
        noInt.style.marginTop = '4px';
        noInt.textContent = 'No interferences.';
        item.appendChild(noInt);
      } else {
        item.appendChild(relList);
      }
      
      this.dom.testResults.appendChild(item);
    });
  }

  /**
   * Generates detailed logs of edge projections against the other face's normal.
   * @param {Object} faceA 
   * @param {Object} faceB 
   * @returns {string} HTML string detailing the relations.
   */
  getEdgeNormalDetails(faceA, faceB) {
    let html = `<strong>Evaluating ${faceA.id} edges against ${faceB.id} normal:</strong><br>`;
    let foundA = false;
    for (let i = 0; i < faceA.pts.length; i++) {
      const p1_2d = faceA.pts[i];
      const p2_2d = faceA.pts[(i + 1) % faceA.pts.length];
      
      if (this.edgeProjects(p1_2d, p2_2d, faceB)) {
        foundA = true;
        const p1_3d = faceA.pts3D[i];
        const p2_3d = faceA.pts3D[(i + 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;
        const relation = Math.abs(dot) > 1e-4 ? (dot > 0 ? 'Front' : 'Behind') : 'Coplanar';
        html += `Edge ${i}: dot = ${dot.toFixed(4)} &rarr; <b>${relation}</b><br>`;
      }
    }
    if (!foundA) html += `No projecting edges found.<br>`;

    html += `<br><strong>Evaluating ${faceB.id} edges against ${faceA.id} normal:</strong><br>`;
    let foundB = false;
    for (let i = 0; i < faceB.pts.length; i++) {
      const p1_2d = faceB.pts[i];
      const p2_2d = faceB.pts[(i + 1) % faceB.pts.length];
      
      if (this.edgeProjects(p1_2d, p2_2d, faceA)) {
        foundB = true;
        const p1_3d = faceB.pts3D[i];
        const p2_3d = faceB.pts3D[(i + 1) % faceB.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 = faceA.pts3D[0];
        
        const dot = (midX - q.x) * faceA.normal3D.x + (midY - q.y) * faceA.normal3D.y + (midZ - q.z) * faceA.normal3D.z;
        const relation = Math.abs(dot) > 1e-4 ? (dot > 0 ? 'Behind' : 'Front') : 'Coplanar';
        html += `Edge ${i}: dot = ${dot.toFixed(4)} &rarr; <b>${relation}</b><br>`;
      }
    }
    if (!foundB) html += `No projecting edges found.<br>`;
    
    return html;
  }

```
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5653
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 179 次
被圈友点赞: 207 次
联系:

Re: 正在開發新遊戲:星•球

帖子 ejsoon »

改進checkEdgeNormalRelation這個函式:
首先檢測A面有沒有端點與B面干涉,如果有則用這個點與B面的法向量判定前後。如果沒有,則檢測B面有沒有端點與A面干涉,如果有則用這個點與A面的法向量判定前後。如果兩個面都沒有點與另一個面干涉,再用之前的邊判定。

排除點在面上的情況,當兩個面的點都處於另一個面上,則像之前那樣警告並返回null。

代码: 全选

  /**
   * Evaluates whether Face A is positioned in front of Face B based on normal vectors.
   * It iterates through all edges and returns true or false immediately upon finding a definitive relation.
   * If no definitive relation is found, it logs a warning listing the coplanar edges and returns null.
   * @param {Object} faceA - The first face object containing pts, pts3D, and normal3D.
   * @param {Object} faceB - The second face object containing pts, pts3D, and normal3D.
   * @returns {boolean|null} true if Face A is in front, false if Face A is behind, or null if inconclusive.
   */
  checkEdgeNormalRelation(faceA, faceB) {
    const coplanarEdgesA = [];
    const coplanarEdgesB = [];

    // ==========================================
    // Step 1: Evaluate projecting edges of Face A against Face B's plane
    // ==========================================
    for (let i = 0; i < faceA.pts.length; i++) {
      const p1_2d = faceA.pts[i];
      const p2_2d = faceA.pts[(i + 1) % faceA.pts.length];

      if (this.edgeProjects(p1_2d, p2_2d, faceB)) {
        const p1_3d = faceA.pts3D[i];
        const p2_3d = faceA.pts3D[(i + 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 (Math.abs(dot) > 1e-4) {
          return dot > 0; // true if front, false if behind
        } else {
          coplanarEdgesA.push(i);
        }
      }
    }

    // ==========================================
    // Step 2: Evaluate projecting edges of Face B against Face A's plane
    // ==========================================
    for (let i = 0; i < faceB.pts.length; i++) {
      const p1_2d = faceB.pts[i];
      const p2_2d = faceB.pts[(i + 1) % faceB.pts.length];

      if (this.edgeProjects(p1_2d, p2_2d, faceA)) {
        const p1_3d = faceB.pts3D[i];
        const p2_3d = faceB.pts3D[(i + 1) % faceB.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 = faceA.pts3D[0];

        const dot = (midX - q.x) * faceA.normal3D.x + (midY - q.y) * faceA.normal3D.y + (midZ - q.z) * faceA.normal3D.z;

        if (Math.abs(dot) > 1e-4) {
          return dot <= 0; // false if dot > 0 (A is behind), true if dot < 0 (A is front)
        } else {
          coplanarEdgesB.push(i);
        }
      }
    }

    // ==========================================
    // Step 3: Handle inconclusive cases
    // ==========================================
    if (coplanarEdgesA.length > 0) {
      console.warn(`Which edges of Face A are completely on Face B: Indices [${coplanarEdgesA.join(', ')}]`);
    }
    if (coplanarEdgesB.length > 0) {
      console.warn(`Which edges of Face B are completely on Face A: Indices [${coplanarEdgesB.join(', ')}]`);
    }
    if (coplanarEdgesA.length === 0 && coplanarEdgesB.length === 0) {
      console.warn('No overlapping projection edges found between Face A and Face B.');
    }

    return null;
  }

改進checkEdgeNormalRelation這個函式:
首先檢測A面有沒有端點與B面干涉,如果有則用這個點與B面的法向量判定前後。如果沒有,則檢測B面有沒有端點與A面干涉,如果有則用這個點與A面的法向量判定前後。如果兩個面都沒有點與另一個面干涉,再用之前的邊判定。

排除點在面上的情況,當兩個面的點都處於另一個面上,則像之前那樣警告並返回null。

所有的代碼和注釋都要使用英文。
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5653
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 179 次
被圈友点赞: 207 次
联系:

Re: 正在開發新遊戲:星•球

帖子 ejsoon »

gemini thinking:

代码: 全选

Here is the improved version of the `checkEdgeNormalRelation` function.

It introduces vertex-plane interference checks for both faces before falling back to the edge-projection logic. It assumes a helper method `pointProjects(pt2d, face)` exists within your class, matching the convention of your existing `edgeProjects` method.

```javascript
  /**
   * Evaluates whether Face A is positioned in front of Face B based on normal vectors.
   * It first checks vertex-plane interference, and falls back to edge-plane projection if needed.
   * If vertices or edges are entirely coplanar, it logs a warning and returns null.
   * @param {Object} faceA - The first face object containing pts, pts3D, and normal3D.
   * @param {Object} faceB - The second face object containing pts, pts3D, and normal3D.
   * @returns {boolean|null} true if Face A is in front, false if Face A is behind, or null if inconclusive.
   */
  checkEdgeNormalRelation(faceA, faceB) {
    const coplanarPointsA = [];
    const coplanarPointsB = [];
    const coplanarEdgesA = [];
    const coplanarEdgesB = [];

    // ==========================================
    // Step 1: Check Face A's vertices against Face B's plane
    // ==========================================
    for (let i = 0; i < faceA.pts.length; i++) {
      if (this.pointProjects && this.pointProjects(faceA.pts[i], faceB)) {
        const p3d = faceA.pts3D[i];
        const q = faceB.pts3D[0];

        const dot = (p3d.x - q.x) * faceB.normal3D.x + 
                    (p3d.y - q.y) * faceB.normal3D.y + 
                    (p3d.z - q.z) * faceB.normal3D.z;

        if (Math.abs(dot) > 1e-4) {
          return dot > 0; // true if Face A is in front, false if behind
        } else {
          coplanarPointsA.push(i);
        }
      }
    }

    // ==========================================
    // Step 2: Check Face B's vertices against Face A's plane
    // ==========================================
    for (let i = 0; i < faceB.pts.length; i++) {
      if (this.pointProjects && this.pointProjects(faceB.pts[i], faceA)) {
        const p3d = faceB.pts3D[i];
        const q = faceA.pts3D[0];

        const dot = (p3d.x - q.x) * faceA.normal3D.x + 
                    (p3d.y - q.y) * faceA.normal3D.y + 
                    (p3d.z - q.z) * faceA.normal3D.z;

        if (Math.abs(dot) > 1e-4) {
          return dot <= 0; // false if Face B is in front (A is behind), true if Face B is behind (A is in front)
        } else {
          coplanarPointsB.push(i);
        }
      }
    }

    // ==========================================
    // Step 3: Handle coplanar vertex cases (Early Exit)
    // ==========================================
    if (coplanarPointsA.length > 0 || coplanarPointsB.length > 0) {
      if (coplanarPointsA.length > 0) {
        console.warn(`Which vertices of Face A are completely on Face B: Indices [${coplanarPointsA.join(', ')}]`);
      }
      if (coplanarPointsB.length > 0) {
        console.warn(`Which vertices of Face B are completely on Face A: Indices [${coplanarPointsB.join(', ')}]`);
      }
      return null;
    }

    // ==========================================
    // Step 4: Fallback - Evaluate projecting edges of Face A against Face B's plane
    // ==========================================
    for (let i = 0; i < faceA.pts.length; i++) {
      const p1_2d = faceA.pts[i];
      const p2_2d = faceA.pts[(i + 1) % faceA.pts.length];

      if (this.edgeProjects(p1_2d, p2_2d, faceB)) {
        const p1_3d = faceA.pts3D[i];
        const p2_3d = faceA.pts3D[(i + 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 (Math.abs(dot) > 1e-4) {
          return dot > 0; 
        } else {
          coplanarEdgesA.push(i);
        }
      }
    }

    // ==========================================
    // Step 5: Fallback - Evaluate projecting edges of Face B against Face A's plane
    // ==========================================
    for (let i = 0; i < faceB.pts.length; i++) {
      const p1_2d = faceB.pts[i];
      const p2_2d = faceB.pts[(i + 1) % faceB.pts.length];

      if (this.edgeProjects(p1_2d, p2_2d, faceA)) {
        const p1_3d = faceB.pts3D[i];
        const p2_3d = faceB.pts3D[(i + 1) % faceB.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 = faceA.pts3D[0];

        const dot = (midX - q.x) * faceA.normal3D.x + (midY - q.y) * faceA.normal3D.y + (midZ - q.z) * faceA.normal3D.z;

        if (Math.abs(dot) > 1e-4) {
          return dot <= 0; 
        } else {
          coplanarEdgesB.push(i);
        }
      }
    }

    // ==========================================
    // Step 6: Handle inconclusive edge cases
    // ==========================================
    if (coplanarEdgesA.length > 0) {
      console.warn(`Which edges of Face A are completely on Face B: Indices [${coplanarEdgesA.join(', ')}]`);
    }
    if (coplanarEdgesB.length > 0) {
      console.warn(`Which edges of Face B are completely on Face A: Indices [${coplanarEdgesB.join(', ')}]`);
    }
    if (coplanarEdgesA.length === 0 && coplanarEdgesB.length === 0) {
      console.warn('No overlapping projection vertices or edges found between Face A and Face B.');
    }

    return null;
  }

```
https://ejsoon.vip/
弈趣極光:享受思維樂趣
回复

在线用户

正浏览此版面之用户: 没有注册用户 和 2 访客