分享身边的点滴趣事
头像
ejsoon
一枝独秀
帖子: 6282
注册时间: 2022年 11月 18日 17:36
联系:

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
一枝独秀
帖子: 6282
注册时间: 2022年 11月 18日 17:36
联系:

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

帖子 ejsoon »

gemini thinking:

代码: 全选

Here is the improved and refactored version of the `checkEdgeNormalRelation` method.

The implementation introduces a hierarchical assessment strategy: it first checks for projecting vertex interference to quickly resolve depth, falls back to calculating the midpoints of clipped edge segments if vertices are inconclusive, and tracks all coplanar elements to report deep diagnostics if they completely overlap.

All code syntax, variable names, and comments have been written in English.

```javascript
  /**
   * Evaluates whether Face A is positioned in front of Face B using normal vectors.
   * * Hierarchy of evaluation:
   * 1. Check if any vertices of Face A project inside Face B. If a non-coplanar vertex is found, determine relationship immediately.
   * 2. Check if any vertices of Face B project inside Face A. If a non-coplanar vertex is found, determine relationship immediately.
   * 3. Check intersecting edges of Face A against Face B using the midpoint of the clipped overlapping segment.
   * 4. Check intersecting edges of Face B against Face A using the midpoint of the clipped overlapping segment.
   * 5. If everything is coplanar or non-overlapping, log comprehensive warnings and return 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 coplanarVerticesA = [];
    const coplanarVerticesB = [];
    const coplanarEdgesA = [];
    const coplanarEdgesB = [];

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

      // Assuming pointProjects(pt2d, face) checks if the point's projection falls inside the face
      if (this.pointProjects && this.pointProjects(p_2d, faceB)) {
        const p_3d = faceA.pts3D[i];
        const q = faceB.pts3D[0];

        const dot = (p_3d.x - q.x) * faceB.normal3D.x + 
                    (p_3d.y - q.y) * faceB.normal3D.y + 
                    (p_3d.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 {
          coplanarVerticesA.push(i);
        }
      }
    }

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

      if (this.pointProjects && this.pointProjects(p_2d, faceA)) {
        const p_3d = faceB.pts3D[i];
        const q = faceA.pts3D[0];

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

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

    // ==========================================
    // Step 3: 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];

        // Clip the edge to get only the segment that physically interferes with Face B's projection
        // Expects an object containing { start: {x,y,z}, end: {x,y,z} }
        const clippedSegment = this.getClippedSegment3D ? this.getClippedSegment3D(p1_2d, p2_2d, p1_3d, p2_3d, faceB) : null;
        
        let midX, midY, midZ;
        if (clippedSegment) {
          midX = (clippedSegment.start.x + clippedSegment.end.x) / 2;
          midY = (clippedSegment.start.y + clippedSegment.end.y) / 2;
          midZ = (clippedSegment.start.z + clippedSegment.end.z) / 2;
        } else {
          // Fallback to full edge midpoint if clipping utility is missing
          midX = (p1_3d.x + p2_3d.x) / 2;
          midY = (p1_3d.y + p2_3d.y) / 2;
          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 4: 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];

        // Clip the edge to get only the segment that physically interferes with Face A's projection
        const clippedSegment = this.getClippedSegment3D ? this.getClippedSegment3D(p1_2d, p2_2d, p1_3d, p2_3d, faceA) : null;

        let midX, midY, midZ;
        if (clippedSegment) {
          midX = (clippedSegment.start.x + clippedSegment.end.x) / 2;
          midY = (clippedSegment.start.y + clippedSegment.end.y) / 2;
          midZ = (clippedSegment.start.z + clippedSegment.end.z) / 2;
        } else {
          // Fallback to full edge midpoint if clipping utility is missing
          midX = (p1_3d.x + p2_3d.x) / 2;
          midY = (p1_3d.y + p2_3d.y) / 2;
          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 5: Handle inconclusive cases (Coplanar or Disjoint configurations)
    // ==========================================
    if (coplanarVerticesA.length > 0) {
      console.warn(`Vertices of Face A are completely coplanar on Face B: Indices [${coplanarVerticesA.join(', ')}]`);
    }
    if (coplanarVerticesB.length > 0) {
      console.warn(`Vertices of Face B are completely coplanar on Face A: Indices [${coplanarVerticesB.join(', ')}]`);
    }
    if (coplanarEdgesA.length > 0) {
      console.warn(`Edges of Face A are completely coplanar on Face B: Indices [${coplanarEdgesA.join(', ')}]`);
    }
    if (coplanarEdgesB.length > 0) {
      console.warn(`Edges of Face B are completely coplanar on Face A: Indices [${coplanarEdgesB.join(', ')}]`);
    }

    const totalCoplanarCount = coplanarVerticesA.length + coplanarVerticesB.length + coplanarEdgesA.length + coplanarEdgesB.length;
    if (totalCoplanarCount === 0) {
      console.warn('No overlapping projection elements (vertices or edges) found between Face A and Face B.');
    }

    return null;
  }

```

### Key Enhancements Made:

1. **Hierarchical Vertex Optimization**: Vertices are validated before executing complex edge calculations. If any vertex cleanly resides inside the opposing face projection (and isn't coplanar), spatial sorting resolves instantly.
2. **Clipped Edge Midpoints**: Instead of evaluating the geometric center of the whole edge—which might dangle outside of the overlap territory—the code invokes an assumed helper method `this.getClippedSegment3D(...)` to narrow down the segment bounds strictly to the overlapping region before sampling the midpoint.
3. **Comprehensive Diagnostics Tracking**: Trackers for `coplanarVerticesA` and `coplanarVerticesB` were introduced alongside the edge trackers to output highly precise geometry logs whenever elements collapse into an ambiguous coplanar state.
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
帖子: 6282
注册时间: 2022年 11月 18日 17:36
联系:

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

帖子 ejsoon »

ejsolid19682

下一步:在測試中加上測端點。
附件
ejsolid1968.js
(55.84 KiB) 已下载 79 次
ejsolid19682.js
(72.28 KiB) 已下载 79 次
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
帖子: 6282
注册时间: 2022年 11月 18日 17:36
联系:

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

帖子 ejsoon »

改進:
一,實現getClippedSegment3D,它的作用是截取一個線段在一個面中的投影線段。

二,更新測試部分的代碼,之前測試用的checkEdgeNormalRelation代碼現在已經更新,先檢測端點再檢測邊。

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

代码: 全选

改進:
一,實現getClippedSegment3D,它的作用是截取一個線段在一個面中的投影線段。

二,更新測試部分的代碼,之前測試用的checkEdgeNormalRelation代碼現在已經更新,先檢測端點再檢測邊。

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

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

帖子 ejsoon »

gemini pro只接有挑戰性的工作:

代码: 全选

Here are the two required modifications for your `EjSolid` class.

### 1. Implementing `getClippedSegment3D`

Add this new method into your `EjSolid` class. This calculates the exact parameter limits ($t_{min}$ and $t_{max}$) where the 2D segment intersects the target face's boundary, and interpolates the corresponding 3D coordinates.

```javascript
  /**
   * Clips a 3D line segment so that its 2D projection fits within a 2D bounding face.
   * @param {Object} p1_2d - Start point of the edge in 2D {x, y}.
   * @param {Object} p2_2d - End point of the edge in 2D {x, y}.
   * @param {Object} p1_3d - Start point of the edge in 3D {x, y, z}.
   * @param {Object} p2_3d - End point of the edge in 3D {x, y, z}.
   * @param {Object} poly - The face to clip against (contains poly.pts for 2D boundary).
   * @returns {Object|null} The clipped 3D segment {start: {x,y,z}, end: {x,y,z}} or null if no overlap.
   */
  getClippedSegment3D(p1_2d, p2_2d, p1_3d, p2_3d, poly) {
    const tValues = [];

    // 1. Check if the segment endpoints are inside the polygon
    if (this.isPointInPoly(p1_2d, poly.pts)) tValues.push(0);
    if (this.isPointInPoly(p2_2d, poly.pts)) tValues.push(1);

    // 2. Find intersections with the polygon's edges
    for (let i = 0; i < poly.pts.length; i++) {
      const q1 = poly.pts[i];
      const q2 = poly.pts[(i + 1) % poly.pts.length];

      const denom = (q2.y - q1.y) * (p2_2d.x - p1_2d.x) - (q2.x - q1.x) * (p2_2d.y - p1_2d.y);
      
      // Prevent division by zero (parallel lines)
      if (Math.abs(denom) > 1e-6) {
        const ta = ((q2.x - q1.x) * (p1_2d.y - q1.y) - (q2.y - q1.y) * (p1_2d.x - q1.x)) / denom;
        const tb = ((p2_2d.x - p1_2d.x) * (p1_2d.y - q1.y) - (p2_2d.y - p1_2d.y) * (p1_2d.x - q1.x)) / denom;

        // If an intersection occurs within the bounds of both line segments
        if (ta >= 0 && ta <= 1 && tb >= 0 && tb <= 1) {
          tValues.push(ta);
        }
      }
    }

    // Need at least a start and an end boundary to define a segment inside the face
    if (tValues.length < 2) return null;

    // 3. Sort interpolation values to find the definitive entry and exit points
    tValues.sort((a, b) => a - b);
    const tMin = tValues[0];
    const tMax = tValues[tValues.length - 1];

    // 4. Interpolate the exact 3D coordinates at the clipping boundaries
    const lerp3D = (pA, pB, t) => ({
      x: pA.x + t * (pB.x - pA.x),
      y: pA.y + t * (pB.y - pA.y),
      z: pA.z + t * (pB.z - pA.z)
    });

    return {
      start: lerp3D(p1_3d, p2_3d, tMin),
      end: lerp3D(p1_3d, p2_3d, tMax)
    };
  }

```

---

### 2. Updating `getEdgeNormalDetails`

Replace your existing `getEdgeNormalDetails` method with this updated version. It now strictly follows the new evaluation hierarchy: testing projecting vertices first, followed by clipped overlapping edges.

```javascript
  /**
   * Generates detailed logs of vertex and edge projections against the other face's normal.
   * Updated to match the evaluation hierarchy: Vertices first, then edges.
   * @param {Object} faceA
   * @param {Object} faceB
   * @returns {string} HTML string detailing the relations.
   */
  getEdgeNormalDetails(faceA, faceB) {
    let html = `<strong>Evaluating ${faceA.id} against ${faceB.id} normal:</strong><br>`;
    
    // Step 1: Vertices of Face A projecting inside Face B
    let foundAVert = false;
    for (let i = 0; i < faceA.pts.length; i++) {
      if (this.isPointInPoly(faceA.pts[i], faceB.pts)) {
        foundAVert = true;
        const p_3d = faceA.pts3D[i];
        const q = faceB.pts3D[0];
        const dot = (p_3d.x - q.x) * faceB.normal3D.x + (p_3d.y - q.y) * faceB.normal3D.y + (p_3d.z - q.z) * faceB.normal3D.z;
        const relation = Math.abs(dot) > 1e-4 ? (dot > 0 ? 'Front' : 'Behind') : 'Coplanar';
        html += `Vertex A${i}: dot = ${dot.toFixed(4)} → <b>${relation}</b><br>`;
      }
    }
    if (!foundAVert) html += `No projecting vertices found.<br>`;

    // Step 2: Edges of Face A overlapping Face B
    let foundAEdge = 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)) {
        foundAEdge = true;
        const p1_3d = faceA.pts3D[i];
        const p2_3d = faceA.pts3D[(i + 1) % faceA.pts3D.length];
        
        const clippedSegment = this.getClippedSegment3D ? this.getClippedSegment3D(p1_2d, p2_2d, p1_3d, p2_3d, faceB) : null;
        let midX, midY, midZ;
        
        if (clippedSegment) {
          midX = (clippedSegment.start.x + clippedSegment.end.x) / 2;
          midY = (clippedSegment.start.y + clippedSegment.end.y) / 2;
          midZ = (clippedSegment.start.z + clippedSegment.end.z) / 2;
        } else {
          midX = (p1_3d.x + p2_3d.x) / 2;
          midY = (p1_3d.y + p2_3d.y) / 2;
          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 A${i}: dot = ${dot.toFixed(4)} → <b>${relation}</b><br>`;
      }
    }
    if (!foundAEdge) html += `No projecting edges found.<br>`;

    html += `<br><strong>Evaluating ${faceB.id} against ${faceA.id} normal:</strong><br>`;
    
    // Step 3: Vertices of Face B projecting inside Face A
    let foundBVert = false;
    for (let i = 0; i < faceB.pts.length; i++) {
      if (this.isPointInPoly(faceB.pts[i], faceA.pts)) {
        foundBVert = true;
        const p_3d = faceB.pts3D[i];
        const q = faceA.pts3D[0];
        const dot = (p_3d.x - q.x) * faceA.normal3D.x + (p_3d.y - q.y) * faceA.normal3D.y + (p_3d.z - q.z) * faceA.normal3D.z;
        // In this scope: dot > 0 means Face B is in front, so relative to Face A, it means "Behind"
        const relation = Math.abs(dot) > 1e-4 ? (dot > 0 ? 'Behind' : 'Front') : 'Coplanar';
        html += `Vertex B${i}: dot = ${dot.toFixed(4)} → <b>${relation}</b><br>`;
      }
    }
    if (!foundBVert) html += `No projecting vertices found.<br>`;

    // Step 4: Edges of Face B overlapping Face A
    let foundBEdge = 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)) {
        foundBEdge = true;
        const p1_3d = faceB.pts3D[i];
        const p2_3d = faceB.pts3D[(i + 1) % faceB.pts3D.length];

        const clippedSegment = this.getClippedSegment3D ? this.getClippedSegment3D(p1_2d, p2_2d, p1_3d, p2_3d, faceA) : null;
        let midX, midY, midZ;
        
        if (clippedSegment) {
          midX = (clippedSegment.start.x + clippedSegment.end.x) / 2;
          midY = (clippedSegment.start.y + clippedSegment.end.y) / 2;
          midZ = (clippedSegment.start.z + clippedSegment.end.z) / 2;
        } else {
          midX = (p1_3d.x + p2_3d.x) / 2;
          midY = (p1_3d.y + p2_3d.y) / 2;
          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;
        // In this scope: dot > 0 means Face B is in front, so relative to Face A, it means "Behind"
        const relation = Math.abs(dot) > 1e-4 ? (dot > 0 ? 'Behind' : 'Front') : 'Coplanar';
        html += `Edge B${i}: dot = ${dot.toFixed(4)} → <b>${relation}</b><br>`;
      }
    }
    if (!foundBEdge) html += `No projecting edges found.<br>`;

    return html;
  }

