Skip to content

Fix imp_spline_2d for non-convex polygons via signed-distance construction - #9

Merged
QL-UoHull merged 3 commits into
mainfrom
copilot/fix-non-convex-polygon-implementation
Jul 16, 2026
Merged

QL-UoHull merged 3 commits into
mainfrom
copilot/fix-non-convex-polygon-implementation

Conversation

Copilot AI commented Jul 16, 2026 •

Copy link
Copy Markdown
Contributor

imp_spline_2d evaluated non-convex polygons by multiplying per-edge half-plane fields ∏ H(L_i), where L_i is the signed distance to the infinite line through each edge. At any interior point where an edge's line extension passes through it, L_i ≤ 0 and the product collapses to 0. For non-convex polygons this means the field degenerates to the polygon kernel rather than filling the full shape.

Root fix — imp_spline_2d non-convex path

is_convex(P) selects the code path:

  • Convex → original ∏ H(L_i) (unchanged, backward-compatible)
  • Non-convex → signed-distance construction:
    1. d_min = min unsigned distance to nearest boundary segment (not line)
    2. sign = +1 inside (ray-casting), −1 outside
    3. B = H(signed_dist, δ, n)
# Before: L-shape interior near reflex vertex incorrectly gives 0
L = np.array([[0,0],[2,0],[2,1],[1,1],[1,2],[0,2]], dtype=float)
imp_spline_2d(0.5, 1.0, L, delta=0.05)  # was: 0.0  now: >0.9
imp_spline_2d(1.5, 1.5, L, delta=0.05)  # exterior notch: still 0.0 ✓

New polygon utilities

  • is_convex(P) — cross-product sign test; selects code path in imp_spline_2d
  • polygon_validate(P) — removes repeated closing vertex, deduplicates consecutive vertices, rejects degenerate edges and self-intersecting polygons, normalizes to CCW
  • triangulate_polygon(P) — ear-clipping triangulation for simple polygons (convex or concave); area of triangles sums exactly to polygon area

All three are exported from __init__.py.

Demo (Section 7)

Replaced the single convex-decomposition panel with a 4-panel figure: (a) direct corrected field via imp_spline_2d, (b) convex decomposition via convex_decomp_field, (c) absolute error map |direct − decomp| with shared decomposition edge annotated, (d) 3D surface. Error is non-zero at shared decomposition edges (piece product gives 0 at its own boundary; direct signed-distance gives the correct interior value) and near polygon corners (different decay profiles). Delta-evolution panel uses the direct path.

Known construction difference

The two approaches are not numerically identical across the full grid. They agree at deep interior points (both → 1) and exterior points (both → 0), but diverge:

  • At shared decomposition edges: piece product = 0; direct ≈ 1 (correct)
  • Near polygon corners: product of two small H values vs. H of the diagonal distance

This is documented in tests and the demo error panel rather than hidden.

Copilot AI changed the title [WIP] Fix non-convex polygon implementation in QL-UoHull Fix imp_spline_2d for non-convex polygons via signed-distance construction Jul 16, 2026
Copilot AI requested a review from QL-UoHull July 16, 2026 17:31
@QL-UoHull
QL-UoHull marked this pull request as ready for review July 16, 2026 19:25
Copilot AI review requested due to automatic review settings July 16, 2026 19:25
@QL-UoHull
QL-UoHull merged commit 4cfa046 into main Jul 16, 2026
@QL-UoHull
QL-UoHull deleted the copilot/fix-non-convex-polygon-implementation branch July 16, 2026 19:26

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes imp_spline_2d evaluation for non-convex simple polygons by switching from the convex-only per-edge half-plane product to a signed-distance construction when is_convex(P) is false. It also introduces new polygon utilities (is_convex, polygon_validate, triangulate_polygon), expands tests, and updates the demo to compare direct vs decomposition behavior.

Changes:

  • Add convexity detection and a signed-distance path in imp_spline_2d for non-convex polygons.
  • Introduce and export polygon utilities: is_convex, polygon_validate, triangulate_polygon.
  • Add comprehensive tests for the new utilities + non-convex behavior; update the demo visualization accordingly.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 7 comments.

File Description
python/implicit_spline/core.py Adds geometry helpers/utilities and modifies imp_spline_2d to support non-convex polygons via signed distance.
python/implicit_spline/__init__.py Exports the new polygon utility functions.
tests/test_implicit_spline.py Adds unit tests for is_convex, polygon_validate, triangulate_polygon, and non-convex imp_spline_2d behavior.
examples/demo.py Updates Section 7 demo to show direct non-convex evaluation vs convex decomposition, including error visualization.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +420 to +422
# 1. Remove repeated closing vertex
if len(P) >= 2 and np.allclose(P[0], P[-1], atol=1e-12):
P = P[:-1]
Comment on lines +424 to +429
# 2. Remove duplicate consecutive vertices
keep = np.ones(len(P), dtype=bool)
for i in range(1, len(P)):
if np.allclose(P[i], P[i - 1], atol=1e-12):
keep[i] = False
P = P[keep]
Comment on lines +473 to +475
Accepts both convex and concave (non-self-intersecting) polygons. The
vertices need not be in any particular order; the function normalizes to
CCW internally.
Comment on lines +545 to +568
max_iters = m * m + m
iters = 0
while len(indices) > 3:
iters += 1
if iters > max_iters:
break
n = len(indices)
found = False
for k in range(n):
if _is_ear(indices, k):
i = indices[(k - 1) % n]
j = indices[k]
ll = indices[(k + 1) % n]
triangles.append(P[[i, j, ll]])
indices.pop(k)
found = True
break
if not found:
break

if len(indices) >= 3:
triangles.append(P[indices[:3]])

return triangles
Comment on lines 662 to 681
x = np.asarray(x, dtype=float)
y = np.asarray(y, dtype=float)
P = np.asarray(P, dtype=float)

if P.ndim != 2 or P.shape[1] != 2:
raise ValueError(
f"imp_spline_2d: P must be shape (m, 2), got {P.shape}."
)
m = len(P)
if m < 3:
raise ValueError(
f"imp_spline_2d: polygon must have at least 3 vertices (got {m})."
)
if delta <= 0:
raise ValueError(f"imp_spline_2d: delta must be positive (got {delta}).")

# Ensure counter-clockwise orientation so that L_i > 0 inside polygon
# Ensure counter-clockwise orientation
if polygon_signed_area(P) < 0:
P = P[::-1]

Comment on lines +368 to +376
def test_rejects_degenerate_edge(self):
# A polygon with a zero-length edge (two identical consecutive vertices
# after deduplication) should raise.
# Value 1e-10 is clearly above the 1e-12 deduplication threshold,
# so the two vertices are treated as distinct and the zero-length edge
# is detected by the edge-length check.
P_zero = np.array([[0, 0], [0.0, 0.0 + 1e-10], [1, 0], [0.5, 1]], dtype=float)
with pytest.raises(ValueError):
polygon_validate(P_zero)
Comment on lines +562 to +568
# Deep interior points: both methods must give > 0.99
deep_mask = (Z_direct > 0.99) & (Z_union > 0.99)
if deep_mask.any():
deep_err = np.abs(Z_direct[deep_mask] - Z_union[deep_mask])
assert float(deep_err.max()) < 0.01, (
f"At deep interior points both methods must agree; max err={float(deep_err.max()):.4f}"
)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants