ianalin123 commited on
Commit
f6709d8
·
1 Parent(s): 8da8915

feat(3d): render engine paper_state directly with strain heatmap

Browse files

Replace custom fold simulation (mesh + applyFoldToVertices rotation) with
direct rendering of vertices_coords/faces_vertices from the physics engine.
Add strain_per_vertex heat map blending ivory→red. Simplify component: remove
foldProgresses/mode/totalSteps, drop RAF animation, render on paper_state
change. Triangulate n-gon faces via fan from first vertex.

Files changed (1) hide show
  1. src/components/Fold3DCanvas.js +104 -228
src/components/Fold3DCanvas.js CHANGED
@@ -1,11 +1,9 @@
1
- import { useCallback, useEffect, useMemo, useRef } from 'react';
2
 
3
  const PAPER_RGB = [250, 250, 245];
4
  const LIGHT_DIR = normalize3([0.4, -0.45, 1.0]);
5
- const MAX_FOLD_RAD = Math.PI * 0.92;
6
- const SIDE_EPS = 1e-7;
7
- const MOUNTAIN_COLOR = 'rgba(245, 158, 11, 0.95)';
8
- const VALLEY_COLOR = 'rgba(56, 189, 248, 0.95)';
9
 
10
  function clamp(value, min, max) {
11
  return Math.min(Math.max(value, min), max);
@@ -41,99 +39,20 @@ function shadePaper(intensity) {
41
  return `rgb(${r}, ${g}, ${b})`;
42
  }
43
 
44
- function buildGridMesh(resolution = 18) {
45
- const vertices = [];
46
- for (let y = 0; y <= resolution; y += 1) {
47
- for (let x = 0; x <= resolution; x += 1) {
48
- vertices.push([x / resolution, y / resolution, 0]);
49
- }
50
- }
51
-
52
- const triangles = [];
53
- const stride = resolution + 1;
54
- for (let y = 0; y < resolution; y += 1) {
55
- for (let x = 0; x < resolution; x += 1) {
56
- const a = y * stride + x;
57
- const b = a + 1;
58
- const c = a + stride;
59
- const d = c + 1;
60
- triangles.push([a, b, d]);
61
- triangles.push([a, d, c]);
62
- }
63
- }
64
-
65
- return { vertices, triangles, resolution };
66
- }
67
-
68
- function rotateAroundAxis(point, axisPoint, axisDir, angleRad) {
69
- const px = point[0] - axisPoint[0];
70
- const py = point[1] - axisPoint[1];
71
- const pz = point[2] - axisPoint[2];
72
-
73
- const kx = axisDir[0];
74
- const ky = axisDir[1];
75
- const kz = axisDir[2];
76
-
77
- const cosA = Math.cos(angleRad);
78
- const sinA = Math.sin(angleRad);
79
-
80
- const crossX = ky * pz - kz * py;
81
- const crossY = kz * px - kx * pz;
82
- const crossZ = kx * py - ky * px;
83
-
84
- const dot = px * kx + py * ky + pz * kz;
85
- const oneMinus = 1.0 - cosA;
86
-
87
- return [
88
- axisPoint[0] + px * cosA + crossX * sinA + kx * dot * oneMinus,
89
- axisPoint[1] + py * cosA + crossY * sinA + ky * dot * oneMinus,
90
- axisPoint[2] + pz * cosA + crossZ * sinA + kz * dot * oneMinus,
91
- ];
92
- }
93
-
94
- function applyFoldToVertices(vertices, fold, progress) {
95
- if (!fold || progress <= 0) return;
96
- const [x1, y1] = fold.from;
97
- const [x2, y2] = fold.to;
98
- const dx = x2 - x1;
99
- const dy = y2 - y1;
100
- const len = Math.hypot(dx, dy);
101
- if (len < 1e-8) return;
102
-
103
- const sideValues = [];
104
- let posCount = 0;
105
- let negCount = 0;
106
-
107
- for (let i = 0; i < vertices.length; i += 1) {
108
- const v = vertices[i];
109
- const side = dx * (v[1] - y1) - dy * (v[0] - x1);
110
- sideValues.push(side);
111
- if (side > SIDE_EPS) posCount += 1;
112
- else if (side < -SIDE_EPS) negCount += 1;
113
- }
114
-
115
- let rotatePositive = posCount <= negCount;
116
- if (posCount === 0 && negCount > 0) rotatePositive = false;
117
- if (negCount === 0 && posCount > 0) rotatePositive = true;
118
- if (posCount === 0 && negCount === 0) return;
119
-
120
- const sign = fold.assignment === 'V' ? 1 : -1;
121
- const angle = sign * MAX_FOLD_RAD * progress;
122
- const axisPoint = [x1, y1, 0];
123
- const axisDir = [dx / len, dy / len, 0];
124
-
125
- for (let i = 0; i < vertices.length; i += 1) {
126
- const side = sideValues[i];
127
- const shouldRotate = rotatePositive ? side > SIDE_EPS : side < -SIDE_EPS;
128
- if (!shouldRotate) continue;
129
- vertices[i] = rotateAroundAxis(vertices[i], axisPoint, axisDir, angle);
130
- }
131
  }
132
 
133
  function projectVertex(vertex, dim) {
134
  let x = vertex[0] - 0.5;
135
  let y = vertex[1] - 0.5;
136
- let z = vertex[2];
137
 
138
  const pitch = 1.04;
139
  const yaw = -0.78;
@@ -158,162 +77,119 @@ function projectVertex(vertex, dim) {
158
  };
159
  }
160
 
161
- function foldProgresses(stepValue, foldCount, mode, totalSteps) {
162
- const values = new Array(foldCount).fill(0);
163
- if (foldCount === 0) return values;
164
-
165
- if (mode === 'final') {
166
- const startCollapse = Math.max(totalSteps - 1, 0);
167
- const collapse = clamp(stepValue - startCollapse, 0, 1);
168
- for (let i = 0; i < foldCount; i += 1) values[i] = collapse;
169
- return values;
 
 
170
  }
171
 
172
- for (let i = 0; i < foldCount; i += 1) {
173
- if (stepValue >= i + 1) values[i] = 1;
174
- else if (stepValue > i) values[i] = clamp(stepValue - i, 0, 1);
175
- }
176
- return values;
177
- }
178
-
179
- function stepEasing(t) {
180
- return t < 0.5 ? 4 * t * t * t : 1 - ((-2 * t + 2) ** 3) / 2;
181
- }
182
-
183
- export default function Fold3DCanvas({
184
- steps,
185
- currentStep,
186
- totalSteps,
187
- mode = 'progressive',
188
- dim = 280,
189
- }) {
190
- const canvasRef = useRef(null);
191
- const rafRef = useRef(null);
192
- const animatedStepRef = useRef(currentStep);
193
-
194
- const folds = useMemo(
195
- () => (steps || [])
196
- .map((s) => s.fold)
197
- .filter(Boolean)
198
- .map((fold) => ({
199
- from: [Number(fold.from_point[0]), Number(fold.from_point[1])],
200
- to: [Number(fold.to_point[0]), Number(fold.to_point[1])],
201
- assignment: fold.assignment === 'M' ? 'M' : 'V',
202
- })),
203
- [steps],
204
- );
205
-
206
- const mesh = useMemo(() => buildGridMesh(18), []);
207
-
208
- const draw = useCallback((stepValue) => {
209
- const canvas = canvasRef.current;
210
- if (!canvas) return;
211
- const ctx = canvas.getContext('2d');
212
- if (!ctx) return;
213
 
214
- ctx.clearRect(0, 0, dim, dim);
215
  ctx.fillStyle = '#121220';
216
  ctx.fillRect(0, 0, dim, dim);
 
 
217
 
218
- const vertices = mesh.vertices.map((v) => [v[0], v[1], v[2]]);
219
- const progress = foldProgresses(stepValue, folds.length, mode, totalSteps);
220
-
221
- for (let i = 0; i < folds.length; i += 1) {
222
- if (progress[i] <= 0) continue;
223
- applyFoldToVertices(vertices, folds[i], progress[i]);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
224
  }
 
 
225
 
226
- const projected = vertices.map((v) => projectVertex(v, dim));
 
227
 
228
- const tris = mesh.triangles.map((tri) => {
229
- const p0 = projected[tri[0]];
230
- const p1 = projected[tri[1]];
231
- const p2 = projected[tri[2]];
 
 
 
 
 
232
  const avgZ = (p0.z + p1.z + p2.z) / 3;
233
-
234
- const v0 = vertices[tri[0]];
235
- const v1 = vertices[tri[1]];
236
- const v2 = vertices[tri[2]];
237
  const normal = normalize3(cross3(sub3(v1, v0), sub3(v2, v0)));
238
  const intensity = dot3(normal, LIGHT_DIR);
 
 
 
 
 
 
239
 
240
- return {
241
- tri,
242
- avgZ,
243
- shade: shadePaper(intensity),
244
- };
245
- });
246
 
247
- tris.sort((a, b) => a.avgZ - b.avgZ);
 
 
 
248
 
249
- for (const triInfo of tris) {
250
- const [a, b, c] = triInfo.tri;
251
- const p0 = projected[a];
252
- const p1 = projected[b];
253
- const p2 = projected[c];
254
-
255
- ctx.beginPath();
256
- ctx.moveTo(p0.x, p0.y);
257
- ctx.lineTo(p1.x, p1.y);
258
- ctx.lineTo(p2.x, p2.y);
259
- ctx.closePath();
260
- ctx.fillStyle = triInfo.shade;
261
- ctx.fill();
262
- ctx.strokeStyle = 'rgba(42, 42, 58, 0.22)';
263
- ctx.lineWidth = 0.55;
264
- ctx.stroke();
265
- }
266
 
267
- const res = mesh.resolution;
268
- const stride = res + 1;
269
- const pointToIndex = (pt) => {
270
- const ix = clamp(Math.round(pt[0] * res), 0, res);
271
- const iy = clamp(Math.round(pt[1] * res), 0, res);
272
- return iy * stride + ix;
273
- };
274
 
275
- for (let i = 0; i < folds.length; i += 1) {
276
- if (progress[i] <= 0.02) continue;
277
- const fold = folds[i];
278
- const aIdx = pointToIndex(fold.from);
279
- const bIdx = pointToIndex(fold.to);
280
- const pa = projected[aIdx];
281
- const pb = projected[bIdx];
282
 
283
- ctx.beginPath();
284
- ctx.moveTo(pa.x, pa.y);
285
- ctx.lineTo(pb.x, pb.y);
286
- ctx.strokeStyle = fold.assignment === 'M' ? MOUNTAIN_COLOR : VALLEY_COLOR;
287
- ctx.globalAlpha = clamp(0.35 + 0.65 * progress[i], 0, 1);
288
- ctx.lineWidth = 2.15;
289
- ctx.stroke();
290
- ctx.globalAlpha = 1;
291
- }
292
- }, [dim, folds, mesh, mode, totalSteps]);
293
 
294
- useEffect(() => {
295
- draw(animatedStepRef.current);
296
- }, [draw]);
 
 
297
 
298
  useEffect(() => {
299
- cancelAnimationFrame(rafRef.current);
300
- const startValue = animatedStepRef.current;
301
- const endValue = currentStep;
302
- const durationMs = 420;
303
- const startAt = performance.now();
304
-
305
- const tick = (now) => {
306
- const t = clamp((now - startAt) / durationMs, 0, 1);
307
- const eased = stepEasing(t);
308
- const value = startValue + (endValue - startValue) * eased;
309
- animatedStepRef.current = value;
310
- draw(value);
311
- if (t < 1) rafRef.current = requestAnimationFrame(tick);
312
- };
313
-
314
- rafRef.current = requestAnimationFrame(tick);
315
- return () => cancelAnimationFrame(rafRef.current);
316
- }, [currentStep, draw]);
317
 
318
  return (
319
  <canvas
 
1
+ import { useCallback, useEffect, useRef } from 'react';
2
 
3
  const PAPER_RGB = [250, 250, 245];
4
  const LIGHT_DIR = normalize3([0.4, -0.45, 1.0]);
5
+ const MOUNTAIN_COLOR = 'rgba(245, 158, 11, 0.9)';
6
+ const VALLEY_COLOR = 'rgba(56, 189, 248, 0.9)';
 
 
7
 
8
  function clamp(value, min, max) {
9
  return Math.min(Math.max(value, min), max);
 
39
  return `rgb(${r}, ${g}, ${b})`;
40
  }
41
 
42
+ function strainColor(strain, intensity) {
43
+ const t = clamp(strain / 0.15, 0, 1);
44
+ const lit = clamp(0.3 + 0.7 * Math.abs(intensity), 0, 1);
45
+ // Blend from paper ivory to red-orange with lighting
46
+ const r = Math.round((250 + t * 5) * lit);
47
+ const g = Math.round((250 - t * 200) * lit);
48
+ const bv = Math.round((245 - t * 245) * lit);
49
+ return `rgb(${clamp(r,0,255)}, ${clamp(g,0,255)}, ${clamp(bv,0,255)})`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50
  }
51
 
52
  function projectVertex(vertex, dim) {
53
  let x = vertex[0] - 0.5;
54
  let y = vertex[1] - 0.5;
55
+ let z = vertex[2] || 0;
56
 
57
  const pitch = 1.04;
58
  const yaw = -0.78;
 
77
  };
78
  }
79
 
80
+ function drawPaperState(ctx, paperState, dim) {
81
+ ctx.clearRect(0, 0, dim, dim);
82
+ ctx.fillStyle = '#121220';
83
+ ctx.fillRect(0, 0, dim, dim);
84
+
85
+ if (!paperState) {
86
+ // Draw flat sheet for initial state
87
+ const flatVerts = [[0,0,0],[1,0,0],[1,1,0],[0,1,0]];
88
+ const flatFaces = [[0,1,2],[0,2,3]];
89
+ renderMesh(ctx, flatVerts, flatFaces, null, dim);
90
+ return;
91
  }
92
 
93
+ const { vertices_coords, faces_vertices, strain_per_vertex, edges_vertices, edges_assignment } = paperState;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
94
 
95
+ if (!vertices_coords || !faces_vertices) {
96
  ctx.fillStyle = '#121220';
97
  ctx.fillRect(0, 0, dim, dim);
98
+ return;
99
+ }
100
 
101
+ renderMesh(ctx, vertices_coords, faces_vertices, strain_per_vertex, dim);
102
+
103
+ // Draw fold creases on top
104
+ if (edges_vertices && edges_assignment) {
105
+ const projected = vertices_coords.map(v => projectVertex(v, dim));
106
+ for (let i = 0; i < edges_vertices.length; i++) {
107
+ const asgn = edges_assignment[i];
108
+ if (asgn !== 'M' && asgn !== 'V') continue;
109
+ const [ai, bi] = edges_vertices[i];
110
+ const pa = projected[ai];
111
+ const pb = projected[bi];
112
+ if (!pa || !pb) continue;
113
+ ctx.beginPath();
114
+ ctx.moveTo(pa.x, pa.y);
115
+ ctx.lineTo(pb.x, pb.y);
116
+ ctx.strokeStyle = asgn === 'M' ? MOUNTAIN_COLOR : VALLEY_COLOR;
117
+ ctx.lineWidth = 2.0;
118
+ ctx.globalAlpha = 0.85;
119
+ ctx.stroke();
120
+ ctx.globalAlpha = 1;
121
  }
122
+ }
123
+ }
124
 
125
+ function renderMesh(ctx, verts, faces, strain, dim) {
126
+ const projected = verts.map(v => projectVertex(v, dim));
127
 
128
+ const tris = [];
129
+ for (const face of faces) {
130
+ // Triangulate face (fan from first vertex)
131
+ for (let k = 1; k < face.length - 1; k++) {
132
+ const a = face[0], b = face[k], c = face[k + 1];
133
+ const p0 = projected[a];
134
+ const p1 = projected[b];
135
+ const p2 = projected[c];
136
+ if (!p0 || !p1 || !p2) continue;
137
  const avgZ = (p0.z + p1.z + p2.z) / 3;
138
+ const v0 = verts[a], v1 = verts[b], v2 = verts[c];
 
 
 
139
  const normal = normalize3(cross3(sub3(v1, v0), sub3(v2, v0)));
140
  const intensity = dot3(normal, LIGHT_DIR);
141
+ const avgStrain = strain
142
+ ? ((strain[a] || 0) + (strain[b] || 0) + (strain[c] || 0)) / 3
143
+ : 0;
144
+ tris.push({ a, b, c, avgZ, intensity, avgStrain });
145
+ }
146
+ }
147
 
148
+ tris.sort((x, y) => x.avgZ - y.avgZ);
 
 
 
 
 
149
 
150
+ for (const tri of tris) {
151
+ const p0 = projected[tri.a];
152
+ const p1 = projected[tri.b];
153
+ const p2 = projected[tri.c];
154
 
155
+ ctx.beginPath();
156
+ ctx.moveTo(p0.x, p0.y);
157
+ ctx.lineTo(p1.x, p1.y);
158
+ ctx.lineTo(p2.x, p2.y);
159
+ ctx.closePath();
 
 
 
 
 
 
 
 
 
 
 
 
160
 
161
+ const fillColor = tri.avgStrain > 0.005
162
+ ? strainColor(tri.avgStrain, tri.intensity)
163
+ : shadePaper(tri.intensity);
 
 
 
 
164
 
165
+ ctx.fillStyle = fillColor;
166
+ ctx.fill();
167
+ ctx.strokeStyle = 'rgba(42, 42, 58, 0.22)';
168
+ ctx.lineWidth = 0.55;
169
+ ctx.stroke();
170
+ }
171
+ }
172
 
173
+ export default function Fold3DCanvas({
174
+ steps,
175
+ currentStep,
176
+ dim = 280,
177
+ }) {
178
+ const canvasRef = useRef(null);
 
 
 
 
179
 
180
+ const getPaperState = useCallback(() => {
181
+ if (!steps || steps.length === 0 || currentStep === 0) return null;
182
+ const stepData = steps[currentStep - 1];
183
+ return stepData ? stepData.paper_state : null;
184
+ }, [steps, currentStep]);
185
 
186
  useEffect(() => {
187
+ const canvas = canvasRef.current;
188
+ if (!canvas) return;
189
+ const ctx = canvas.getContext('2d');
190
+ if (!ctx) return;
191
+ drawPaperState(ctx, getPaperState(), dim);
192
+ }, [getPaperState, dim]);
 
 
 
 
 
 
 
 
 
 
 
 
193
 
194
  return (
195
  <canvas