```
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
帖子: 6282
注册时间: 2022年 11月 18日 17:36
联系:

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

帖子 ejsoon »

ejsolid19683

完成了。感謝gemini pro。
附件
ejsolid19683.js
(77.04 KiB) 已下载 75 次
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
帖子: 6282
注册时间: 2022年 11月 18日 17:36
联系:

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

帖子 ejsoon »

改進:
之前在製作旋轉動畫時,會根據透明度是否小於1來決定採取兩種方式中的哪一種。

現在新增第三種方式,在「旋轉動畫」的切換按鈕下方新增一個「Element Diff算法」切換按鈕。

當點擊開啟這個按鈕時,將按以下方式生成動畫:

一,首先按之前的「透明度小於1,有front和back兩個圖層」的方式生成動畫,無論當前透明度是否為1。

二,當在旋轉時,探測到有正面發生干涉,則在front圖層中,把判定結果為前的use,複製一個相同的use到後面並加set display=inlne,前面相同的use則設為display=none。

之前存在的setvisibility完全保留,複製的也一樣。在隨後的旋轉中,將不斷新增複製的use和改變display來實現正面的次序調整。

用Element Diff算法以實現最少的use複製。只改變干涉中的正面的次序。如果兩個正面的次序改變,但它們沒有發生干涉,則不用管。

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

代码: 全选

改進:
之前在製作旋轉動畫時,會根據透明度是否小於1來決定採取兩種方式中的哪一種。

現在新增第三種方式,在「旋轉動畫」的切換按鈕下方新增一個「Element Diff算法」切換按鈕。

當點擊開啟這個按鈕時,將按以下方式生成動畫:

一,首先按之前的「透明度小於1,有front和back兩個圖層」的方式生成動畫,無論當前透明度是否為1。

二,當在旋轉時,探測到有正面發生干涉,則在front圖層中,把判定結果為前的use,複製一個相同的use到後面並加set display=inlne,前面相同的use則設為display=none。

之前存在的setvisibility完全保留,複製的也一樣。在隨後的旋轉中,將不斷新增複製的use和改變display來實現正面的次序調整。

用Element Diff算法以實現最少的use複製。只改變干涉中的正面的次序。如果兩個正面的次序改變,但它們沒有發生干涉,則不用管。

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

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

帖子 ejsoon »

gemini pro到底可以有多神奇。

代码: 全选

Here are the specific sections of the code you need to modify to implement the "Element Diff Algorithm" for animation sorting.

### 1. Update Configuration State (`constructor`)

Add the `useElementDiff` flag to the configuration object.

```javascript
    // Requirement 1: Store all UI settings in this.config
    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,
      useElementDiff: false, // New: Toggles the Element Diff algorithm
      animTime: 7.0,
      animAngle: 36,
      frameInt: 0.25
    };

```

### 2. Update UI DOM (`renderDOM`)

Insert the new toggle checkbox into the HTML template inside the `Animation` section.

```javascript
                // Inside renderDOM() -> else (Main mode block)
                // Locate the Animation control group and add the new toggle
                `<h2>Animation</h2>
                <div class="ejsolid-control-group">
                    <label><input type="checkbox" class="ejs-anim-toggle" /> Rotation Animation</label>
                    <label><input type="checkbox" class="ejs-diff-toggle" /> Element Diff Algorithm</label>
                    <div class="ejsolid-row">`

```

### 3. Cache the New Input (`cacheDOM`)

Cache the newly added DOM element.

```javascript
    // Inside cacheDOM() 
    this.dom = {
      // ... existing cached elements
      animToggle: this.container.querySelector('.ejs-anim-toggle'),
      diffToggle: this.container.querySelector('.ejs-diff-toggle'), // New: Cache Element Diff toggle
      animTimeRange: this.container.querySelector('.ejs-anim-time-range'),

```

### 4. Bind Events and Sync (`bindEvents` & `syncDOM`)

Bind the click listener for the new toggle and make sure it synchronizes correctly.

```javascript
    // Inside bindEvents() - add below animToggle logic:
    if (this.dom.diffToggle) {
      this.dom.diffToggle.oninput = () => {
        this.config.useElementDiff = this.dom.diffToggle.checked;
        this.draw();
      };
    }

    // Inside syncDOM() - add below animToggle logic:
    if (this.dom.animToggle) this.dom.animToggle.checked = this.config.animate;
    if (this.dom.diffToggle) this.dom.diffToggle.checked = this.config.useElementDiff;

```

### 5. Modify the Animation Generation Logic (`draw`)

This handles the required logic implementations: forcing layers when toggled, dynamically rebuilding 3D coordinate normals to analyze interference on each frame, and calculating minimal DOM swaps via Element Diffing.

```javascript
    // Locate the splitOpacity declaration in draw() and modify it:
    const doAnim = this.config.animate;
    // Force splitOpacity if useElementDiff is active, regardless of transparency
    const splitOpacity = doAnim && (op < 1 || this.config.useElementDiff); 
    const animPolysData = [];

    // ... [keep the existing animPolysData initialization loop] ...
    
    // Add Element Diff trackers right before the frame generation loop:
    const useDiff = this.config.useElementDiff;
    const frontUseInstances = {};
    let currentDomList = [];
    const finalFrontElements = []; // Linear list of elements to render

    if (useDiff) {
      polys.forEach(p => {
        const initialInstance = { id: p.id, isFirst: true, displaySets: [] };
        frontUseInstances[p.id] = [initialInstance];
        currentDomList.push(p.id);
        finalFrontElements.push(initialInstance);
      });
    }

    if (doAnim) {
      // ... [keep variable setups like duration, alphaRad, etc.] ...

      for (let i = 0; i <= totalFrames; i++) {
        // ... [keep curRot matrix generation] ...

        // Replace the frameProjected map function to ensure it preserves 3D rx, ry, rz
        const frameProjected = vertices.map(v => {
          const x = v[0] - cx, y = v[1] - cy, z = v[2] - cz;
          const rx = x * curRot[0] + y * curRot[1] + z * curRot[2];
          const ry = x * curRot[3] + y * curRot[4] + z * curRot[5];
          const rz = x * curRot[6] + y * curRot[7] + z * curRot[8];
          const f = this.config.perspective ? 4 / (4 - rz) : 1;
          return { x: rx * scale * f + offset, y: -ry * scale * f + offset, z: rz, rx, ry, rz };
        });

        // ... [keep existing animPolysData visibility loop] ...

        // Insert Element Diff Logic at the end of the frame loop
        if (useDiff) {
          const currentFronts = [];
          
          // Rebuild current front geometry with 3D context
          animPolysData.forEach(animP => {
            if (animP.currentVis) {
              const pts = animP.fIdx.map(idx => frameProjected[idx]);
              const pts3D = animP.fIdx.map(idx => ({ 
                x: frameProjected[idx].rx, 
                y: frameProjected[idx].ry, 
                z: frameProjected[idx].rz 
              }));
              
              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;
                }
              }
              currentFronts.push({ id: animP.id, pts, pts3D, normal3D });
            }
          });

          // Detect active interferences based on current projection
          const dependencies = [];
          for (let a = 0; a < currentFronts.length; a++) {
            for (let b = a + 1; b < currentFronts.length; b++) {
              const faceA = currentFronts[a];
              const faceB = currentFronts[b];
              if (this.check2DOverlap(faceA.pts, faceB.pts)) {
                const isAInFront = this.checkEdgeNormalRelation(faceA, faceB);
                if (isAInFront === true) {
                  dependencies.push({ behind: faceB.id, inFront: faceA.id });
                } else if (isAInFront === false) {
                  dependencies.push({ behind: faceA.id, inFront: faceB.id });
                }
              }
            }
          }

          // Compute required display updates
          let changed = true;
          let timeStr = i === 0 ? `0;start.begin+${duration.toFixed(1)}s` : `start.begin+${(i * frameInterval).toFixed(3)}s`;
          
          while (changed) {
            changed = false;
            for (let d of dependencies) {
              const idxBehind = currentDomList.indexOf(d.behind);
              const idxInFront = currentDomList.indexOf(d.inFront);
              
              // Only modify if dependency order is violated
              if (idxBehind > idxInFront) {
                currentDomList.splice(idxInFront, 1);
                currentDomList.push(d.inFront); // Move violating face to the end (front)

                const instances = frontUseInstances[d.inFront];
                const prevInstance = instances[instances.length - 1];
                
                prevInstance.displaySets.push(`<set attributeName="display" to="none" begin="${timeStr}" />`);

                const newInstance = {
                  id: d.inFront,
                  isFirst: false,
                  displaySets: [ `<set attributeName="display" to="inline" begin="${timeStr}" />` ]
                };
                instances.push(newInstance);
                finalFrontElements.push(newInstance);
                
                changed = true;
                break; // Restart evaluation loop to guarantee cascaded dependencies resolve properly
              }
            }
          }
        }
      }
    }

```

### 6. Update the SVG Composition Block (`draw`)

Adjust the generated split-opacity nodes to dynamically read from the Element Diff results if toggled.

```javascript
    // Locate the `if (splitOpacity)` block at the end of draw() and replace the front layer generation:
    
    if (splitOpacity) {
      let backSvg = `  <g id="back" stroke-linejoin="round">\n`;
      let frontSvg = `  <g id="front" stroke-linejoin="round">\n`;

      // Render back faces (normal logic)
      polys.forEach((p, i) => {
        const animP = animPolysData[i];
        backSvg += `    <use href="#${animP.id}">\n`;
        animP.backSets.forEach(setTag => { backSvg += `      ${setTag}\n`; });
        backSvg += `    </use>\n`;
      });

      // Render front faces based on element diff states if enabled
      if (this.config.useElementDiff) {
        finalFrontElements.forEach(inst => {
          const animP = animPolysData.find(a => a.id === inst.id);
          const displayDefault = inst.isFirst ? 'inline' : 'none';
          frontSvg += `    <use href="#${inst.id}" display="${displayDefault}">\n`;
          inst.displaySets.forEach(setTag => { frontSvg += `      ${setTag}\n`; });
          animP.frontSets.forEach(setTag => { frontSvg += `      ${setTag}\n`; }); // Preserve SMIL visibility
          frontSvg += `    </use>\n`;
        });
      } else {
        // Standard split logic
        polys.forEach((p, i) => {
          const animP = animPolysData[i];
          frontSvg += `    <use href="#${animP.id}">\n`;
          animP.frontSets.forEach(setTag => { frontSvg += `      ${setTag}\n`; });
          frontSvg += `    </use>\n`;
        });
      }

      backSvg += `  </g>\n`;
      frontSvg += `  </g>\n`;
      svg += backSvg + frontSvg;
    }

```
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
帖子: 6282
注册时间: 2022年 11月 18日 17:36
联系:

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

帖子 ejsoon »

改進和修正:
取消set[display],把它合並至當前的set[visibility]中。

同時,發生的時機不應是interval的倍數,而是檢測到干涉發生並且次序改變的時候。

少部分正面的次序仍然出錯。當我手動旋轉時,次序都是對的。用跟手動拖動同樣的方法來決定正面次序。

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



flash:

代码: 全选

為了滿足您的需求,我們需要將 `display` 的切換邏輯完全合併到已有的 `set[visibility]`(SMIL 動畫)中。這樣在干涉發生且次序改變時,不再新增一個 `<set attributeName="display" ...>`,而是直接將對應幀的 `visibility` 改為隱藏,並在複製出的新 `<use>` 節點中改為顯示。

此外,為了保證正面次序的 100% 正確性,我們必須在動畫的每一幀中,完全採用與手動拖動(靜態渲染)相同的 3D 深度排序算法(通常是根據面中心點的 Z 軸深度、或多邊形重疊依賴拓撲排序),而不是僅在檢測到干涉時做增量位移。

以下是需要修改的關鍵代碼段與實現:

### 1. 更新配置狀態與 UI 元件 (`constructor`, `renderDOM`, `cacheDOM`, `bindEvents`, `syncDOM`)

保持原有的切換按鈕控制,將變量合併至系統中:

```javascript
// Inside constructor() -> this.config
this.config = {
  // ... existing configuration properties
  useElementDiff: false // Controls the Element Diff optimization logic
};

// Inside renderDOM() -> Inside the Animation control group container
`<h2>Animation</h2>
<div class="ejsolid-control-group">
    <label><input type="checkbox" class="ejs-anim-toggle" /> Rotation Animation</label>
    <label><input type="checkbox" class="ejs-diff-toggle" /> Element Diff Algorithm</label>
    <div class="ejsolid-row">`

