diff options
author | Dahlia Trimble | 2008-11-29 11:02:14 +0000 |
---|---|---|
committer | Dahlia Trimble | 2008-11-29 11:02:14 +0000 |
commit | fdd238833163eb947986bfcdd09da82f6949a5f2 (patch) | |
tree | 6b90177758405f6106f4f5d4d75e3b98bf08053c /OpenSim/Region/Physics/Meshing/Extruder.cs | |
parent | Comment the ScriptSponsor and restore the indefinite lifetime for (diff) | |
download | opensim-SC_OLD-fdd238833163eb947986bfcdd09da82f6949a5f2.zip opensim-SC_OLD-fdd238833163eb947986bfcdd09da82f6949a5f2.tar.gz opensim-SC_OLD-fdd238833163eb947986bfcdd09da82f6949a5f2.tar.bz2 opensim-SC_OLD-fdd238833163eb947986bfcdd09da82f6949a5f2.tar.xz |
Update meshing code to sync with current PrimMesher.cs on forge.
Migrate sculpt meshing code to primMesher version. This should result in more accurate physical sculpted prim proxies.
Remove much obsolete code from Region/Physics/Meshing
Diffstat (limited to 'OpenSim/Region/Physics/Meshing/Extruder.cs')
-rw-r--r-- | OpenSim/Region/Physics/Meshing/Extruder.cs | 472 |
1 files changed, 0 insertions, 472 deletions
diff --git a/OpenSim/Region/Physics/Meshing/Extruder.cs b/OpenSim/Region/Physics/Meshing/Extruder.cs deleted file mode 100644 index 1fc65e3..0000000 --- a/OpenSim/Region/Physics/Meshing/Extruder.cs +++ /dev/null | |||
@@ -1,472 +0,0 @@ | |||
1 | /* | ||
2 | * Copyright (c) Contributors, http://opensimulator.org/ | ||
3 | * See CONTRIBUTORS.TXT for a full list of copyright holders. | ||
4 | * | ||
5 | * Redistribution and use in source and binary forms, with or without | ||
6 | * modification, are permitted provided that the following conditions are met: | ||
7 | * * Redistributions of source code must retain the above copyright | ||
8 | * notice, this list of conditions and the following disclaimer. | ||
9 | * * Redistributions in binary form must reproduce the above copyright | ||
10 | * notice, this list of conditions and the following disclaimer in the | ||
11 | * documentation and/or other materials provided with the distribution. | ||
12 | * * Neither the name of the OpenSim Project nor the | ||
13 | * names of its contributors may be used to endorse or promote products | ||
14 | * derived from this software without specific prior written permission. | ||
15 | * | ||
16 | * THIS SOFTWARE IS PROVIDED BY THE DEVELOPERS ``AS IS'' AND ANY | ||
17 | * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED | ||
18 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | ||
19 | * DISCLAIMED. IN NO EVENT SHALL THE CONTRIBUTORS BE LIABLE FOR ANY | ||
20 | * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES | ||
21 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; | ||
22 | * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND | ||
23 | * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | ||
24 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS | ||
25 | * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | ||
26 | */ | ||
27 | //#define SPAM | ||
28 | |||
29 | using OpenMetaverse; | ||
30 | using OpenSim.Region.Physics.Manager; | ||
31 | |||
32 | namespace OpenSim.Region.Physics.Meshing | ||
33 | { | ||
34 | internal class Extruder | ||
35 | { | ||
36 | //public float startParameter; | ||
37 | //public float stopParameter; | ||
38 | public PhysicsVector size; | ||
39 | |||
40 | public float taperTopFactorX = 1f; | ||
41 | public float taperTopFactorY = 1f; | ||
42 | public float taperBotFactorX = 1f; | ||
43 | public float taperBotFactorY = 1f; | ||
44 | |||
45 | public float pushX = 0f; | ||
46 | public float pushY = 0f; | ||
47 | |||
48 | // twist amount in radians. NOT DEGREES. | ||
49 | public float twistTop = 0; | ||
50 | public float twistBot = 0; | ||
51 | public float twistMid = 0; | ||
52 | public float pathScaleX = 1.0f; | ||
53 | public float pathScaleY = 0.5f; | ||
54 | public float skew = 0.0f; | ||
55 | public float radius = 0.0f; | ||
56 | public float revolutions = 1.0f; | ||
57 | |||
58 | public float pathCutBegin = 0.0f; | ||
59 | public float pathCutEnd = 1.0f; | ||
60 | |||
61 | public ushort pathBegin = 0; | ||
62 | public ushort pathEnd = 0; | ||
63 | |||
64 | public float pathTaperX = 0.0f; | ||
65 | public float pathTaperY = 0.0f; | ||
66 | |||
67 | /// <summary> | ||
68 | /// Creates an extrusion of a profile along a linear path. Used to create prim types box, cylinder, and prism. | ||
69 | /// </summary> | ||
70 | /// <param name="m"></param> | ||
71 | /// <returns>A mesh of the extruded shape</returns> | ||
72 | public Mesh ExtrudeLinearPath(Mesh m) | ||
73 | { | ||
74 | Mesh result = new Mesh(); | ||
75 | |||
76 | Mesh newLayer; | ||
77 | Mesh lastLayer = null; | ||
78 | |||
79 | int step = 0; | ||
80 | int steps = 1; | ||
81 | |||
82 | float twistTotal = twistTop - twistBot; | ||
83 | // if the profile has a lot of twist, add more layers otherwise the layers may overlap | ||
84 | // and the resulting mesh may be quite inaccurate. This method is arbitrary and may not | ||
85 | // accurately match the viewer | ||
86 | float twistTotalAbs = System.Math.Abs(twistTotal); | ||
87 | if (twistTotalAbs > 0.01) | ||
88 | steps += (int)(twistTotalAbs * 3.66f); // dahlia's magic number ;) | ||
89 | |||
90 | #if SPAM | ||
91 | System.Console.WriteLine("ExtrudeLinearPath: twistTotalAbs: " + twistTotalAbs.ToString() + " steps: " + steps.ToString()); | ||
92 | #endif | ||
93 | |||
94 | double percentOfPathMultiplier = 1.0 / steps; | ||
95 | |||
96 | float start = -0.5f; | ||
97 | |||
98 | float stepSize = 1.0f / (float)steps; | ||
99 | |||
100 | float xProfileScale = 1.0f; | ||
101 | float yProfileScale = 1.0f; | ||
102 | |||
103 | float xOffset = 0.0f; | ||
104 | float yOffset = 0.0f; | ||
105 | float zOffset = start; | ||
106 | |||
107 | float xOffsetStepIncrement = pushX / steps; | ||
108 | float yOffsetStepIncrement = pushY / steps; | ||
109 | |||
110 | #if SPAM | ||
111 | System.Console.WriteLine("Extruder: twistTop: " + twistTop.ToString() + " twistbot: " + twistBot.ToString() + " twisttotal: " + twistTotal.ToString()); | ||
112 | System.Console.WriteLine("Extruder: taperBotFactorX: " + taperBotFactorX.ToString() + " taperBotFactorY: " + taperBotFactorY.ToString() | ||
113 | + " taperTopFactorX: " + taperTopFactorX.ToString() + " taperTopFactorY: " + taperTopFactorY.ToString()); | ||
114 | System.Console.WriteLine("Extruder: PathScaleX: " + pathScaleX.ToString() + " pathScaleY: " + pathScaleY.ToString()); | ||
115 | #endif | ||
116 | |||
117 | //float percentOfPath = 0.0f; | ||
118 | float percentOfPath = (float)pathBegin * 2.0e-5f; | ||
119 | zOffset += percentOfPath; | ||
120 | bool done = false; | ||
121 | do // loop through the length of the path and add the layers | ||
122 | { | ||
123 | newLayer = m.Clone(); | ||
124 | |||
125 | if (taperBotFactorX < 1.0f) | ||
126 | xProfileScale = 1.0f - (1.0f - percentOfPath) * (1.0f - taperBotFactorX); | ||
127 | else if (taperTopFactorX < 1.0f) | ||
128 | xProfileScale = 1.0f - percentOfPath * (1.0f - taperTopFactorX); | ||
129 | else xProfileScale = 1.0f; | ||
130 | |||
131 | if (taperBotFactorY < 1.0f) | ||
132 | yProfileScale = 1.0f - (1.0f - percentOfPath) * (1.0f - taperBotFactorY); | ||
133 | else if (taperTopFactorY < 1.0f) | ||
134 | yProfileScale = 1.0f - percentOfPath * (1.0f - taperTopFactorY); | ||
135 | else yProfileScale = 1.0f; | ||
136 | |||
137 | #if SPAM | ||
138 | //System.Console.WriteLine("xProfileScale: " + xProfileScale.ToString() + " yProfileScale: " + yProfileScale.ToString()); | ||
139 | #endif | ||
140 | Vertex vTemp = new Vertex(0.0f, 0.0f, 0.0f); | ||
141 | |||
142 | // apply the taper to the profile before any rotations | ||
143 | if (xProfileScale != 1.0f || yProfileScale != 1.0f) | ||
144 | { | ||
145 | foreach (Vertex v in newLayer.vertices) | ||
146 | { | ||
147 | if (v != null) | ||
148 | { | ||
149 | v.X *= xProfileScale; | ||
150 | v.Y *= yProfileScale; | ||
151 | } | ||
152 | } | ||
153 | } | ||
154 | |||
155 | |||
156 | float twist = twistBot + (twistTotal * (float)percentOfPath); | ||
157 | #if SPAM | ||
158 | System.Console.WriteLine("Extruder: percentOfPath: " + percentOfPath.ToString() + " zOffset: " + zOffset.ToString() | ||
159 | + " xProfileScale: " + xProfileScale.ToString() + " yProfileScale: " + yProfileScale.ToString()); | ||
160 | #endif | ||
161 | |||
162 | // apply twist rotation to the profile layer and position the layer in the prim | ||
163 | |||
164 | Quaternion profileRot = Quaternion.CreateFromAxisAngle(new Vector3(0.0f, 0.0f, 1.0f), twist); | ||
165 | foreach (Vertex v in newLayer.vertices) | ||
166 | { | ||
167 | if (v != null) | ||
168 | { | ||
169 | vTemp = v * profileRot; | ||
170 | v.X = vTemp.X + xOffset; | ||
171 | v.Y = vTemp.Y + yOffset; | ||
172 | v.Z = vTemp.Z + zOffset; | ||
173 | } | ||
174 | } | ||
175 | |||
176 | if (step == 0) // the first layer, invert normals | ||
177 | { | ||
178 | foreach (Triangle t in newLayer.triangles) | ||
179 | { | ||
180 | t.invertNormal(); | ||
181 | } | ||
182 | } | ||
183 | |||
184 | result.Append(newLayer); | ||
185 | |||
186 | int iLastNull = 0; | ||
187 | |||
188 | if (lastLayer != null) | ||
189 | { | ||
190 | int i, count = newLayer.vertices.Count; | ||
191 | |||
192 | for (i = 0; i < count; i++) | ||
193 | { | ||
194 | int iNext = (i + 1); | ||
195 | |||
196 | if (lastLayer.vertices[i] == null) // cant make a simplex here | ||
197 | { | ||
198 | iLastNull = i + 1; | ||
199 | } | ||
200 | else | ||
201 | { | ||
202 | if (i == count - 1) // End of list | ||
203 | iNext = iLastNull; | ||
204 | |||
205 | if (lastLayer.vertices[iNext] == null) // Null means wrap to begin of last segment | ||
206 | iNext = iLastNull; | ||
207 | |||
208 | result.Add(new Triangle(newLayer.vertices[i], lastLayer.vertices[i], newLayer.vertices[iNext])); | ||
209 | result.Add(new Triangle(newLayer.vertices[iNext], lastLayer.vertices[i], lastLayer.vertices[iNext])); | ||
210 | } | ||
211 | } | ||
212 | } | ||
213 | lastLayer = newLayer; | ||
214 | |||
215 | // calc the step for the next interation of the loop | ||
216 | |||
217 | if (step < steps) | ||
218 | { | ||
219 | step++; | ||
220 | percentOfPath += (float)percentOfPathMultiplier; | ||
221 | |||
222 | xOffset += xOffsetStepIncrement; | ||
223 | yOffset += yOffsetStepIncrement; | ||
224 | zOffset += stepSize; | ||
225 | |||
226 | if (percentOfPath > 1.0f - (float)pathEnd * 2.0e-5f) | ||
227 | done = true; | ||
228 | } | ||
229 | else done = true; | ||
230 | |||
231 | } while (!done); // loop until all the layers in the path are completed | ||
232 | |||
233 | // scale the mesh to the desired size | ||
234 | float xScale = size.X; | ||
235 | float yScale = size.Y; | ||
236 | float zScale = size.Z; | ||
237 | |||
238 | foreach (Vertex v in result.vertices) | ||
239 | { | ||
240 | if (v != null) | ||
241 | { | ||
242 | v.X *= xScale; | ||
243 | v.Y *= yScale; | ||
244 | v.Z *= zScale; | ||
245 | } | ||
246 | } | ||
247 | |||
248 | return result; | ||
249 | } | ||
250 | |||
251 | /// <summary> | ||
252 | /// Extrudes a shape around a circular path. Used to create prim types torus, ring, and tube. | ||
253 | /// </summary> | ||
254 | /// <param name="m"></param> | ||
255 | /// <returns>a mesh of the extruded shape</returns> | ||
256 | public Mesh ExtrudeCircularPath(Mesh m) | ||
257 | { | ||
258 | Mesh result = new Mesh(); | ||
259 | |||
260 | Mesh newLayer; | ||
261 | Mesh lastLayer = null; | ||
262 | |||
263 | int step; | ||
264 | int steps = 24; | ||
265 | |||
266 | float twistTotal = twistTop - twistBot; | ||
267 | // if the profile has a lot of twist, add more layers otherwise the layers may overlap | ||
268 | // and the resulting mesh may be quite inaccurate. This method is arbitrary and doesn't | ||
269 | // accurately match the viewer | ||
270 | if (System.Math.Abs(twistTotal) > (float)System.Math.PI * 1.5f) steps *= 2; | ||
271 | if (System.Math.Abs(twistTotal) > (float)System.Math.PI * 3.0f) steps *= 2; | ||
272 | |||
273 | // double percentOfPathMultiplier = 1.0 / steps; | ||
274 | // double angleStepMultiplier = System.Math.PI * 2.0 / steps; | ||
275 | |||
276 | float yPathScale = pathScaleY * 0.5f; | ||
277 | float pathLength = pathCutEnd - pathCutBegin; | ||
278 | float totalSkew = skew * 2.0f * pathLength; | ||
279 | float skewStart = (-skew) + pathCutBegin * 2.0f * skew; | ||
280 | |||
281 | // It's not quite clear what pushY (Y top shear) does, but subtracting it from the start and end | ||
282 | // angles appears to approximate it's effects on path cut. Likewise, adding it to the angle used | ||
283 | // to calculate the sine for generating the path radius appears to approximate it's effects there | ||
284 | // too, but there are some subtle differences in the radius which are noticeable as the prim size | ||
285 | // increases and it may affect megaprims quite a bit. The effect of the Y top shear parameter on | ||
286 | // the meshes generated with this technique appear nearly identical in shape to the same prims when | ||
287 | // displayed by the viewer. | ||
288 | |||
289 | |||
290 | float startAngle = (float)(System.Math.PI * 2.0 * pathCutBegin * revolutions) - pushY * 0.9f; | ||
291 | float endAngle = (float)(System.Math.PI * 2.0 * pathCutEnd * revolutions) - pushY * 0.9f; | ||
292 | float stepSize = (float)0.2617993878; // 2*PI / 24 segments per revolution | ||
293 | |||
294 | step = (int)(startAngle / stepSize); | ||
295 | float angle = startAngle; | ||
296 | |||
297 | float xProfileScale = 1.0f; | ||
298 | float yProfileScale = 1.0f; | ||
299 | |||
300 | |||
301 | #if SPAM | ||
302 | System.Console.WriteLine("Extruder: twistTop: " + twistTop.ToString() + " twistbot: " + twistBot.ToString() + " twisttotal: " + twistTotal.ToString()); | ||
303 | System.Console.WriteLine("Extruder: startAngle: " + startAngle.ToString() + " endAngle: " + endAngle.ToString() + " step: " + step.ToString()); | ||
304 | System.Console.WriteLine("Extruder: taperBotFactorX: " + taperBotFactorX.ToString() + " taperBotFactorY: " + taperBotFactorY.ToString() | ||
305 | + " taperTopFactorX: " + taperTopFactorX.ToString() + " taperTopFactorY: " + taperTopFactorY.ToString()); | ||
306 | System.Console.WriteLine("Extruder: PathScaleX: " + pathScaleX.ToString() + " pathScaleY: " + pathScaleY.ToString()); | ||
307 | #endif | ||
308 | |||
309 | bool done = false; | ||
310 | do // loop through the length of the path and add the layers | ||
311 | { | ||
312 | newLayer = m.Clone(); | ||
313 | |||
314 | float percentOfPath = (angle - startAngle) / (endAngle - startAngle); // endAngle should always be larger than startAngle | ||
315 | |||
316 | if (pathTaperX > 0.001f) // can't really compare to 0.0f as the value passed is never exactly zero | ||
317 | xProfileScale = 1.0f - percentOfPath * pathTaperX; | ||
318 | else if (pathTaperX < -0.001f) | ||
319 | xProfileScale = 1.0f + (1.0f - percentOfPath) * pathTaperX; | ||
320 | else xProfileScale = 1.0f; | ||
321 | |||
322 | if (pathTaperY > 0.001f) | ||
323 | yProfileScale = 1.0f - percentOfPath * pathTaperY; | ||
324 | else if (pathTaperY < -0.001f) | ||
325 | yProfileScale = 1.0f + (1.0f - percentOfPath) * pathTaperY; | ||
326 | else yProfileScale = 1.0f; | ||
327 | |||
328 | #if SPAM | ||
329 | //System.Console.WriteLine("xProfileScale: " + xProfileScale.ToString() + " yProfileScale: " + yProfileScale.ToString()); | ||
330 | #endif | ||
331 | Vertex vTemp = new Vertex(0.0f, 0.0f, 0.0f); | ||
332 | |||
333 | // apply the taper to the profile before any rotations | ||
334 | if (xProfileScale != 1.0f || yProfileScale != 1.0f) | ||
335 | { | ||
336 | foreach (Vertex v in newLayer.vertices) | ||
337 | { | ||
338 | if (v != null) | ||
339 | { | ||
340 | v.X *= xProfileScale; | ||
341 | v.Y *= yProfileScale; | ||
342 | } | ||
343 | } | ||
344 | } | ||
345 | |||
346 | float radiusScale; | ||
347 | |||
348 | if (radius > 0.001f) | ||
349 | radiusScale = 1.0f - radius * percentOfPath; | ||
350 | else if (radius < 0.001f) | ||
351 | radiusScale = 1.0f + radius * (1.0f - percentOfPath); | ||
352 | else | ||
353 | radiusScale = 1.0f; | ||
354 | |||
355 | #if SPAM | ||
356 | System.Console.WriteLine("Extruder: angle: " + angle.ToString() + " percentOfPath: " + percentOfPath.ToString() | ||
357 | + " radius: " + radius.ToString() + " radiusScale: " + radiusScale.ToString() | ||
358 | + " xProfileScale: " + xProfileScale.ToString() + " yProfileScale: " + yProfileScale.ToString()); | ||
359 | #endif | ||
360 | |||
361 | float twist = twistBot + (twistTotal * (float)percentOfPath); | ||
362 | |||
363 | float xOffset; | ||
364 | float yOffset; | ||
365 | float zOffset; | ||
366 | |||
367 | xOffset = 0.5f * (skewStart + totalSkew * (float)percentOfPath); | ||
368 | xOffset += (float) System.Math.Sin(angle) * pushX * 0.45f; | ||
369 | yOffset = (float)(System.Math.Cos(angle) * (0.5f - yPathScale)) * radiusScale; | ||
370 | zOffset = (float)(System.Math.Sin(angle + pushY * 0.9f) * (0.5f - yPathScale)) * radiusScale; | ||
371 | |||
372 | // next apply twist rotation to the profile layer | ||
373 | if (twistTotal != 0.0f || twistBot != 0.0f) | ||
374 | { | ||
375 | Quaternion profileRot = new Quaternion(new Vector3(0.0f, 0.0f, 1.0f), twist); | ||
376 | foreach (Vertex v in newLayer.vertices) | ||
377 | { | ||
378 | if (v != null) | ||
379 | { | ||
380 | vTemp = v * profileRot; | ||
381 | v.X = vTemp.X; | ||
382 | v.Y = vTemp.Y; | ||
383 | v.Z = vTemp.Z; | ||
384 | } | ||
385 | } | ||
386 | } | ||
387 | |||
388 | // now orient the rotation of the profile layer relative to it's position on the path | ||
389 | // adding pushY to the angle used to generate the quat appears to approximate the viewer | ||
390 | Quaternion layerRot = Quaternion.CreateFromAxisAngle(new Vector3(1.0f, 0.0f, 0.0f), (float)angle + pushY * 0.9f); | ||
391 | foreach (Vertex v in newLayer.vertices) | ||
392 | { | ||
393 | if (v != null) | ||
394 | { | ||
395 | vTemp = v * layerRot; | ||
396 | v.X = vTemp.X + xOffset; | ||
397 | v.Y = vTemp.Y + yOffset; | ||
398 | v.Z = vTemp.Z + zOffset; | ||
399 | } | ||
400 | } | ||
401 | |||
402 | if (angle == startAngle) // the first layer, invert normals | ||
403 | { | ||
404 | foreach (Triangle t in newLayer.triangles) | ||
405 | { | ||
406 | t.invertNormal(); | ||
407 | } | ||
408 | } | ||
409 | |||
410 | result.Append(newLayer); | ||
411 | |||
412 | int iLastNull = 0; | ||
413 | |||
414 | if (lastLayer != null) | ||
415 | { | ||
416 | int i, count = newLayer.vertices.Count; | ||
417 | |||
418 | for (i = 0; i < count; i++) | ||
419 | { | ||
420 | int iNext = (i + 1); | ||
421 | |||
422 | if (lastLayer.vertices[i] == null) // cant make a simplex here | ||
423 | { | ||
424 | iLastNull = i + 1; | ||
425 | } | ||
426 | else | ||
427 | { | ||
428 | if (i == count - 1) // End of list | ||
429 | iNext = iLastNull; | ||
430 | |||
431 | if (lastLayer.vertices[iNext] == null) // Null means wrap to begin of last segment | ||
432 | iNext = iLastNull; | ||
433 | |||
434 | result.Add(new Triangle(newLayer.vertices[i], lastLayer.vertices[i], newLayer.vertices[iNext])); | ||
435 | result.Add(new Triangle(newLayer.vertices[iNext], lastLayer.vertices[i], lastLayer.vertices[iNext])); | ||
436 | } | ||
437 | } | ||
438 | } | ||
439 | lastLayer = newLayer; | ||
440 | |||
441 | // calc the angle for the next interation of the loop | ||
442 | if (angle >= endAngle) | ||
443 | { | ||
444 | done = true; | ||
445 | } | ||
446 | else | ||
447 | { | ||
448 | angle = stepSize * ++step; | ||
449 | if (angle > endAngle) | ||
450 | angle = endAngle; | ||
451 | } | ||
452 | } while (!done); // loop until all the layers in the path are completed | ||
453 | |||
454 | // scale the mesh to the desired size | ||
455 | float xScale = size.X; | ||
456 | float yScale = size.Y; | ||
457 | float zScale = size.Z; | ||
458 | |||
459 | foreach (Vertex v in result.vertices) | ||
460 | { | ||
461 | if (v != null) | ||
462 | { | ||
463 | v.X *= xScale; | ||
464 | v.Y *= yScale; | ||
465 | v.Z *= zScale; | ||
466 | } | ||
467 | } | ||
468 | |||
469 | return result; | ||
470 | } | ||
471 | } | ||
472 | } | ||