|
82
|
1 // -- Dog -- //
|
|
|
2 const SCREEN_WIDTH = 800;
|
|
|
3 const SCREEN_HEIGHT = 600;
|
|
|
4 const PIXEL_SIZE = 3;
|
|
|
5 const FRAME = 60;
|
|
|
6
|
|
|
7 const dog = document.getElementById("epi3D");
|
|
|
8 dog.width = SCREEN_WIDTH;
|
|
|
9 dog.height = SCREEN_HEIGHT;
|
|
|
10 const ctx = dog.getContext("2d");
|
|
|
11
|
|
|
12 function drawBackground() {
|
|
|
13 ctx.fillStyle = "black";
|
|
|
14 ctx.fillRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);
|
|
|
15 }
|
|
|
16
|
|
|
17 function drawPixel({x, y}) {
|
|
|
18 ctx.fillStyle = "blue";
|
|
|
19 ctx.fillRect(x * SCREEN_WIDTH, y * SCREEN_HEIGHT, PIXEL_SIZE, PIXEL_SIZE);
|
|
|
20 }
|
|
|
21
|
|
|
22 function normalize({x, y}) {
|
|
|
23 return {
|
|
|
24 x: ((x + 1) / 2),
|
|
|
25 y: (1 - ((y + 1) / 2)),
|
|
|
26 }
|
|
|
27 }
|
|
|
28
|
|
|
29 function threeDtotwoD({x, y, z}) {
|
|
|
30 return {
|
|
|
31 x: x/z,
|
|
|
32 y: y/z
|
|
|
33 }
|
|
|
34 }
|
|
|
35
|
|
|
36 function drawLine(point1, point2) {
|
|
|
37 ctx.beginPath();
|
|
|
38 ctx.moveTo(point1.x * SCREEN_WIDTH, point1.y * SCREEN_HEIGHT);
|
|
|
39 ctx.lineTo(point2.x * SCREEN_WIDTH, point2.y * SCREEN_HEIGHT);
|
|
|
40 ctx.lineWidth = 3;
|
|
|
41 ctx.strokeStyle = "red";
|
|
|
42 ctx.stroke();
|
|
|
43 }
|
|
|
44
|
|
|
45 let points = [];
|
|
|
46 let vertices = [];
|
|
|
47
|
|
|
48 async function loadXYZModel(filepath) {
|
|
|
49 try {
|
|
|
50 const response = await fetch(filepath);
|
|
|
51 const xyzContent = await response.text();
|
|
|
52 // return parseXYZFile(xyzContent);
|
|
|
53 return parseSmartSTL(xyzContent);
|
|
|
54 } catch (error) {
|
|
|
55 console.error('Error loading XYZ file:', error);
|
|
|
56 return null;
|
|
|
57 }
|
|
|
58 }
|
|
|
59
|
|
|
60 function downsamplePoints(points, gridSize = 0.2) {
|
|
|
61 const grid = new Map();
|
|
|
62
|
|
|
63 for (const point of points) {
|
|
|
64 // Create grid cell key
|
|
|
65 const cellX = Math.floor(point.x / gridSize);
|
|
|
66 const cellY = Math.floor(point.y / gridSize);
|
|
|
67 const cellZ = Math.floor(point.z / gridSize);
|
|
|
68 const key = `${cellX},${cellY},${cellZ}`;
|
|
|
69
|
|
|
70 // Keep first point in each cell (or you could average them)
|
|
|
71 if (!grid.has(key)) {
|
|
|
72 grid.set(key, point);
|
|
|
73 }
|
|
|
74 }
|
|
|
75
|
|
|
76 return Array.from(grid.values());
|
|
|
77 }
|
|
|
78
|
|
|
79 async function initModel() {
|
|
|
80 const model = await loadXYZModel('/public/dog.xyz');
|
|
|
81
|
|
|
82 if (model) {
|
|
|
83 const normalizedPoints = normalizePoints(model.points, 1.0);
|
|
|
84 points = downsamplePoints(normalizedPoints, 0.1); // grid method
|
|
|
85
|
|
|
86 vertices = model.vertices;
|
|
|
87 for (let i = 0; i < points.length - 1; i++) {
|
|
|
88 vertices.push([i, i + 1]);
|
|
|
89 }
|
|
|
90
|
|
|
91 // Start the animation after loading
|
|
|
92 drawAnimation();
|
|
|
93 } else {
|
|
|
94 console.error('Failed to load model');
|
|
|
95 }
|
|
|
96 }
|
|
|
97
|
|
|
98 function parseSmartSTL(stlString) {
|
|
|
99 const lines = stlString.split('\n');
|
|
|
100 const points = [];
|
|
|
101 const vortexs = [];
|
|
|
102
|
|
|
103 let currentNormal = "";
|
|
|
104 let tempVertices = [];
|
|
|
105
|
|
|
106 lines.forEach((line) => {
|
|
|
107 const trimmed = line.trim();
|
|
|
108
|
|
|
109 // 1. Detect the "Angle" (Normal)
|
|
|
110 if (trimmed.startsWith('facet normal')) {
|
|
|
111 const normal = trimmed.replace('facet normal ', '');
|
|
|
112 // Only care if the normal is different from the last one (it's a new angle)
|
|
|
113 // Or just keep them all for now and we'll filter by position
|
|
|
114 currentNormal = normal;
|
|
|
115 tempVertices = [];
|
|
|
116 }
|
|
|
117
|
|
|
118 // 2. Grab the vertices
|
|
|
119 if (trimmed.startsWith('vertex')) {
|
|
|
120 const parts = trimmed.split(/\s+/);
|
|
|
121 tempVertices.push({
|
|
|
122 x: parseFloat(parts[1]),
|
|
|
123 y: parseFloat(parts[2]),
|
|
|
124 z: parseFloat(parts[3])
|
|
|
125 });
|
|
|
126 }
|
|
|
127
|
|
|
128 // 3. When the triangle ends, connect them
|
|
|
129 if (trimmed.startsWith('endloop')) {
|
|
|
130 const startIndex = points.length;
|
|
|
131 points.push(...tempVertices);
|
|
|
132
|
|
|
133 // Create a triangle connection (vortex)
|
|
|
134 vortexs.push([startIndex, startIndex + 1, startIndex + 2]);
|
|
|
135 }
|
|
|
136 });
|
|
|
137
|
|
|
138 return { points, vortexs };
|
|
|
139 }
|
|
|
140
|
|
|
141 // Usage:
|
|
|
142 // const { points, vortexs } = parseSmartSTL(yourStlString);
|
|
|
143
|
|
|
144 initModel();
|
|
|
145
|
|
|
146 function parseXYZFile(xyzFileContent) {
|
|
|
147 const lines = xyzFileContent.trim().split('\n').filter(line => line.trim());
|
|
|
148
|
|
|
149 const points = lines.map(line => {
|
|
|
150 const coords = line.trim().split(/\s+/).map(Number);
|
|
|
151 return {
|
|
|
152 x: coords[0],
|
|
|
153 y: coords[1],
|
|
|
154 z: coords[2]
|
|
|
155 };
|
|
|
156 });
|
|
|
157
|
|
|
158 return { points };
|
|
|
159 }
|
|
|
160
|
|
|
161 function normalizePoints(points, scale = 1.0) {
|
|
|
162 const xs = points.map(p => p.x);
|
|
|
163 const ys = points.map(p => p.y);
|
|
|
164 const zs = points.map(p => p.z);
|
|
|
165
|
|
|
166 const minX = Math.min(...xs), maxX = Math.max(...xs);
|
|
|
167 const minY = Math.min(...ys), maxY = Math.max(...ys);
|
|
|
168 const minZ = Math.min(...zs), maxZ = Math.max(...zs);
|
|
|
169
|
|
|
170 const centerX = (minX + maxX) / 2;
|
|
|
171 const centerY = (minY + maxY) / 2;
|
|
|
172 const centerZ = (minZ + maxZ) / 2;
|
|
|
173
|
|
|
174 const rangeX = maxX - minX;
|
|
|
175 const rangeY = maxY - minY;
|
|
|
176 const rangeZ = maxZ - minZ;
|
|
|
177 const maxRange = Math.max(rangeX, rangeY, rangeZ);
|
|
|
178
|
|
|
179 return points.map(p => ({
|
|
|
180 x: ((p.x - centerX) / maxRange) * scale,
|
|
|
181 y: ((p.y - centerY) / maxRange) * scale,
|
|
|
182 z: ((p.z - centerZ) / maxRange) * scale
|
|
|
183 }));
|
|
|
184 }
|
|
|
185
|
|
|
186 function rotate_xz({x, y, z}, angle) {
|
|
|
187 return {
|
|
|
188 x: x * Math.cos(angle) - z * Math.sin(angle),
|
|
|
189 y: y,
|
|
|
190 z: x * Math.sin(angle) + z * Math.cos(angle),
|
|
|
191 }
|
|
|
192 }
|
|
|
193
|
|
|
194 function move_point(point, { x, y, z}) {
|
|
|
195 return {
|
|
|
196 ...point,
|
|
|
197 x: point.x + x,
|
|
|
198 y: 1 - (point.y + y),
|
|
|
199 z: point.z + z
|
|
|
200 }
|
|
|
201 }
|
|
|
202
|
|
|
203 function move_z(point, dz) {
|
|
|
204 return {...point, z: point.z + dz}
|
|
|
205 }
|
|
|
206
|
|
|
207 let dz = 0;
|
|
|
208 let dt = 1/FRAME;
|
|
|
209 let angle = 0;
|
|
|
210
|
|
|
211 function drawAnimation() {
|
|
|
212 drawBackground();
|
|
|
213 // dz += 1 * dt;
|
|
|
214 angle += 1 * Math.PI * dt;
|
|
|
215
|
|
|
216 for (const point of points) {
|
|
|
217 drawPixel(
|
|
|
218 threeDtotwoD(
|
|
|
219 move_point(rotate_xz(point, angle), { x: 0.7, y: 0.5, z: 1.5})
|
|
|
220 )
|
|
|
221 );
|
|
|
222 }
|
|
|
223
|
|
|
224 for (const vertex of vertices) {
|
|
|
225 for (let i = 0; i < vertex.length - 1; i++) {
|
|
|
226 const point1 = normalize(
|
|
|
227 threeDtotwoD(
|
|
|
228 move_point(rotate_xz(points[vertex[i]], angle), { x: 0.7, y: 0.5, z: 1.5})
|
|
|
229 )
|
|
|
230 );
|
|
|
231 const point2 = normalize(
|
|
|
232 threeDtotwoD(
|
|
|
233 move_point(rotate_xz(points[vertex[i + 1]], angle), { x: 0.7, y: 0.5, z: 1.5})
|
|
|
234 )
|
|
|
235 );
|
|
|
236 drawLine(point1, point2);
|
|
|
237 }
|
|
|
238 }
|
|
|
239
|
|
|
240 setTimeout(() => drawAnimation(), 1000/60);
|
|
|
241 }
|
|
|
242 drawAnimation();
|