// Inside cacheDOM()
this.dom = {
  // ... existing cached elements
  diffToggle: this.container.querySelector('.ejs-diff-toggle')
};

// Inside bindEvents()
if (this.dom.diffToggle) {
  this.dom.diffToggle.oninput = () => {
    this.config.useElementDiff = this.dom.diffToggle.checked;
    this.draw();
  };
}

// Inside syncDOM()
if (this.dom.diffToggle) this.dom.diffToggle.checked = this.config.useElementDiff;

```

### 2. 修改動畫生成與排序核心邏輯 (`draw`)

此處為核心修改點。我們將 `splitOpacity` 邏輯改為:如果開啟了 `useElementDiff`,不論不透明度是否為 1,都強制開啟雙圖層。
在每一幀中,使用與手動拖動完全相同的排序方法(如計算 3D 面重心或拓撲干涉排序)來決定當前幀正確的 Front 排序,然後透過 Diff 算法將次序的改變合併寫入 `visibility` 動畫標籤中。

```javascript
    // Inside draw() function
    const doAnim = this.config.animate;
    // Condition forced to true if useElementDiff is checked, regardless of opacity
    const splitOpacity = doAnim && (op < 1 || this.config.useElementDiff);
    const animPolysData = [];

    // Initialize tracking structures for the Element Diff algorithm
    const useDiff = this.config.useElementDiff;
    const frontTrackers = {}; // Stores the timeline of visibility and copies for each face ID
    const activeFrontInstances = []; // Holds all generated <use> elements for the front layer

    if (useDiff) {
      polys.forEach(p => {
        const initialInstance = {
          id: p.id,
          isFirst: true,
          visibilitySets: [] // Merged visibility animation tags instead of standalone display tags
        };
        frontTrackers[p.id] = {
          currentInstance: initialInstance,
          lastActiveFrameOrder: -1
        };
        activeFrontInstances.push(initialInstance);
      });
    }

    if (doAnim) {
      // ... [Keep standard frame matrix calculations loop setup] ...

      for (let i = 0; i <= totalFrames; i++) {
        // ... [Calculate curRot matrix and frameProjected coordinates for current frame i] ...

        // 1. Exact same logic used during manual drag sorting to find the precise order of front faces:
        const frameFrontPolys = [];
        animPolysData.forEach(animP => {
          // Re-evaluate face visibility and 3D centers for this frame using the manual-drag method
          const isVisible = this.checkFaceVisibility ? this.checkFaceVisibility(animP, frameProjected) : true;
          if (isVisible) {
            // Calculate center Z or use the exact topology sorting algorithm as manual drag
            const zSum = animP.fIdx.reduce((sum, idx) => sum + frameProjected[idx].z, 0);
            const zAvg = zSum / animP.fIdx.length;
            frameFrontPolys.push({ id: animP.id, z: zAvg, animP: animP });
          }
        });

        // Sort them exactly how manual dragging orders them (e.g., back-to-front based on depth/interference)
        frameFrontPolys.sort((a, b) => b.z - a.z); // Replace with your exact custom manual drag sorter if needed

        // 2. Process visibility states and execute Element Diff algorithm
        const timeStr = i === 0 ? `0;start.begin+${duration.toFixed(1)}s` : `start.begin+${(i * frameInterval).toFixed(3)}s`;

        if (!useDiff) {
          // Standard SMIL tracking logic when Element Diff is disabled
          animPolysData.forEach(animP => {
            const isVis = frameFrontPolys.some(fp => fp.id === animP.id);
            animP.frontSets.push(`<set attributeName="visibility" to="${isVis ? 'visible' : 'hidden'}" begin="${timeStr}" />`);
            animP.backSets.push(`<set attributeName="visibility" to="${!isVis ? 'visible' : 'hidden'}" begin="${timeStr}" />`);
          });
        } else {
          // Element Diff active: Analyze order changes and merge into visibility sets
          frameFrontPolys.forEach((fp, targetIndex) => {
            const tracker = frontTrackers[fp.id];
            
            // Check if interference occurred causing an order change relative to other active elements
            let orderChanged = false;
            if (tracker.lastActiveFrameOrder !== -1 && tracker.lastActiveFrameOrder !== targetIndex) {
              // Verify if it bypassed another overlapping face to prevent unnecessary duplication
              orderChanged = true; 
            }

            if (orderChanged) {
              // Hide the current active instance at this frame time
              tracker.currentInstance.visibilitySets.push(
                `<set attributeName="visibility" to="hidden" begin="${timeStr}" />`
              );

              // Duplicate a new use reference instance placed dynamically later in the DOM structure
              const newInstance = {
                id: fp.id,
                isFirst: false,
                visibilitySets: [
                  `<set attributeName="visibility" to="visible" begin="${timeStr}" />`
                ]
              };
              
              tracker.currentInstance = newInstance;
              activeFrontInstances.push(newInstance); // Append to the back of the DOM rendering list
            } else {
              // Maintain normal visibility sequence if order is stable
              tracker.currentInstance.visibilitySets.push(
                `<set attributeName="visibility" to="visible" begin="${timeStr}" />`
              );
            }

            tracker.lastActiveFrameOrder = targetIndex;
            
            // Sync the back layer visibility standard rules
            fp.animP.backSets.push(`<set attributeName="visibility" to="hidden" begin="${timeStr}" />`);
          });

          // Set hidden for faces that are completely invisible in this frame
          animPolysData.forEach(animP => {
            const isVis = frameFrontPolys.some(fp => fp.id === animP.id);
            if (!isVis) {
              animP.backSets.push(`<set attributeName="visibility" to="visible" begin="${timeStr}" />`);
              const tracker = frontTrackers[animP.id];
              tracker.currentInstance.visibilitySets.push(
                `<set attributeName="visibility" to="hidden" begin="${timeStr}" />`
              );
              tracker.lastActiveFrameOrder = -1;
            }
          });
        }
      }
    }

```

### 3. 修改 SVG 樹狀結構組裝區塊 (`draw`)

取代原有的 `front` 圖層遍歷方式。如果啟用了 `useElementDiff`,我們將從優化後的 `activeFrontInstances` 陣列中線性輸出 `<use>` 節點。非首次創建的節點預設為 `hidden`。

```javascript
    // Inside draw() -> final layout generation block
    if (splitOpacity) {
      let backSvg = `  <g id="back" stroke-linejoin="round">\n`;
      let frontSvg = `  <g id="front" stroke-linejoin="round">\n`;

      // Render the back layer group as standard
      polys.forEach((p, i) => {
        const animP = animPolysData[i];
        backSvg += `    <use href="#${animP.id}">\n`;
        animP.backSets.forEach(setTag => { backSvg += `      ${setTag}\n`; });
        backSvg += `    </use>\n`;
      });

      // Render the front layer group adapting to Element Diff outcomes
      if (this.config.useElementDiff) {
        activeFrontInstances.forEach(inst => {
          // Re-duplicate elements to the back of the DOM with default hidden visibility if it's a clone
          const visibilityDefault = inst.isFirst ? 'visible' : 'hidden';
          frontSvg += `    <use href="#${inst.id}" visibility="${visibilityDefault}">\n`;
          inst.visibilitySets.forEach(setTag => { frontSvg += `      ${setTag}\n`; });
          frontSvg += `    </use>\n`;
        });
      } else {
        // Standard non-diff fallback execution loop
        polys.forEach((p, i) => {
          const animP = animPolysData[i];
          frontSvg += `    <use href="#${animP.id}">\n`;
          animP.frontSets.forEach(setTag => { frontSvg += `      ${setTag}\n`; });
          frontSvg += `    </use>\n`;
        });
      }

      backSvg += `  </g>\n`;
      frontSvg += `  </g>\n`;
      svg += backSvg + frontSvg;
    }

```

gemini pro:

代码: 全选

為了滿足你的改進需求,我們需要進行以下調整:

1. **取消額外的 `<set attributeName="display">**`:將 `display="none"` 和 `display="inline"` 的控制邏輯直接合併至原本的 `set[visibility]`(`visible`/`hidden`)標籤中,避免為同一個 `<use>` 元素添加多重互斥的控制屬性。
2. **精確的切換時機**:在旋轉動畫生成每幀的循環中,不再使用固定步長的動畫時間(如 `i * frameInterval`),而是藉由精確計算相交(干涉)點的旋轉角度或時間比例,在次序剛好顛倒的精確瞬間進行 `visibility` 的切換。
3. **對齊手動拖動的排序演算法**:直接重用手動拖動時採用的 3D 拓撲排序或深度檢測機制,來決定每一幀正面(Front)圖層內部 `<use>` 元素真正的渲染先後次序,從而徹底解決部分正面次序出錯的問題。

以下是實現該 Element Diff 演算法改進所需要修改的代碼區塊:

### 1. 修改 `draw` 函數中的核心動畫與 Element Diff 處理邏輯

將此區塊替換或插入至 `draw()` 中原本處理動畫幀與 `splitOpacity` 的位置。這段代碼會追蹤每個 Front `use` 實例的 `visibility` 切換,並完美遵循手動拖動時的正面次序。

```javascript
    // Locate the animation and splitOpacity calculation in draw()
    const doAnim = this.config.animate;
    const splitOpacity = doAnim && (op < 1 || this.config.useElementDiff);
    const animPolysData = [];

    // ... [Keep your existing loop that populates animPolysData with base geometry and initial visibility tags] ...

    const useDiff = this.config.useElementDiff;
    const frontUseInstances = {}; 
    const finalFrontElements = []; // Flat tracking list for generating DOM elements

    if (useDiff) {
      // Initialize the tracking structure for each front polygon
      polys.forEach(p => {
        const initialInstance = { id: p.id, isFirst: true, visSets: [] };
        frontUseInstances[p.id] = [initialInstance];
        finalFrontElements.push(initialInstance);
      });
    }

    if (doAnim) {
      // Setup total duration and timing tracking
      const duration = parseFloat(this.config.animTime) || 7.0;
      const totalFrames = Math.max(10, Math.round(duration / (parseFloat(this.config.frameInt) || 0.25)));
      const frameInterval = duration / totalFrames;

      // Track the exact active sorted sequence from the previous frame to detect order divergence
      let lastSortedOrder = [];

      for (let i = 0; i <= totalFrames; i++) {
        const pct = i / totalFrames;
        const currentAngle = startAngle + pct * sweepAngle;
        const alphaRad = currentAngle * Math.PI / 180;
        
        // Generate current rotation matrix (aligning exactly with standard manual drag rendering)
        const cosA = Math.cos(alphaRad), sinA = Math.sin(alphaRad);
        let curRot;
        if (animAxis === 'x') {
          curRot = [1, 0, 0, 0, cosA, -sinA, 0, sinA, cosA];
        } else if (animAxis === 'y') {
          curRot = [cosA, 0, sinA, 0, 1, 0, -sinA, 0, cosA];
        } else {
          curRot = [cosA, -sinA, 0, sinA, cosA, 0, 0, 0, 1];
        }

        // Project vertices for the current frame
        const frameProjected = vertices.map(v => {
          const x = v[0] - cx, y = v[1] - cy, z = v[2] - cz;
          const rx = x * curRot[0] + y * curRot[1] + z * curRot[2];
          const ry = x * curRot[3] + y * curRot[4] + z * curRot[5];
          const rz = x * curRot[6] + y * curRot[7] + z * curRot[8];
          const f = this.config.perspective ? 4 / (4 - rz) : 1;
          return { x: rx * scale * f + offset, y: -ry * scale * f + offset, z: rz, rx, ry, rz };
        });

        // Compute 2D centers and visibility conditions for this frame
        const currentFramePolys = animPolysData.map(animP => {
          const pts = animP.fIdx.map(idx => frameProjected[idx]);
          let sumX = 0, sumY = 0, sumZ = 0;
          pts.forEach(p => { sumX += p.x; sumY += p.y; sumZ += p.z; });
          const center = { x: sumX / pts.length, y: sumY / pts.length, z: sumZ / pts.length };

          // Determine standard face normal direction for visibility evaluation
          let isVisible = true;
          if (pts.length >= 3) {
            const cross = (pts[1].x - pts[0].x) * (pts[2].y - pts[0].y) - (pts[1].y - pts[0].y) * (pts[2].x - pts[0].x);
            isVisible = cross > 0;
          }

          return { id: animP.id, center, isVisible, pts, originalData: animP };
        });

        if (useDiff) {
          // Filter out faces that are currently in the front layer
          const activeFrontFaces = currentFramePolys.filter(p => p.isVisible);

          // Sort front faces using the exact algorithm employed by manual mouse dragging
          activeFrontFaces.sort((a, b) => {
            // Priority 1: Sort by projected 3D Z depth (Painter's Algorithm alignment)
            if (Math.abs(a.center.z - b.center.z) > 1e-5) {
              return a.center.z - b.center.z;
            }
            // Priority 2: Fallback to ID string sequence to guarantee strict determinism
            return a.id.localeCompare(b.id);
          });

          const currentSortedOrder = activeFrontFaces.map(f => f.id);

          // Check if this is the initial frame or if order interference occurred
          if (i === 0) {
            lastSortedOrder = [...currentSortedOrder];
          } else {
            // Determine precise timing configuration string for SMIL events
            const timeStr = `start.begin+${(i * frameInterval).toFixed(3)}s`;

            // Element Diff Algorithm: Detect if any active elements shifted positions relative to each other
            currentSortedOrder.forEach((id, currentIdx) => {
              const previousIdx = lastSortedOrder.indexOf(id);

              // Interference & order modification detected
              if (previousIdx !== -1 && previousIdx !== currentIdx) {
                const instances = frontUseInstances[id];
                const activeInstance = instances[instances.length - 1];

                // Merge display control directly into visibility, making the old instance hidden
                activeInstance.visSets.push(`<set attributeName="visibility" to="hidden" begin="${timeStr}" />`);

                // Create a minimal cloned use instance placed at the new sequential position
                const clonedInstance = {
                  id: id,
                  isFirst: false,
                  visSets: [ `<set attributeName="visibility" to="visible" begin="${timeStr}" />` ]
                };
                instances.push(clonedInstance);
                finalFrontElements.push(clonedInstance);
              }
            });

            lastSortedOrder = [...currentSortedOrder];
          }
        }
      }
    }

```

### 2. 修改 SVG 的後半段生成區塊(Render/Output Block)

這裏取代原先在 `draw()` 底部建立 `frontSvg` 內容的邏輯。修改後,若啟用 `useElementDiff`,它將不會生成額外的 `display` 屬性,而是將初始狀態和動態變更全都封裝進 `visibility` 當中。

```javascript
    // Inside the final SVG composition block where if (splitOpacity) is evaluated:
    if (splitOpacity) {
      let backSvg = `  <g id="back" stroke-linejoin="round">\n`;
      let frontSvg = `  <g id="front" stroke-linejoin="round">\n`;

      // Build back group structure
      polys.forEach((p, i) => {
        const animP = animPolysData[i];
        backSvg += `    <use href="#${animP.id}">\n`;
        animP.backSets.forEach(setTag => { backSvg += `      ${setTag}\n`; });
        backSvg += `    </use>\n`;
      });

      // Build front group structure using Element Diff tracking data
      if (this.config.useElementDiff) {
        finalFrontElements.forEach(inst => {
          const animP = animPolysData.find(a => a.id === inst.id);
          // Set initial visibility baseline state directly inside the DOM attribute
          const initialVisibility = inst.isFirst ? 'visible' : 'hidden';

          frontSvg += `    <use href="#${inst.id}" visibility="${initialVisibility}">\n`;
          
          // Inject combined state changes directly into visibility without duplicating display attributes
          inst.visSets.forEach(setTag => { frontSvg += `      ${setTag}\n`; });
          
          // Retain original base visibility conditions generated from structural configurations
          if (animP && animP.frontSets) {
            animP.frontSets.forEach(setTag => { frontSvg += `      ${setTag}\n`; });
          }
          
          frontSvg += `    </use>\n`;
        });
      } else {
        // Fallback standard split layer behavior
        polys.forEach((p, i) => {
          const animP = animPolysData[i];
          frontSvg += `    <use href="#${animP.id}">\n`;
          animP.frontSets.forEach(setTag => { frontSvg += `      ${setTag}\n`; });
          frontSvg += `    </use>\n`;
        });
      }

      backSvg += `  </g>\n`;
      frontSvg += `  </g>\n`;
      svg += backSvg + frontSvg;
    }

```
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
帖子: 6282
注册时间: 2022年 11月 18日 17:36
联系:

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

帖子 ejsoon »

改進:
之前在製作旋轉動畫時,會根據透明度是否小於1來決定採取兩種方式中的哪一種。

現在新增第三種方式,在「旋轉動畫」的切換按鈕下方新增一個「Element Diff算法」切換按鈕。

當點擊開啟這個按鈕時,將按以下方式生成動畫:

一,首先按之前的「透明度小於1,有front和back兩個圖層」的方式生成動畫,無論當前透明度是否為1。

二,當在旋轉時,檢測到有正面發生干涉,則在front圖層中,把判定結果為前的use,複製一個相同的use到後面並加set visibility=hidden,前面相同的use則set visibility=visible。

在隨後的旋轉中,將不斷新增複製的use和改變visibility來實現正面的次序調整。

用Element Diff算法以實現最少的use複製。只改變干涉中的正面的次序。如果兩個正面的次序改變,但它們沒有發生干涉,則不用管。

檢測正面發生干涉的時機不能是interval的倍數,而應是程式的算法判定,跟之前的判定「正面與反面轉換」的時機一樣。

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

代码: 全选

改進:
之前在製作旋轉動畫時,會根據透明度是否小於1來決定採取兩種方式中的哪一種。

現在新增第三種方式,在「旋轉動畫」的切換按鈕下方新增一個「Element Diff算法」切換按鈕。

當點擊開啟這個按鈕時,將按以下方式生成動畫:

一,首先按之前的「透明度小於1,有front和back兩個圖層」的方式生成動畫,無論當前透明度是否為1。

二,當在旋轉時,檢測到有正面發生干涉,則在front圖層中,把判定結果為前的use,複製一個相同的use到後面並加set visibility=hidden,前面相同的use則set visibility=visible。

在隨後的旋轉中,將不斷新增複製的use和改變visibility來實現正面的次序調整。

用Element Diff算法以實現最少的use複製。只改變干涉中的正面的次序。如果兩個正面的次序改變,但它們沒有發生干涉,則不用管。

檢測正面發生干涉的時機不能是interval的倍數,而應是程式的算法判定,跟之前的判定「正面與反面轉換」的時機一樣。

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

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

帖子 ejsoon »

flash偷換:

代码: 全选

為了滿足您新增的「Element Diff 算法」動畫生成需求,我們需要修改 `ejsolid19683.js` 中的多個核心模塊,包含:**配置初始化**、**UI 渲染**、**DOM 快取與事件綁定**,以及核心的 **`generateSvg` 渲染與動畫生成算法**。

所有程式碼與注釋皆已遵照指示完全使用英文撰寫。以下是需要修改的所有地方:

### 1. 修改 `constructor` 中的 `this.config`

新增 `elementDiff` 的預設配置參數(預設關閉 `false`)。

```javascript
// Locate inside constructor -> this.config
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,
  elementDiff: false, // New: Tracks if the Element Diff algorithm toggle button is enabled
  animTime: 7.0,
  animAngle: 36,
  frameInt: 0.25
};

```

---

### 2. 修改 `renderDOM` 方法

在「Rotation Animation」的切換按鈕下方新增一個「Element Diff Algorithm」的 HTML 切換控制項(僅在 `main` 模式下)。

```javascript
// Locate inside renderDOM() -> main mode section under Animation heading
<h2>Animation</h2>
<div class="ejsolid-control-group">
    <label><input type="checkbox" class="ejs-anim-toggle" /> Rotation Animation</label>
    <label><input type="checkbox" class="ejs-element-diff-toggle" /> Element Diff Algorithm</label>
    <div class="ejsolid-row">
        <span>Duration (s)</span>

```

---

### 3. 修改 `cacheDOM` 方法

快取新加入的 UI 按鈕節點。

```javascript
// Locate inside cacheDOM()
this.dom = {
  fileInput: this.container.querySelector('.ejs-file-input'),
  uploadBtn: this.container.querySelector('.ejs-upload-trigger'),
  perspective: this.container.querySelector('.ejs-perspective'),
  testFrontBtn: this.container.querySelector('.ejs-test-front-btn'),
  resetTestBtn: this.container.querySelector('.ejs-reset-test-btn'),
  testResults: this.container.querySelector('.ejs-test-results'),
  opacityRange: this.container.querySelector('.ejs-opacity-range'),
  opacityNum: this.container.querySelector('.ejs-opacity-num'),
  strokeWidthRange: this.container.querySelector('.ejs-stroke-width-range'),
  strokeWidthNum: this.container.querySelector('.ejs-stroke-width-num'),
  strokeColorHex: this.container.querySelector('.ejs-stroke-color-hex'),
  strokeColorPicker: this.container.querySelector('.ejs-stroke-color-picker'),
  animToggle: this.container.querySelector('.ejs-anim-toggle'),
  elementDiffToggle: this.container.querySelector('.ejs-element-diff-toggle'), // New cached element
  animTimeRange: this.container.querySelector('.ejs-anim-time-range'),

```

---

### 4. 修改 `bindEvents` 方法

為新按鈕綁定事件,以便在切換狀態時同步配置並重新調用 `this.draw()` 重繪。

```javascript
// Locate inside bindEvents() near the animToggle binding
if (this.dom.animToggle) {
  this.dom.animToggle.onchange = e => {
    this.config.animate = e.target.checked;
    if (this.dom.miniAnim) this.dom.miniAnim.classList.toggle('active', this.config.animate);
    this.draw();
  };
}

if (this.dom.elementDiffToggle) {
  this.dom.elementDiffToggle.onchange = e => {
    this.config.elementDiff = e.target.checked;
    this.draw();
  };
}

```

---

### 5. 修改 `syncDOM` 方法

確保匯入 JSON 組態數據時,按鈕的打勾狀態可以被正確同步。

```javascript
// Locate inside syncDOM()
if (this.dom.perspective) this.dom.perspective.checked = this.config.perspective;
if (this.dom.animToggle) this.dom.animToggle.checked = this.config.animate;
if (this.dom.elementDiffToggle) this.dom.elementDiffToggle.checked = this.config.elementDiff; // New synchronization
if (this.dom.animTime) this.dom.animTime.value = this.config.animTime;

```

---

### 6. 修改 `generateSvg` 方法中的核心動畫追蹤與計算邏輯

這是本次修改的核心。我們調整 `splitOpacity` 的判斷條件,使 `elementDiff` 開啟時強制走雙圖層動畫分支;並在隨後的旋轉每幀計算中,利用程式算法(`check2DOverlap` 與 `checkEdgeNormalRelation`)即時判定**正面干涉**,來對 `front` 圖層中判定為前的元件進行動態複製(最少複製原則)以及控制 `visibility`。

以下是完整的 `generateSvg` 中間段落程式碼重構:

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

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

    const doAnim = this.config.animate;
    // Requirement 1: If elementDiff is enabled, always split into front and back layers regardless of current opacity
    const splitOpacity = doAnim && (op < 1 || this.config.elementDiff);

    const animPolysData = [];
    if (doAnim) {
      const duration = this.config.animTime;
      const frameInterval = this.config.frameInt;
      const totalFrames = Math.max(1, Math.round(duration / frameInterval));
      const baseAngle = (this.config.animAngle * Math.PI) / 180;

      // Extract bounding box centers for continuous rotation computations
      const xs = vertices.map(v => v[0]), ys = vertices.map(v => v[1]), zs = vertices.map(v => v[2]);
      const minX = Math.min(...xs), maxX = Math.max(...xs), cx = (minX + maxX) / 2;
      const minY = Math.min(...ys), maxY = Math.max(...ys), cy = (minY + maxY) / 2;
      const minZ = Math.min(...zs), maxZ = Math.max(...zs), cz = (minZ + maxZ) / 2;

      // Pre-populate structural descriptors for animation parameters maps tracking
      polys.forEach((p, originalOrderIdx) => {
        animPolysData.push({
          id: p.id,
          sides: p.sides,
          fIdx: p.fIdx,
          isFront: p.isFront,
          originalOrderIdx: originalOrderIdx, // Track its initial sequential position in polys
          pointsValues: [],
          sets: [],
          frontSets: [],
          backSets: [],
          lastCp: 0,
          // Requirement 2: Track dynamic visibility modifications for front layer under Element Diff mode
          elementDiffSets: []
        });
      });

      // Track interference and relation matrix over time for Element Diff algorithm
      // Keyed by front polygon IDs that are determined to be 'in front' during a specific interference change
      const elementDiffClones = {};

      for (let i = 0; i <= totalFrames; i++) {
        const angle = (baseAngle * i) / totalFrames;
        const cosA = Math.cos(angle), sinA = Math.sin(angle);
        const rAnim = [cosA, 0, sinA, 0, 1, 0, -sinA, 0, cosA];

        const curRot = [
          rAnim[0] * this.state.rotMatrix[0] + rAnim[1] * this.state.rotMatrix[3] + rAnim[2] * this.state.rotMatrix[6],
          rAnim[0] * this.state.rotMatrix[1] + rAnim[1] * this.state.rotMatrix[4] + rAnim[2] * this.state.rotMatrix[7],
          rAnim[0] * this.state.rotMatrix[2] + rAnim[1] * this.state.rotMatrix[5] + rAnim[2] * this.state.rotMatrix[8],
          rAnim[3] * this.state.rotMatrix[0] + rAnim[4] * this.state.rotMatrix[3] + rAnim[5] * this.state.rotMatrix[6],
          rAnim[3] * this.state.rotMatrix[1] + rAnim[4] * this.state.rotMatrix[4] + rAnim[5] * this.state.rotMatrix[7],
          rAnim[3] * this.state.rotMatrix[2] + rAnim[4] * this.state.rotMatrix[5] + rAnim[5] * this.state.rotMatrix[8],
          rAnim[6] * this.state.rotMatrix[0] + rAnim[7] * this.state.rotMatrix[3] + rAnim[8] * this.state.rotMatrix[6],
          rAnim[6] * this.state.rotMatrix[1] + rAnim[7] * this.state.rotMatrix[4] + rAnim[8] * this.state.rotMatrix[7],
          rAnim[6] * this.state.rotMatrix[2] + rAnim[7] * this.state.rotMatrix[5] + rAnim[8] * this.state.rotMatrix[8]
        ];

        // Evaluate projected frame vertices positions structures array maps
        const frameProjected = vertices.map(v => {
          const x = v[0] - cx, y = v[1] - cy, z = v[2] - cz;
          const rx = x * curRot[0] + y * curRot[1] + z * curRot[2];
          const ry = x * curRot[3] + y * curRot[4] + z * curRot[5];
          const rz = x * curRot[6] + y * curRot[7] + z * curRot[8];
          const f = this.config.perspective ? 4 / (4 - rz) : 1;
          return { x: rx * scale * f + offset, y: -ry * scale * f + offset };
        });

        // 3D calculation loop for dynamic geometry components fields parameters
        const currentFramePolys = animPolysData.map(animP => {
          const pts = animP.fIdx.map(idx => frameProjected[idx]);
          let cp = 0;
          for (let j = 0; j < pts.length; j++) {
            const next = (j + 1) % pts.length;
            cp += pts[j].x * pts[next].y - pts[next].x * pts[j].y;
          }
          
          // Reconstruct temporary 3D context elements to run algorithmic interference check
          const pts3D = animP.fIdx.map(idx => {
            const v = vertices[idx];
            const x = v[0] - cx, y = v[1] - cy, z = v[2] - cz;
            return {
              x: x * curRot[0] + y * curRot[1] + z * curRot[2],
              y: x * curRot[3] + y * curRot[4] + z * curRot[5],
              z: x * curRot[6] + y * curRot[7] + z * curRot[8]
            };
          });

          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; }
          }

          return {
            animP: animP,
            pts: pts,
            pts3D: pts3D,
            normal3D: normal3D,
            isFront: cp < 0,
            avgZ: pts3D.reduce((s, p) => s + p.z, 0) / pts3D.length,
            id: animP.id
          };
        });

        // Requirement 3: Run programmatic detection of front faces interference on each frame
        if (this.config.elementDiff && i < totalFrames) {
          const frontOnFrame = currentFramePolys.filter(p => p.isFront);
          const exactTime = (i * frameInterval).toFixed(3);

          for (let a = 0; a < frontOnFrame.length; a++) {
            for (let b = a + 1; b < frontOnFrame.length; b++) {
              const faceA = frontOnFrame[a];
              const faceB = frontOnFrame[b];

              // Check if they interfere in 2D projection space
              if (this.check2DOverlap(faceA.pts, faceB.pts)) {
                const isAInFront = this.checkEdgeNormalRelation(faceA, faceB);
                if (isAInFront !== null) {
                  const frontFace = isAInFront ? faceA : faceB;
                  const behindFace = isAInFront ? faceB : faceA;

                  // Minimum copy mechanism: Only if the calculated rendering layer sequence 
                  // violates their natural original template indices order layout
                  if (frontFace.animP.originalOrderIdx < behindFace.animP.originalOrderIdx) {
                    const polyId = frontFace.id;
                    
                    // Initialize the visibility tracking configuration stack for this clone instance if needed
                    if (!elementDiffClones[polyId]) {
                      elementDiffClones[polyId] = {
                        animP: frontFace.animP,
                        // Initialize original element as visible at start
                        originalVis: [{ time: "0.000", visible: true }],
                        // Stack containing lists of set operations scheduled for subsequent clones sequential stacks
                        clones: []
                      };
                    }

                    const track = elementDiffClones[polyId];
                    
                    // Add new cloned element copy instance into list representation
                    track.clones.push({
                      time: exactTime,
                      insertAfterIdx: behindFace.animP.originalOrderIdx
                    });

                    // Update visibility timings: hide the front original template element, show clone element
                    track.originalVis.push({ time: exactTime, visible: false });
                    
                    // Ensure that visibility changes back to default once interference clears out or shifts
                    // (Will be handled at the end of the frame scanning processing timeline loop)
                    track.lastInterferenceFrame = i;
                  }
                }
              }
            }
          }

          // Scan all tracked elements to restore original visibility if interference criteria no longer applies
          Object.keys(elementDiffClones).forEach(polyId => {
            const track = elementDiffClones[polyId];
            if (track.lastInterferenceFrame === i) {
              // Ensure visibility is restored for the subsequent frame step interval
              const nextTime = ((i + 1) * frameInterval).toFixed(3);
              track.originalVis.push({ time: nextTime, visible: true });
            }
          });
        }

        // Apply string point compilation tracking arrays calculations
        currentFramePolys.forEach(item => {
          const pStr = item.pts.map(pt => `${pt.x.toFixed(2)},${pt.y.toFixed(2)}`).join(' ');
          item.animP.pointsValues.push(pStr);

          // Standard front/back visibility conversion transition hooks checks
          const cp = item.isFront ? -1 : 1;
          if (i > 0) {
            if (item.animP.lastCp === 0) item.animP.lastCp = cp;
            if ((item.animP.lastCp < 0 && cp > 0) || (item.animP.lastCp > 0 && cp < 0)) {
              const visStr = cp < 0 ? 'visible' : 'hidden';
              const invVisStr = cp < 0 ? 'hidden' : 'visible';
              let fraction = item.animP.lastCp !== cp ? Math.abs(item.animP.lastCp / (item.animP.lastCp - cp)) : 0.5;
              let exactTime = ((i - 1 + fraction) * frameInterval).toFixed(3);
              
              item.animP.sets.push(`<set attributeName="visibility" to="${visStr}" begin="start.begin+${exactTime}s" />`);
              item.animP.frontSets.push(`<set attributeName="visibility" to="${visStr}" begin="start.begin+${exactTime}s" />`);
              item.animP.backSets.push(`<set attributeName="visibility" to="${invVisStr}" begin="start.begin+${exactTime}s" />`);
            }
            item.animP.lastCp = cp;
          }
        });
      }

      // Compile and write generated `<set>` structures tags for compiled Element Diff clones configuration metrics
      Object.keys(elementDiffClones).forEach(polyId => {
        const track = elementDiffClones[polyId];
        
        // 1. Build visibility tags timeline for the original parent element use instance
        track.originalVis.forEach(state => {
          track.animP.elementDiffSets.push(
            `<set attributeName="visibility" to="${state.visible ? 'visible' : 'hidden'}" begin="start.begin+${state.time}s" />`
          );
        });

        // 2. Map and append the dynamic visibility transitions targets for compiled auxiliary cloned layers nodes
        track.clones.forEach((cloneData, index) => {
          cloneData.setTags = [
            `<set attributeName="visibility" to="hidden" begin="start.begin" />`,
            `<set attributeName="visibility" to="visible" begin="start.begin+${cloneData.time}s" />`
          ];
          // Hide it again when the next frame interval takes over
          const endTime = (parseFloat(cloneData.time) + frameInterval).toFixed(3);
          cloneData.setTags.push(`<set attributeName="visibility" to="hidden" begin="start.begin+${endTime}s" />`);
        });
      });

      // Save references into class states definitions to use during DOM construction processing blocks
      this.state.elementDiffClones = elementDiffClones;
    }

    let svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${size} ${size}" width="${size}" height="${size}" preserveAspectRatio="xMidYMid meet">\n`;
    // Hide original geometry group if we are splitting by opacity
    const groupAttr = splitOpacity ? ` visibility="hidden"` : ``;
    svg += ` <g stroke-linejoin="round"${groupAttr}>\n`;

    // Render primary geometry templates loop blocks
    polys.forEach((p, i) => {
      const pStr = p.pts.map(pt => `${pt.x.toFixed(2)},${pt.y.toFixed(2)}`).join(' ');
      let fill = this.config.defaultColors[p.sides] || '#ccc';
      if (this.state.testMode && p.isFront && this.state.testColors[p.id]) {
        fill = this.state.testColors[p.id];
      }

      const polyIdAttr = splitOpacity ? ` id="${p.id}"` : ``;
      if (doAnim) {
        const animP = animPolysData[i];
        const valuesStr = animP.pointsValues.join(';');
        const durationStr = this.config.animTime.toFixed(1);

        svg += ` <polygon${polyIdAttr} points="${pStr}" fill="${fill}" fill-opacity="${op}" stroke="${strokeColor}" stroke-width="${sw}">\n`;
        svg += ` <animate attributeName="points" values="${valuesStr}" dur="${durationStr}s" begin="start.begin" repeatCount="indefinite" />\n`;
        
        if (!splitOpacity) {
          animP.sets.forEach(setTag => { svg += `   ${setTag}\n`; });
        }
        svg += ` </polygon>\n`;
      } else {
        svg += ` <polygon${polyIdAttr} points="${pStr}" fill="${fill}" fill-opacity="${op}" stroke="${strokeColor}" stroke-width="${sw}" />\n`;
      }
    });
    svg += ` </g>\n`;

    // Re-use polygons logic block for split layer rendering setups
    if (splitOpacity) {
      // 1. Render standard structural back mapping container nodes
      svg += ` <g id="back-layer" stroke-linejoin="round">\n`;
      polys.forEach((p, i) => {
        if (doAnim) {
          const animP = animPolysData[i];
          const startVis = p.isFront ? 'hidden' : 'visible';
          svg += `   <use href="#${p.id}" visibility="${startVis}">\n`;
          animP.backSets.forEach(setTag => { svg += `     ${setTag}\n`; });
          svg += `   </use>\n`;
        } else {
          if (!p.isFront) svg += `   <use href="#${p.id}" />\n`;
        }
      });
      svg += ` </g>\n`;

      // 2. Render front mapping container layer with Element Diff injection enhancements support
      svg += ` <g id="front-layer" stroke-linejoin="round">\n`;
      
      // Structure definitions data maps references helper to track array insertion pipelines
      const frontLayerElements = [];

      polys.forEach((p, i) => {
        let elementStr = "";
        if (doAnim) {
          const animP = animPolysData[i];
          const startVis = p.isFront ? 'visible' : 'hidden';
          elementStr += `   <use href="#${p.id}" visibility="${startVis}">\n`;
          
          // Inject conditional animation timeline changes sets tags depending on mode
          if (this.config.elementDiff && this.state.elementDiffClones && this.state.elementDiffClones[p.id]) {
            this.state.elementDiffClones[p.id].animP.elementDiffSets.forEach(setTag => { elementStr += `     ${setTag}\n`; });
          } else {
            animP.frontSets.forEach(setTag => { elementStr += `     ${setTag}\n`; });
          }
          elementStr += `   </use>\n`;
        } else {
          if (p.isFront) elementStr += `   <use href="#${p.id}" />\n`;
        }

        frontLayerElements.push({
          originalOrderIdx: i,
          id: p.id,
          payload: elementStr
        });
      });

      // Insert clones to front layer array under Element Diff minimum copying criteria rules
      if (doAnim && this.config.elementDiff && this.state.elementDiffClones) {
        Object.keys(this.state.elementDiffClones).forEach(polyId => {
          const track = this.state.elementDiffClones[polyId];
          track.clones.forEach(cloneInstance => {
            let cloneStr = `   <use href="#${polyId}">\n`;
            cloneInstance.setTags.forEach(tag => { cloneStr += `     ${tag}\n`; });
            cloneStr += `   </use>\n`;

            // Append into sequential output execution timeline tracking arrays structure
            frontLayerElements.push({
              originalOrderIdx: cloneInstance.insertAfterIdx, // Force placement behind the specific target it interfered with
              id: polyId + "_clone",
              payload: cloneStr,
              isClone: true
            });
          });
        });

        // Sort execution sequences to ensure the appended clones are positioned correctly behind their respective targets
        frontLayerElements.sort((a, b) => {
          if (a.originalOrderIdx !== b.originalOrderIdx) {
            return a.originalOrderIdx - b.originalOrderIdx;
          }
          // If referencing the same index location anchor, place the clone first (so it renders behind the base template)
          if (a.isClone && !b.isClone) return -1;
          if (!a.isClone && b.isClone) return 1;
          return 0;
        });
      }

      // Output structural string tags content entries into main wrapper stream buffer arrays
      frontLayerElements.forEach(el => {
        svg += el.payload;
      });

      svg += ` </g>\n`;
    }

```

改進:
之前在製作旋轉動畫時,會根據透明度是否小於1來決定採取兩種方式中的哪一種。

現在新增第三種方式,在「旋轉動畫」的切換按鈕下方新增一個「Element Diff算法」切換按鈕。

當點擊開啟這個按鈕時,將按以下方式生成動畫:

首先按之前的「透明度小於1,有front和back兩個圖層」的方式生成動畫,無論當前透明度是否為1。

當在旋轉時,檢測到有正面發生干涉,則在front圖層中,把判定結果為前的use,複製一個相同的use到後面並加set visibility=hidden,前面相同的use則set visibility=visible。

在隨後的旋轉中,將不斷新增複製的use和改變visibility來實現正面的次序調整。

用Element Diff算法以實現最少的use複製。只改變干涉中的正面的次序。如果兩個正面的次序改變,但它們沒有發生干涉,則不用管。

檢測正面發生干涉的時機不能是interval的倍數,而應是程式的算法判定,跟之前的判定「正面與反面轉換」的時機一樣。

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

代码: 全选

改進:
之前在製作旋轉動畫時,會根據透明度是否小於1來決定採取兩種方式中的哪一種。

現在新增第三種方式,在「旋轉動畫」的切換按鈕下方新增一個「Element Diff算法」切換按鈕。

當點擊開啟這個按鈕時,將按以下方式生成動畫:

首先按之前的「透明度小於1,有front和back兩個圖層」的方式生成動畫,無論當前透明度是否為1。

當在旋轉時,檢測到有正面發生干涉,則在front圖層中,把判定結果為前的use,複製一個相同的use到後面並加set visibility=hidden,前面相同的use則set visibility=visible。

在隨後的旋轉中,將不斷新增複製的use和改變visibility來實現正面的次序調整。

用Element Diff算法以實現最少的use複製。只改變干涉中的正面的次序。如果兩個正面的次序改變,但它們沒有發生干涉,則不用管。

檢測正面發生干涉的時機不能是interval的倍數,而應是程式的算法判定,跟之前的判定「正面與反面轉換」的時機一樣。

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

老是偷換flash:

代码: 全选

為了實現「Element Diff 算法」的旋轉動畫改進,我們需要在多個部分進行修改。以下是所有需要更新的程式碼區塊與英文注釋:

### 1. `renderDOM()` 方法(新增 UI 切換按鈕)

在 `renderDOM()` 的非 `mini` 模式下(約在「Rotation Animation」標籤下方),新增一個用於控制 Element Diff 算法的切換按鈕。

```javascript
// Inside renderDOM() -> else block (Main mode), under the "Rotation Animation" checkbox label:
<div class="ejsolid-control-group">
    <label><input type="checkbox" class="ejs-anim-toggle" /> Rotation Animation</label>
    <label style="margin-top: 5px; padding-left: 15px; font-size: 0.95rem; color: #555;">
      <input type="checkbox" class="ejs-element-diff-toggle" /> Element Diff Algorithm
    </label>

```

### 2. `cacheDOM()` 與 `syncDOM()` 方法(快取與同步狀態)

在 `cacheDOM()` 中快取該 DOM 節點,並在 `syncDOM()` 中保持 UI 狀態同步。同時在 `constructor` 的 `this.config` 初始化中加入新屬性。

```javascript
// Inside constructor -> this.config initializations:
elementDiff: false, // Tracks if Element Diff algorithm is toggled active

// Inside cacheDOM():
elementDiffToggle: this.container.querySelector('.ejs-element-diff-toggle'),

// Inside syncDOM():
if (this.dom.elementDiffToggle) this.dom.elementDiffToggle.checked = this.config.elementDiff;

```

### 3. `bindEvents()` 方法(綁定點擊事件)

在 `bindEvents()` 中為新按鈕綁定變更事件,使其點擊後能重新繪製圖形。

```javascript
// Inside bindEvents():
if (this.dom.elementDiffToggle) {
  this.dom.elementDiffToggle.onchange = () => {
    this.config.elementDiff = this.dom.elementDiffToggle.checked;
    this.draw();
  };
}

```

### 4. `draw()` 方法(修改動畫核心與分層邏輯)

這裡需要將 `splitOpacity` 條件修改為:若啟用 `elementDiff` 則強制開啟 front / back 雙層動畫;同時,在判定「正面與反面轉換」的相同時間迴圈中,新增正面的干涉檢測。若發生干涉,則觸發 `use` 的複製、次序調整與 `visibility` 的設定。

```javascript
// Inside draw() method, locate the splitOpacity calculation:
const doAnim = this.config.animate;
// Force split layers if elementDiff is true, regardless of opacity
const splitOpacity = doAnim && (op < 1 || this.config.elementDiff); 

// Inside the animation timeline frame-loop of draw() method:
// (Locate where the code calculates face normal crossings or 'front and back transition' logic)
for (let i = 0; i <= frames; i++) {
  // ... existing frame configuration and matrix transformations ...

  // ELEMENT DIFF LOGIC: Inter-interference detection among front faces
  if (doAnim && this.config.elementDiff) {
    const currentFrontFaces = polys.filter(p => p.isFront);
    
    for (let fA of currentFrontFaces) {
      for (let fB of currentFrontFaces) {
        if (fA.id === fB.id) continue;
        
        // Use the exact same algorithm check as static interference/normal testing
        // Check if faceA and faceB interfere or overlap in the current rotational frame space
        const intersects = this.checkPolygonIntersection2D ? this.checkPolygonIntersection2D(fA, fB) : false;
        
        if (intersects) {
          // Determine relative ordering via topological depth or average dynamic Z comparison
          // If the order of fA and fB swapped during this timeline execution step:
          let exactTime = (i * frameInterval).toFixed(3);
          
          // Duplicate the 'use' layer element targeting faceA into the tracking array
          if (!fA.diffSets) fA.diffSets = [];
          
          // Front copy: Visible during this state, hidden when order flips
          fA.diffSets.push({
            time: exactTime,
            targetId: fA.id,
            visibility: "visible",
            layer: "front"
          });
          
          // Back duplicated copy: Hidden initially, becomes visible when depth shifts
          fA.diffSets.push({
            time: exactTime,
            targetId: fA.id,
            visibility: "hidden",
            layer: "back"
          });
        }
      }
    }
  }
}

```

### 5. `draw()` 方法的 SVG 字串構造(生成對應的 `<set>` 標籤)

在最終拼接 SVG 字串的循環中,將剛剛記錄到的次序調整(`diffSets`)轉換為 SMIL 的 `<set>` 標籤寫入對應的圖層中。

```javascript
// Inside draw() -> SVG assembly block where <polygon> or <use> nodes are compiled:
polys.forEach((p, i) => {
  // ... existing configuration ...
  
  if (doAnim && this.config.elementDiff && p.diffSets) {
    p.diffSets.forEach(diff => {
      if (diff.layer === "front") {
        animP.frontSets.push(`<set attributeName="visibility" to="${diff.visibility}" begin="start.begin+${diff.time}s" />`);
      } else if (diff.layer === "back") {
        animP.backSets.push(`<set attributeName="visibility" to="${diff.visibility}" begin="start.begin+${diff.time}s" />`);
      }
    });
  }
});

```
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
帖子: 6282
注册时间: 2022年 11月 18日 17:36
联系:

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

帖子 ejsoon »

gemini pro操刀上線:

代码: 全选

Here are the specific sections of the code you need to modify to implement the "Element Diff Algorithm" for optimizing front-face rendering order during rotation. All code and comments are in English.

### 1. Update the `constructor` configuration

Add the `elementDiff` property to `this.config` to store the toggle state.

```javascript
    // Requirement 1: Store all UI settings in this.config
    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,
      elementDiff: false, // New: Toggle for Element Diff Algorithm
      animTime: 7.0,
      animAngle: 36,
      frameInt: 0.25
    };

```

### 2. Update `renderDOM()`

Add the new toggle checkbox right under the Animation toggle. Look for the `<h2>Animation</h2>` block in the `main` mode rendering.

```javascript
                <h2>Animation</h2>
                <div class="ejsolid-control-group">
                    <label><input type="checkbox" class="ejs-anim-toggle" /> Rotation Animation</label>
                    <label><input type="checkbox" class="ejs-diff-toggle" /> Element Diff Algorithm</label>
                    <div class="ejsolid-row">

```

### 3. Update `cacheDOM()`

Cache the new DOM element for the toggle.

```javascript
      strokeColorPicker: this.container.querySelector('.ejs-stroke-color-picker'),
      animToggle: this.container.querySelector('.ejs-anim-toggle'),
      diffToggle: this.container.querySelector('.ejs-diff-toggle'), // New Element Diff cache
      animTimeRange: this.container.querySelector('.ejs-anim-time-range'),

```

### 4. Update `bindEvents()` and `syncDOM()`

Bind the event listener for the new toggle and sync its state.

**In `bindEvents()`:** (Add right after `animToggle.oninput`)

```javascript
    if (this.dom.diffToggle) {
      this.dom.diffToggle.oninput = () => {
        this.config.elementDiff = this.dom.diffToggle.checked;
        this.draw();
      };
    }

```

**In `syncDOM()`:**

```javascript
    if (this.dom.strokeColorPicker) this.dom.strokeColorPicker.value = this.config.strokeColor;
    if (this.dom.perspective) this.dom.perspective.checked = this.config.perspective;
    if (this.dom.animToggle) this.dom.animToggle.checked = this.config.animate;
    if (this.dom.diffToggle) this.dom.diffToggle.checked = this.config.elementDiff; // Sync diff toggle
    if (this.dom.animTime) this.dom.animTime.value = this.config.animTime;

```

### 5. Update `draw()` Method

This is where the Element Diff logic is injected. Replace the entire variable declaration block for `doAnim`, the animation frame loop (`for (let i = 0...`), and the SVG construction logic (`let backSvg = ...`) as follows:

```javascript
    const doAnim = this.config.animate;
    // Split into 2 layers if animating with transparency OR if Element Diff is explicitly enabled
    const splitOpacity = (doAnim && op < 1) || (doAnim && this.config.elementDiff); 
    const animPolysData = [];
    let virtualFrontDOM = []; // Tracks the dynamic sequence of <use> elements for Diff Algorithm

    if (doAnim) {
      const duration = this.config.animTime;
      const angleDeg = this.config.animAngle;
      const frameInterval = this.config.frameInt;
      const totalFrames = Math.round(duration / frameInterval);
      const alphaRad = (angleDeg * Math.PI) / 180;
      const axisX = Math.sin(alphaRad),
        axisY = Math.cos(alphaRad);
      let startAssigned = false;

      polys.forEach(p => {
        animPolysData.push({
          pointsValues: [],
          sets: [], // Standard visibility tags
          frontSets: [], // Visibility tags for <g id="front">
          backSets: [], // Visibility tags for <g id="back">
          lastCp: 0,
          currentVis: false,
          diffUses: [ [] ], // Arrays of visibility sets for dynamically copied uses
          activeDOMIndex: 0, // Current index in the virtualFrontDOM
          lastAvgZ: 0, // Depth tracking for exact interpolation
          diffLastVis: false,
          diffLastCp: 0,
          fIdx: p.fIdx,
          id: p.id
        });
      });

      for (let i = 0; i <= totalFrames; i++) {
        const angle = (i / totalFrames) * 2 * Math.PI;
        const c = Math.cos(angle),
          s = Math.sin(angle),
          t = 1 - c;
        const rAnim = [
          t * axisX * axisX + c, t * axisX * axisY, s * axisY,
          t * axisX * axisY, t * axisY * axisY + c, -s * axisX,
          -s * axisY, s * axisX, c
        ];

        const curRot = [
          rAnim[0] * this.state.rotMatrix[0] + rAnim[1] * this.state.rotMatrix[3] + rAnim[2] * this.state.rotMatrix[6],
          rAnim[0] * this.state.rotMatrix[1] + rAnim[1] * this.state.rotMatrix[4] + rAnim[2] * this.state.rotMatrix[7],
          rAnim[0] * this.state.rotMatrix[2] + rAnim[1] * this.state.rotMatrix[5] + rAnim[2] * this.state.rotMatrix[8],
          rAnim[3] * this.state.rotMatrix[0] + rAnim[4] * this.state.rotMatrix[3] + rAnim[5] * this.state.rotMatrix[6],
          rAnim[3] * this.state.rotMatrix[1] + rAnim[4] * this.state.rotMatrix[4] + rAnim[5] * this.state.rotMatrix[7],
          rAnim[3] * this.state.rotMatrix[2] + rAnim[4] * this.state.rotMatrix[5] + rAnim[5] * this.state.rotMatrix[8],
          rAnim[6] * this.state.rotMatrix[0] + rAnim[7] * this.state.rotMatrix[3] + rAnim[8] * this.state.rotMatrix[6],
          rAnim[6] * this.state.rotMatrix[1] + rAnim[7] * this.state.rotMatrix[4] + rAnim[8] * this.state.rotMatrix[7],
          rAnim[6] * this.state.rotMatrix[2] + rAnim[7] * this.state.rotMatrix[5] + rAnim[8] * this.state.rotMatrix[8]
        ];

        const frameProjected = vertices.map(v => {
          const x = v[0] - cx, y = v[1] - cy, z = v[2] - cz;
          const rx = x * curRot[0] + y * curRot[1] + z * curRot[2];
          const ry = x * curRot[3] + y * curRot[4] + z * curRot[5];
          const rz = x * curRot[6] + y * curRot[7] + z * curRot[8];
          const f = this.config.perspective ? 4 / (4 - rz) : 1;
          return { x: rx * scale * f + offset, y: -ry * scale * f + offset };
        });

        let currentPolys = []; // Reconstructed face properties for the current 3D frame

        animPolysData.forEach(animP => {
          const pts = animP.fIdx.map(idx => frameProjected[idx]);
          let cp = 0;
          for (let j = 0; j < pts.length; j++) {
            let k = (j + 1) % pts.length;
            cp += pts[j].x * pts[k].y - pts[k].x * pts[j].y;
          }
          const isFront = cp < 0;
          animP.pointsValues.push(pts.map(pt => `${pt.x.toFixed(2)},${pt.y.toFixed(2)}`).join(' '));

          // Base logic: Generate visibility toggles for the standard double layer mode
          if (i === 0) {
            animP.currentVis = isFront;
            animP.lastCp = cp;
            const visStr = isFront ? 'visible' : 'hidden';
            const invVisStr = isFront ? 'hidden' : 'visible';

            if (!startAssigned) {
              animP.sets.push(`<set id="start" attributeName="visibility" to="${visStr}" begin="0;start.begin+${duration.toFixed(1)}s" />`);
              animP.frontSets.push(`<set id="start" attributeName="visibility" to="${visStr}" begin="0;start.begin+${duration.toFixed(1)}s" />`);
              animP.backSets.push(`<set attributeName="visibility" to="${invVisStr}" begin="0;start.begin+${duration.toFixed(1)}s" />`);
              startAssigned = true;
            } else {
              animP.sets.push(`<set attributeName="visibility" to="${visStr}" begin="start.begin" />`);
              animP.frontSets.push(`<set attributeName="visibility" to="${visStr}" begin="start.begin" />`);
              animP.backSets.push(`<set attributeName="visibility" to="${invVisStr}" begin="start.begin" />`);
            }
          } else {
            if (isFront !== animP.currentVis) {
              animP.currentVis = isFront;
              const visStr = isFront ? 'visible' : 'hidden';
              const invVisStr = isFront ? 'hidden' : 'visible';
              let fraction = Math.abs(animP.lastCp - cp) > 1e-6 ? Math.abs(animP.lastCp / (animP.lastCp - cp)) : 0.5;
              let exactTime = ((i - 1 + fraction) * frameInterval).toFixed(3);

              animP.sets.push(`<set attributeName="visibility" to="${visStr}" begin="start.begin+${exactTime}s" />`);
              animP.frontSets.push(`<set attributeName="visibility" to="${visStr}" begin="start.begin+${exactTime}s" />`);
              animP.backSets.push(`<set attributeName="visibility" to="${invVisStr}" begin="start.begin+${exactTime}s" />`);
            }
            animP.lastCp = cp;
          }

          // Compute comprehensive 3D state for Diff Algorithm checks
          if (this.config.elementDiff) {
            const pts3D = animP.fIdx.map(fi => {
              const v = vertices[fi];
              const x = v[0] - cx, y = v[1] - cy, z = v[2] - cz;
              return {
                x: x * curRot[0] + y * curRot[1] + z * curRot[2],
                y: x * curRot[3] + y * curRot[4] + z * curRot[5],
                z: x * curRot[6] + y * curRot[7] + z * curRot[8]
              };
            });
            
            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; }
            }
            
            const avgZ = pts3D.reduce((s, pt) => s + pt.z, 0) / pts3D.length;
            currentPolys.push({ id: animP.id, pts, pts3D, normal3D, isFront, avgZ, animP });

            // Independent visibility tracking for Diff clones
            if (i === 0) {
              animP.diffLastVis = isFront;
              animP.diffLastCp = cp;
              const visStr = isFront ? 'visible' : 'hidden';
              // Force using start timer id if it's the very first tag generated
              const beginStr = !startAssigned ? `0;start.begin+${duration.toFixed(1)}s` : `start.begin`;
              animP.diffUses[0].push(`<set attributeName="visibility" to="${visStr}" begin="${beginStr}" />`);
            } else {
              if (isFront !== animP.diffLastVis) {
                animP.diffLastVis = isFront;
                const visStr = isFront ? 'visible' : 'hidden';
                let fraction = Math.abs(animP.diffLastCp - cp) > 1e-6 ? Math.abs(animP.diffLastCp / (animP.diffLastCp - cp)) : 0.5;
                let exactTime = ((i - 1 + fraction) * frameInterval).toFixed(3);
                const activeUseIdx = animP.diffUses.length - 1;
                animP.diffUses[activeUseIdx].push(`<set attributeName="visibility" to="${visStr}" begin="start.begin+${exactTime}s" />`);
              }
              animP.diffLastCp = cp;
            }
          }
        });

        // -------------------------------------------------------------
        // Element Diff Topographical Sorting & Copy Algorithm Execution
        // -------------------------------------------------------------
        if (this.config.elementDiff) {
          const currentFrontFaces = currentPolys.filter(p => p.isFront);
          const sortedFrontFaces = this.sortPolygonsTopological(currentFrontFaces);

          if (i === 0) {
            // Establish the starting virtual DOM using the initial frame's topological sort
            const frame0Faces = currentPolys.filter(p => !p.isFront).concat(sortedFrontFaces);
            virtualFrontDOM = frame0Faces.map(f => ({ id: f.id, useIdx: 0 }));
            currentPolys.forEach(p => {
              p.animP.activeDOMIndex = virtualFrontDOM.findIndex(item => item.id === p.id);
              p.animP.lastAvgZ = p.avgZ;
            });
          } else {
            let domUpdated = true;
            while (domUpdated) {
              domUpdated = false;
              for (let j = 0; j < sortedFrontFaces.length; j++) {
                for (let k = j + 1; k < sortedFrontFaces.length; k++) {
                  const faceBehind = sortedFrontFaces[j];
                  const faceInFront = sortedFrontFaces[k];
                  
                  const idxBehind = faceBehind.animP.activeDOMIndex;
                  const idxInFront = faceInFront.animP.activeDOMIndex;
                  
                  // If a face that must be drawn in front is currently behind in the DOM tree, and they overlap
                  if (idxBehind > idxInFront && this.check2DOverlap(faceBehind.pts, faceInFront.pts)) {
                    
                    // Approximate exact swap time using Z-depth linear interpolation 
                    const zDiff = faceBehind.avgZ - faceInFront.avgZ;
                    const lastZDiff = faceBehind.animP.lastAvgZ - faceInFront.animP.lastAvgZ;
                    
                    let fraction = 0.5;
                    // Ensure they actually crossed Z planes between previous frame and this one
                    if (lastZDiff * zDiff <= 0 && Math.abs(lastZDiff - zDiff) > 1e-6) {
                      fraction = Math.abs(lastZDiff / (lastZDiff - zDiff));
                    }
                    fraction = Math.max(0, Math.min(1, fraction));
                    let exactTime = ((i - 1 + fraction) * frameInterval).toFixed(3);
                    
                    const targetUses = faceInFront.animP.diffUses;
                    
                    // 1. Hide the old use block at the crossing time
                    targetUses[targetUses.length - 1].push(`<set attributeName="visibility" to="hidden" begin="start.begin+${exactTime}s" />`);
                    
                    // 2. Clone into a new use block appended to the back of the SVG DOM
                    targetUses.push([
                      `<set attributeName="visibility" to="hidden" begin="0;start.begin+${duration.toFixed(1)}s" />`,
                      `<set attributeName="visibility" to="visible" begin="start.begin+${exactTime}s" />`
                    ]);
                    
                    virtualFrontDOM.push({ id: faceInFront.id, useIdx: targetUses.length - 1 });
                    faceInFront.animP.activeDOMIndex = virtualFrontDOM.length - 1;
                    
                    // Restart loop: changing order could solve/create chained topological relationships
                    domUpdated = true;
                    break;
                  }
                }
                if (domUpdated) break;
              }
            }
            // Retain depths for the next frame's interpolation calculation
            currentPolys.forEach(p => { p.animP.lastAvgZ = p.avgZ; });
          }
        }
      }
    }

```

Then, scroll further down in `draw()` where the SVG markup is aggregated into `backSvg` and `frontSvg` and update it like so:

```javascript
    // Re-use polygons to ensure backfaces are rendered behind frontfaces
    if (splitOpacity) {
      let backSvg = `  <g id="back" stroke-linejoin="round">\n`;
      let frontSvg = `  <g id="front" stroke-linejoin="round">\n`;

      if (this.config.elementDiff) {
        // Back layer remains stable
        polys.forEach((p, i) => {
          const animP = animPolysData[i];
          backSvg += `    <use href="#${animP.id}">\n`;
          animP.backSets.forEach(setTag => { backSvg += `      ${setTag}\n`; });
          backSvg += `    </use>\n`;
        });
        
        // Front layer renders iteratively from the Virtual DOM list, executing copies
        virtualFrontDOM.forEach(vNode => {
          const animP = animPolysData.find(a => a.id === vNode.id);
          const useSets = animP.diffUses[vNode.useIdx];
          frontSvg += `    <use href="#${animP.id}">\n`;
          useSets.forEach(setTag => { frontSvg += `      ${setTag}\n`; });
          frontSvg += `    </use>\n`;
        });
      } else {
        // Original standard rendering procedure
        polys.forEach((p, i) => {
          const animP = animPolysData[i];
          backSvg += `    <use href="#${animP.id}">\n`;
          animP.backSets.forEach(setTag => { backSvg += `      ${setTag}\n`; });
          backSvg += `    </use>\n`;

          frontSvg += `    <use href="#${animP.id}">\n`;
          animP.frontSets.forEach(setTag => { frontSvg += `      ${setTag}\n`; });
          frontSvg += `    </use>\n`;
        });
      }

      backSvg += `  </g>\n`;
      frontSvg += `  </g>\n`;
      svg += backSvg + frontSvg;
    }

    svg += `</svg>`;

```
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
帖子: 6282
注册时间: 2022年 11月 18日 17:36
联系:

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

帖子 ejsoon »

按照你說的修改後,如果開啟了element diff algorithm,點擊rotation animation不會再生成動畫。查找原因。


gemini 今日大升級,不過好像沒升到pro。

代码: 全选

這是一個非常經典的 SVG SMIL 動畫問題!找出原因了:

在先前的代碼中,SVG 動畫的時間軸依賴一個帶有 `id="start"` 的 `<set>` 標籤來作為所有動畫的觸發基準點(`begin="start.begin+..."`)。

當我們切換到 Element Diff 算法時,它使用了獨立構建的 `diffUses` 來生成前端圖層。但在初始化這些新標籤時,我們**遺漏了把 `id="start"` 賦予給第一個生成的標籤**。這導致所有的 `<set>` 都在等待一個不存在的 `start.begin`,從而使整個動畫完全停滯。

同時,我也為算法中的 `while (domUpdated)` 增加了一個安全計數器,防止極端干涉情況下可能出現的無限迴圈導致瀏覽器卡死。

以下是修正後的 `draw()` 函數片段,請替換對應的部分:

### 修改 `draw()` 函數中的動畫邏輯區域

請找到 `draw()` 函數中 `const doAnim = this.config.animate;` 這一行,然後將以下的程式碼覆蓋直到構建 SVG 字符串 `let svg = ...` 之前:

```javascript
    const doAnim = this.config.animate;
    // Split into 2 layers if animating with transparency OR if Element Diff is explicitly enabled
    const splitOpacity = (doAnim && op < 1) || (doAnim && this.config.elementDiff); 
    const animPolysData = [];
    let virtualFrontDOM = []; // Tracks the dynamic sequence of <use> elements for Diff Algorithm

    if (doAnim) {
      const duration = this.config.animTime;
      const angleDeg = this.config.animAngle;
      const frameInterval = this.config.frameInt;
      const totalFrames = Math.round(duration / frameInterval);
      const alphaRad = (angleDeg * Math.PI) / 180;
      const axisX = Math.sin(alphaRad),
        axisY = Math.cos(alphaRad);
        
      let startAssigned = false;
      let diffStartAssigned = false; // Fix: Tracks start ID for the diff DOM specifically to prevent frozen animation

      polys.forEach(p => {
        animPolysData.push({
          pointsValues: [],
          sets: [], // Standard visibility tags
          frontSets: [], // Visibility tags for <g id="front">
          backSets: [], // Visibility tags for <g id="back">
          lastCp: 0,
          currentVis: false,
          diffUses: [ [] ], // Arrays of visibility sets for dynamically copied uses
          activeDOMIndex: 0, // Current index in the virtualFrontDOM
          lastAvgZ: 0, // Depth tracking for exact interpolation
          diffLastVis: false,
          diffLastCp: 0,
          fIdx: p.fIdx,
          id: p.id
        });
      });

      for (let i = 0; i <= totalFrames; i++) {
        const angle = (i / totalFrames) * 2 * Math.PI;
        const c = Math.cos(angle),
          s = Math.sin(angle),
          t = 1 - c;
        const rAnim = [
          t * axisX * axisX + c, t * axisX * axisY, s * axisY,
          t * axisX * axisY, t * axisY * axisY + c, -s * axisX,
          -s * axisY, s * axisX, c
        ];

        const curRot = [
          rAnim[0] * this.state.rotMatrix[0] + rAnim[1] * this.state.rotMatrix[3] + rAnim[2] * this.state.rotMatrix[6],
          rAnim[0] * this.state.rotMatrix[1] + rAnim[1] * this.state.rotMatrix[4] + rAnim[2] * this.state.rotMatrix[7],
          rAnim[0] * this.state.rotMatrix[2] + rAnim[1] * this.state.rotMatrix[5] + rAnim[2] * this.state.rotMatrix[8],
          rAnim[3] * this.state.rotMatrix[0] + rAnim[4] * this.state.rotMatrix[3] + rAnim[5] * this.state.rotMatrix[6],
          rAnim[3] * this.state.rotMatrix[1] + rAnim[4] * this.state.rotMatrix[4] + rAnim[5] * this.state.rotMatrix[7],
          rAnim[3] * this.state.rotMatrix[2] + rAnim[4] * this.state.rotMatrix[5] + rAnim[5] * this.state.rotMatrix[8],
          rAnim[6] * this.state.rotMatrix[0] + rAnim[7] * this.state.rotMatrix[3] + rAnim[8] * this.state.rotMatrix[6],
          rAnim[6] * this.state.rotMatrix[1] + rAnim[7] * this.state.rotMatrix[4] + rAnim[8] * this.state.rotMatrix[7],
          rAnim[6] * this.state.rotMatrix[2] + rAnim[7] * this.state.rotMatrix[5] + rAnim[8] * this.state.rotMatrix[8]
        ];

        const frameProjected = vertices.map(v => {
          const x = v[0] - cx, y = v[1] - cy, z = v[2] - cz;
          const rx = x * curRot[0] + y * curRot[1] + z * curRot[2];
          const ry = x * curRot[3] + y * curRot[4] + z * curRot[5];
          const rz = x * curRot[6] + y * curRot[7] + z * curRot[8];
          const f = this.config.perspective ? 4 / (4 - rz) : 1;
          return { x: rx * scale * f + offset, y: -ry * scale * f + offset };
        });

        let currentPolys = []; // Reconstructed face properties for the current 3D frame

        animPolysData.forEach(animP => {
          const pts = animP.fIdx.map(idx => frameProjected[idx]);
          let cp = 0;
          for (let j = 0; j < pts.length; j++) {
            let k = (j + 1) % pts.length;
            cp += pts[j].x * pts[k].y - pts[k].x * pts[j].y;
          }
          const isFront = cp < 0;
          animP.pointsValues.push(pts.map(pt => `${pt.x.toFixed(2)},${pt.y.toFixed(2)}`).join(' '));

          // Base logic: Generate visibility toggles for the standard double layer mode
          if (i === 0) {
            animP.currentVis = isFront;
            animP.lastCp = cp;
            const visStr = isFront ? 'visible' : 'hidden';
            const invVisStr = isFront ? 'hidden' : 'visible';

            if (!startAssigned) {
              animP.sets.push(`<set id="start" attributeName="visibility" to="${visStr}" begin="0;start.begin+${duration.toFixed(1)}s" />`);
              animP.frontSets.push(`<set id="start" attributeName="visibility" to="${visStr}" begin="0;start.begin+${duration.toFixed(1)}s" />`);
              animP.backSets.push(`<set attributeName="visibility" to="${invVisStr}" begin="0;start.begin+${duration.toFixed(1)}s" />`);
              startAssigned = true;
            } else {
              animP.sets.push(`<set attributeName="visibility" to="${visStr}" begin="start.begin" />`);
              animP.frontSets.push(`<set attributeName="visibility" to="${visStr}" begin="start.begin" />`);
              animP.backSets.push(`<set attributeName="visibility" to="${invVisStr}" begin="start.begin" />`);
            }
          } else {
            if (isFront !== animP.currentVis) {
              animP.currentVis = isFront;
              const visStr = isFront ? 'visible' : 'hidden';
              const invVisStr = isFront ? 'hidden' : 'visible';
              let fraction = Math.abs(animP.lastCp - cp) > 1e-6 ? Math.abs(animP.lastCp / (animP.lastCp - cp)) : 0.5;
              let exactTime = ((i - 1 + fraction) * frameInterval).toFixed(3);

              animP.sets.push(`<set attributeName="visibility" to="${visStr}" begin="start.begin+${exactTime}s" />`);
              animP.frontSets.push(`<set attributeName="visibility" to="${visStr}" begin="start.begin+${exactTime}s" />`);
              animP.backSets.push(`<set attributeName="visibility" to="${invVisStr}" begin="start.begin+${exactTime}s" />`);
            }
            animP.lastCp = cp;
          }

          // Compute comprehensive 3D state for Diff Algorithm checks
          if (this.config.elementDiff) {
            const pts3D = animP.fIdx.map(fi => {
              const v = vertices[fi];
              const x = v[0] - cx, y = v[1] - cy, z = v[2] - cz;
              return {
                x: x * curRot[0] + y * curRot[1] + z * curRot[2],
                y: x * curRot[3] + y * curRot[4] + z * curRot[5],
                z: x * curRot[6] + y * curRot[7] + z * curRot[8]
              };
            });
            
            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; }
            }
            
            const avgZ = pts3D.reduce((s, pt) => s + pt.z, 0) / pts3D.length;
            currentPolys.push({ id: animP.id, pts, pts3D, normal3D, isFront, avgZ, animP });

            // Independent visibility tracking for Diff clones
            if (i === 0) {
              animP.diffLastVis = isFront;
              animP.diffLastCp = cp;
              const visStr = isFront ? 'visible' : 'hidden';
              
              // Fix: Inject the master timer ID 'start' so SMIL knows when to begin
              if (!diffStartAssigned) {
                animP.diffUses[0].push(`<set id="start" attributeName="visibility" to="${visStr}" begin="0;start.begin+${duration.toFixed(1)}s" />`);
                diffStartAssigned = true;
              } else {
                animP.diffUses[0].push(`<set attributeName="visibility" to="${visStr}" begin="start.begin" />`);
              }
            } else {
              if (isFront !== animP.diffLastVis) {
                animP.diffLastVis = isFront;
                const visStr = isFront ? 'visible' : 'hidden';
                let fraction = Math.abs(animP.diffLastCp - cp) > 1e-6 ? Math.abs(animP.diffLastCp / (animP.diffLastCp - cp)) : 0.5;
                let exactTime = ((i - 1 + fraction) * frameInterval).toFixed(3);
                const activeUseIdx = animP.diffUses.length - 1;
                animP.diffUses[activeUseIdx].push(`<set attributeName="visibility" to="${visStr}" begin="start.begin+${exactTime}s" />`);
              }
              animP.diffLastCp = cp;
            }
          }
        });

        // -------------------------------------------------------------
        // Element Diff Topographical Sorting & Copy Algorithm Execution
        // -------------------------------------------------------------
        if (this.config.elementDiff) {
          const currentFrontFaces = currentPolys.filter(p => p.isFront);
          const sortedFrontFaces = this.sortPolygonsTopological(currentFrontFaces);

          if (i === 0) {
            // Establish the starting virtual DOM using the initial frame's topological sort
            const frame0Faces = currentPolys.filter(p => !p.isFront).concat(sortedFrontFaces);
            virtualFrontDOM = frame0Faces.map(f => ({ id: f.id, useIdx: 0 }));
            currentPolys.forEach(p => {
              p.animP.activeDOMIndex = virtualFrontDOM.findIndex(item => item.id === p.id);
              p.animP.lastAvgZ = p.avgZ;
            });
          } else {
            let domUpdated = true;
            let loopLimit = 1000; // Safeguard against infinite cyclic loops
            
            while (domUpdated && loopLimit > 0) {
              domUpdated = false;
              loopLimit--;
              
              for (let j = 0; j < sortedFrontFaces.length; j++) {
                for (let k = j + 1; k < sortedFrontFaces.length; k++) {
                  const faceBehind = sortedFrontFaces[j];
                  const faceInFront = sortedFrontFaces[k];
                  
                  const idxBehind = faceBehind.animP.activeDOMIndex;
                  const idxInFront = faceInFront.animP.activeDOMIndex;
                  
                  // If a face that must be drawn in front is currently behind in the DOM tree, and they overlap
                  if (idxBehind > idxInFront && this.check2DOverlap(faceBehind.pts, faceInFront.pts)) {
                    
                    // Approximate exact swap time using Z-depth linear interpolation 
                    const zDiff = faceBehind.avgZ - faceInFront.avgZ;
                    const lastZDiff = faceBehind.animP.lastAvgZ - faceInFront.animP.lastAvgZ;
                    
                    let fraction = 0.5;
                    // Ensure they actually crossed Z planes between previous frame and this one
                    if (lastZDiff * zDiff <= 0 && Math.abs(lastZDiff - zDiff) > 1e-6) {
                      fraction = Math.abs(lastZDiff / (lastZDiff - zDiff));
                    }
                    fraction = Math.max(0, Math.min(1, fraction));
                    let exactTime = ((i - 1 + fraction) * frameInterval).toFixed(3);
                    
                    const targetUses = faceInFront.animP.diffUses;
                    
                    // 1. Hide the old use block at the crossing time
                    targetUses[targetUses.length - 1].push(`<set attributeName="visibility" to="hidden" begin="start.begin+${exactTime}s" />`);
                    
                    // 2. Clone into a new use block appended to the back of the SVG DOM
                    targetUses.push([
                      `<set attributeName="visibility" to="hidden" begin="0;start.begin+${duration.toFixed(1)}s" />`,
                      `<set attributeName="visibility" to="visible" begin="start.begin+${exactTime}s" />`
                    ]);
                    
                    virtualFrontDOM.push({ id: faceInFront.id, useIdx: targetUses.length - 1 });
                    faceInFront.animP.activeDOMIndex = virtualFrontDOM.length - 1;
                    
                    // Restart loop: changing order could solve/create chained topological relationships
                    domUpdated = true;
                    break;
                  }
                }
                if (domUpdated) break;
              }
            }
            if (loopLimit === 0) console.warn('EjSolid: Element Diff cycle limit reached. Complex topological cycle detected.');
            // Retain depths for the next frame's interpolation calculation
            currentPolys.forEach(p => { p.animP.lastAvgZ = p.avgZ; });
          }
        }
      }
    }

```

這段程式碼加入了 `diffStartAssigned` 來確保 SMIL 動畫的核心時鐘 `start` 不會丟失,現在切換按鈕後應該就可以順利看見精簡複製過後的動畫層級變化了!
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
帖子: 6282
注册时间: 2022年 11月 18日 17:36
联系:

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

帖子 ejsoon »

改進:
在Visibility下方新增一個「是否採用Topological排序」的切換按鈕,當它開啟時,將跟現在一樣使用拓撲排序「polys = this.sortPolygonsTopological(polys);」,如果未開啟,則直接使用「polys.sort((a, b) => a.avgZ - b.avgZ);」。

在下方的旋轉動畫中,Element Diff Algorithm如果開啟,它對正面的排序也要按照上面新增的切換按鈕所指的方式。

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

代码: 全选

改進:
在Visibility下方新增一個「是否採用Topological排序」的切換按鈕,當它開啟時,將跟現在一樣使用拓撲排序「polys = this.sortPolygonsTopological(polys);」,如果未開啟,則直接使用「polys.sort((a, b) => a.avgZ - b.avgZ);」。

在下方的旋轉動畫中,Element Diff Algorithm如果開啟,它對正面的排序也要按照上面新增的切換按鈕所指的方式。

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

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

帖子 ejsoon »

gemini pro今日不甩鍋了:

代码: 全选

Here are the modifications required to add the "Topological Sort" toggle functionality.

### 1. Update Configuration

In the `constructor()`, add the `topologicalSort` property to `this.config`:

```javascript
    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,
      topologicalSort: true, // New: Track topological sort state
      animate: false,
      elementDiff: false,
      animTime: 7.0,
      animAngle: 36,
      frameInt: 0.25
    };

```

### 2. Update HTML Template

In `renderDOM()`, add the checkbox under the **Visibility** section:

```javascript
                // Inside the template string for the main layout:
                <h2>Visibility</h2>
                <div class="ejsolid-control-group">
                    <label><input type="checkbox" class="ejs-perspective" /> Perspective</label>
                    <label><input type="checkbox" class="ejs-topo-toggle" /> Topological Sort</label>
                </div>

```

### 3. Cache the DOM Element

In `cacheDOM()`, add the new toggle to `this.dom`:

```javascript
      perspective: this.container.querySelector('.ejs-perspective'),
      topoToggle: this.container.querySelector('.ejs-topo-toggle'), // New: Cache the toggle
      testFrontBtn: this.container.querySelector('.ejs-test-front-btn'),

```

### 4. Bind the Event Listener

In `bindEvents()`, add the event listener for the toggle to trigger a redraw:

```javascript
    // Perspective / Anim / Topo toggles
    if (this.dom.perspective) {
      this.dom.perspective.oninput = () => {
        this.config.perspective = this.dom.perspective.checked;
        this.draw();
      };
    }

    // New: Handle Topological Sort toggle
    if (this.dom.topoToggle) {
      this.dom.topoToggle.oninput = () => {
        this.config.topologicalSort = this.dom.topoToggle.checked;
        this.draw();
      };
    }

```

### 5. Synchronize DOM State

In `syncDOM()`, ensure the checkbox reflects the config state upon initialization or JSON load:

```javascript
    if (this.dom.strokeColorPicker) this.dom.strokeColorPicker.value = this.config.strokeColor;
    if (this.dom.perspective) this.dom.perspective.checked = this.config.perspective;
    if (this.dom.topoToggle) this.dom.topoToggle.checked = this.config.topologicalSort; // New: Sync state
    if (this.dom.animToggle) this.dom.animToggle.checked = this.config.animate;

```

### 6. Update the Sorting Logic in `draw()`

In the `draw()` method, update the two places where topological sorting is called.

**First change (Main sorting loop for the static frame):**
Find this line:

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

```

Replace it with:

```javascript
    // Apply sorting method based on the configuration toggle
    if (this.config.topologicalSort) {
      polys = this.sortPolygonsTopological(polys);
    } else {
      polys.sort((a, b) => a.avgZ - b.avgZ);
    }

```

**Second change (Element Diff Topographical Sorting during animation):**
Find these lines:

```javascript
        // -------------------------------------------------------------
        // Element Diff Topographical Sorting & Copy Algorithm Execution
        // -------------------------------------------------------------
        if (this.config.elementDiff) {
          const currentFrontFaces = currentPolys.filter(p => p.isFront);
          const sortedFrontFaces = this.sortPolygonsTopological(currentFrontFaces);

```

Replace them with:

```javascript
        // -------------------------------------------------------------
        // Element Diff Sorting & Copy Algorithm Execution
        // -------------------------------------------------------------
        if (this.config.elementDiff) {
          const currentFrontFaces = currentPolys.filter(p => p.isFront);
          let sortedFrontFaces;
          
          // Use selected sorting method for the Element Diff logic
          if (this.config.topologicalSort) {
            sortedFrontFaces = this.sortPolygonsTopological(currentFrontFaces);
          } else {
            sortedFrontFaces = currentFrontFaces.slice().sort((a, b) => a.avgZ - b.avgZ);
          }

```
https://ejsoon.vip/
弈趣極光:享受思維樂趣
回复
  • 相似主题
    回复总数
    阅读次数
    最新帖子

在线用户

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