diff options
author | Robert Adams | 2012-10-29 14:33:31 -0700 |
---|---|---|
committer | Robert Adams | 2012-11-03 21:14:41 -0700 |
commit | 93fe384cce42e91337f446fd658ef29ca3d9f733 (patch) | |
tree | 8ed885f7b33d929a73a8742063b5a7f9e32a98bb | |
parent | BulletSim: Add gravity force to vehicle. Some debugging additions. (diff) | |
download | opensim-SC-93fe384cce42e91337f446fd658ef29ca3d9f733.zip opensim-SC-93fe384cce42e91337f446fd658ef29ca3d9f733.tar.gz opensim-SC-93fe384cce42e91337f446fd658ef29ca3d9f733.tar.bz2 opensim-SC-93fe384cce42e91337f446fd658ef29ca3d9f733.tar.xz |
BulletSim: Use the PostTaints operation to build the linkset once before the next simulation step. This eliminates the management of children vs taintChildren and simplifies the constratin creation code.
-rwxr-xr-x | OpenSim/Region/Physics/BulletSPlugin/BSLinkset.cs | 52 | ||||
-rwxr-xr-x | OpenSim/Region/Physics/BulletSPlugin/BSLinksetConstraints.cs | 140 |
2 files changed, 68 insertions, 124 deletions
diff --git a/OpenSim/Region/Physics/BulletSPlugin/BSLinkset.cs b/OpenSim/Region/Physics/BulletSPlugin/BSLinkset.cs index 569d2e7..187951e 100755 --- a/OpenSim/Region/Physics/BulletSPlugin/BSLinkset.cs +++ b/OpenSim/Region/Physics/BulletSPlugin/BSLinkset.cs | |||
@@ -61,16 +61,7 @@ public abstract class BSLinkset | |||
61 | public int LinksetID { get; private set; } | 61 | public int LinksetID { get; private set; } |
62 | 62 | ||
63 | // The children under the root in this linkset. | 63 | // The children under the root in this linkset. |
64 | // There are two lists of children: the current children at runtime | ||
65 | // and the children at taint-time. For instance, if you delink a | ||
66 | // child from the linkset, the child is removed from m_children | ||
67 | // but the constraint won't be removed until taint time. | ||
68 | // Two lists lets this track the 'current' children and | ||
69 | // the physical 'taint' children separately. | ||
70 | // After taint processing and before the simulation step, these | ||
71 | // two lists must be the same. | ||
72 | protected HashSet<BSPhysObject> m_children; | 64 | protected HashSet<BSPhysObject> m_children; |
73 | protected HashSet<BSPhysObject> m_taintChildren; | ||
74 | 65 | ||
75 | // We lock the diddling of linkset classes to prevent any badness. | 66 | // We lock the diddling of linkset classes to prevent any badness. |
76 | // This locks the modification of the instances of this class. Changes | 67 | // This locks the modification of the instances of this class. Changes |
@@ -110,7 +101,6 @@ public abstract class BSLinkset | |||
110 | PhysicsScene = scene; | 101 | PhysicsScene = scene; |
111 | LinksetRoot = parent; | 102 | LinksetRoot = parent; |
112 | m_children = new HashSet<BSPhysObject>(); | 103 | m_children = new HashSet<BSPhysObject>(); |
113 | m_taintChildren = new HashSet<BSPhysObject>(); | ||
114 | m_mass = parent.MassRaw; | 104 | m_mass = parent.MassRaw; |
115 | } | 105 | } |
116 | 106 | ||
@@ -192,7 +182,7 @@ public abstract class BSLinkset | |||
192 | lock (m_linksetActivityLock) | 182 | lock (m_linksetActivityLock) |
193 | { | 183 | { |
194 | action(LinksetRoot); | 184 | action(LinksetRoot); |
195 | foreach (BSPhysObject po in m_taintChildren) | 185 | foreach (BSPhysObject po in m_children) |
196 | { | 186 | { |
197 | if (action(po)) | 187 | if (action(po)) |
198 | break; | 188 | break; |
@@ -201,9 +191,24 @@ public abstract class BSLinkset | |||
201 | return ret; | 191 | return ret; |
202 | } | 192 | } |
203 | 193 | ||
194 | // I am the root of a linkset and a new child is being added | ||
195 | // Called while LinkActivity is locked. | ||
196 | protected abstract void AddChildToLinkset(BSPhysObject child); | ||
197 | |||
198 | // Forcefully removing a child from a linkset. | ||
199 | // This is not being called by the child so we have to make sure the child doesn't think | ||
200 | // it's still connected to the linkset. | ||
201 | // Normal OpenSimulator operation will never do this because other SceneObjectPart information | ||
202 | // also has to be updated (like pointer to prim's parent). | ||
203 | protected abstract void RemoveChildFromOtherLinkset(BSPhysObject pchild); | ||
204 | |||
205 | // I am the root of a linkset and one of my children is being removed. | ||
206 | // Safe to call even if the child is not really in my linkset. | ||
207 | protected abstract void RemoveChildFromLinkset(BSPhysObject child); | ||
208 | |||
204 | // When physical properties are changed the linkset needs to recalculate | 209 | // When physical properties are changed the linkset needs to recalculate |
205 | // its internal properties. | 210 | // its internal properties. |
206 | // May be called at runtime or taint-time (just pass the appropriate flag). | 211 | // May be called at runtime or taint-time. |
207 | public abstract void Refresh(BSPhysObject requestor); | 212 | public abstract void Refresh(BSPhysObject requestor); |
208 | 213 | ||
209 | // The object is going dynamic (physical). Do any setup necessary | 214 | // The object is going dynamic (physical). Do any setup necessary |
@@ -238,8 +243,6 @@ public abstract class BSLinkset | |||
238 | public abstract void RestoreBodyDependencies(BSPrim child); | 243 | public abstract void RestoreBodyDependencies(BSPrim child); |
239 | 244 | ||
240 | // ================================================================ | 245 | // ================================================================ |
241 | // Below this point is internal magic | ||
242 | |||
243 | protected virtual float ComputeLinksetMass() | 246 | protected virtual float ComputeLinksetMass() |
244 | { | 247 | { |
245 | float mass = LinksetRoot.MassRaw; | 248 | float mass = LinksetRoot.MassRaw; |
@@ -264,7 +267,7 @@ public abstract class BSLinkset | |||
264 | com = LinksetRoot.Position * LinksetRoot.MassRaw; | 267 | com = LinksetRoot.Position * LinksetRoot.MassRaw; |
265 | float totalMass = LinksetRoot.MassRaw; | 268 | float totalMass = LinksetRoot.MassRaw; |
266 | 269 | ||
267 | foreach (BSPhysObject bp in m_taintChildren) | 270 | foreach (BSPhysObject bp in m_children) |
268 | { | 271 | { |
269 | com += bp.Position * bp.MassRaw; | 272 | com += bp.Position * bp.MassRaw; |
270 | totalMass += bp.MassRaw; | 273 | totalMass += bp.MassRaw; |
@@ -283,31 +286,16 @@ public abstract class BSLinkset | |||
283 | { | 286 | { |
284 | com = LinksetRoot.Position; | 287 | com = LinksetRoot.Position; |
285 | 288 | ||
286 | foreach (BSPhysObject bp in m_taintChildren) | 289 | foreach (BSPhysObject bp in m_children) |
287 | { | 290 | { |
288 | com += bp.Position * bp.MassRaw; | 291 | com += bp.Position * bp.MassRaw; |
289 | } | 292 | } |
290 | com /= (m_taintChildren.Count + 1); | 293 | com /= (m_children.Count + 1); |
291 | } | 294 | } |
292 | 295 | ||
293 | return com; | 296 | return com; |
294 | } | 297 | } |
295 | 298 | ||
296 | // I am the root of a linkset and a new child is being added | ||
297 | // Called while LinkActivity is locked. | ||
298 | protected abstract void AddChildToLinkset(BSPhysObject child); | ||
299 | |||
300 | // Forcefully removing a child from a linkset. | ||
301 | // This is not being called by the child so we have to make sure the child doesn't think | ||
302 | // it's still connected to the linkset. | ||
303 | // Normal OpenSimulator operation will never do this because other SceneObjectPart information | ||
304 | // also has to be updated (like pointer to prim's parent). | ||
305 | protected abstract void RemoveChildFromOtherLinkset(BSPhysObject pchild); | ||
306 | |||
307 | // I am the root of a linkset and one of my children is being removed. | ||
308 | // Safe to call even if the child is not really in my linkset. | ||
309 | protected abstract void RemoveChildFromLinkset(BSPhysObject child); | ||
310 | |||
311 | // Invoke the detailed logger and output something if it's enabled. | 299 | // Invoke the detailed logger and output something if it's enabled. |
312 | protected void DetailLog(string msg, params Object[] args) | 300 | protected void DetailLog(string msg, params Object[] args) |
313 | { | 301 | { |
diff --git a/OpenSim/Region/Physics/BulletSPlugin/BSLinksetConstraints.cs b/OpenSim/Region/Physics/BulletSPlugin/BSLinksetConstraints.cs index 086aa12..6c1fa2a 100755 --- a/OpenSim/Region/Physics/BulletSPlugin/BSLinksetConstraints.cs +++ b/OpenSim/Region/Physics/BulletSPlugin/BSLinksetConstraints.cs | |||
@@ -54,7 +54,7 @@ public sealed class BSLinksetConstraints : BSLinkset | |||
54 | // Queue to happen after all the other taint processing | 54 | // Queue to happen after all the other taint processing |
55 | PhysicsScene.PostTaintObject("BSLinksetContraints.Refresh", requestor.LocalID, delegate() | 55 | PhysicsScene.PostTaintObject("BSLinksetContraints.Refresh", requestor.LocalID, delegate() |
56 | { | 56 | { |
57 | RecomputeLinksetConstraintVariables(); | 57 | RecomputeLinksetConstraints(); |
58 | }); | 58 | }); |
59 | 59 | ||
60 | } | 60 | } |
@@ -98,24 +98,13 @@ public sealed class BSLinksetConstraints : BSLinkset | |||
98 | 98 | ||
99 | lock (m_linksetActivityLock) | 99 | lock (m_linksetActivityLock) |
100 | { | 100 | { |
101 | if (IsRoot(child)) | 101 | // Just undo all the constraints for this linkset. Rebuild at the end of the step. |
102 | { | 102 | DetailLog("{0},BSLinksetConstraint.RemoveBodyDependencies,removeChildrenForRoot,rID={1},rBody={2}", |
103 | // If the one with the dependency is root, must undo all children | 103 | child.LocalID, LinksetRoot.LocalID, LinksetRoot.BSBody.ptr.ToString("X")); |
104 | DetailLog("{0},BSLinksetConstraint.RemoveBodyDependencies,removeChildrenForRoot,rID={1},rBody={2}", | ||
105 | child.LocalID, LinksetRoot.LocalID, LinksetRoot.BSBody.ptr.ToString("X")); | ||
106 | 104 | ||
107 | ret = PhysicallyUnlinkAllChildrenFromRoot(LinksetRoot); | 105 | ret = PhysicallyUnlinkAllChildrenFromRoot(LinksetRoot); |
108 | } | 106 | // Cause the constraints, et al to be rebuilt before the next simulation step. |
109 | else | 107 | Refresh(LinksetRoot); |
110 | { | ||
111 | DetailLog("{0},BSLinksetConstraint.RemoveBodyDependencies,removeSingleChild,rID={1},rBody={2},cID={3},cBody={4}", | ||
112 | child.LocalID, | ||
113 | LinksetRoot.LocalID, LinksetRoot.BSBody.ptr.ToString("X"), | ||
114 | child.LocalID, child.BSBody.ptr.ToString("X")); | ||
115 | // ret = PhysicallyUnlinkAChildFromRoot(LinksetRoot, child); | ||
116 | // Despite the function name, this removes any link to the specified object. | ||
117 | ret = PhysicallyUnlinkAllChildrenFromRoot(child); | ||
118 | } | ||
119 | } | 108 | } |
120 | return ret; | 109 | return ret; |
121 | } | 110 | } |
@@ -125,26 +114,7 @@ public sealed class BSLinksetConstraints : BSLinkset | |||
125 | // Called at taint-time!! | 114 | // Called at taint-time!! |
126 | public override void RestoreBodyDependencies(BSPrim child) | 115 | public override void RestoreBodyDependencies(BSPrim child) |
127 | { | 116 | { |
128 | lock (m_linksetActivityLock) | 117 | // The Refresh operation will build any missing constraints. |
129 | { | ||
130 | if (IsRoot(child)) | ||
131 | { | ||
132 | DetailLog("{0},BSLinksetConstraint.RestoreBodyDependencies,restoreChildrenForRoot,rID={1},numChild={2}", | ||
133 | child.LocalID, LinksetRoot.LocalID, m_taintChildren.Count); | ||
134 | foreach (BSPhysObject bpo in m_taintChildren) | ||
135 | { | ||
136 | PhysicallyLinkAChildToRoot(LinksetRoot, bpo); | ||
137 | } | ||
138 | } | ||
139 | else | ||
140 | { | ||
141 | DetailLog("{0},BSLinksetConstraint.RestoreBodyDependencies,restoreSingleChild,rID={1},rBody={2},cID={3},cBody={4}", | ||
142 | LinksetRoot.LocalID, | ||
143 | LinksetRoot.LocalID, LinksetRoot.BSBody.ptr.ToString("X"), | ||
144 | child.LocalID, child.BSBody.ptr.ToString("X")); | ||
145 | PhysicallyLinkAChildToRoot(LinksetRoot, child); | ||
146 | } | ||
147 | } | ||
148 | } | 118 | } |
149 | 119 | ||
150 | // ================================================================ | 120 | // ================================================================ |
@@ -163,18 +133,7 @@ public sealed class BSLinksetConstraints : BSLinkset | |||
163 | 133 | ||
164 | DetailLog("{0},AddChildToLinkset,call,child={1}", LinksetRoot.LocalID, child.LocalID); | 134 | DetailLog("{0},AddChildToLinkset,call,child={1}", LinksetRoot.LocalID, child.LocalID); |
165 | 135 | ||
166 | PhysicsScene.TaintedObject("AddChildToLinkset", delegate() | 136 | // Cause constraints and assorted properties to be recomputed before the next simulation step. |
167 | { | ||
168 | DetailLog("{0},AddChildToLinkset,taint,rID={1},rBody={2},cID={3},cBody={4}", | ||
169 | rootx.LocalID, | ||
170 | rootx.LocalID, rootx.BSBody.ptr.ToString("X"), | ||
171 | childx.LocalID, childx.BSBody.ptr.ToString("X")); | ||
172 | // Since this is taint-time, the body and shape could have changed for the child | ||
173 | rootx.ForcePosition = rootx.Position; // DEBUG | ||
174 | childx.ForcePosition = childx.Position; // DEBUG | ||
175 | PhysicallyLinkAChildToRoot(rootx, childx); | ||
176 | m_taintChildren.Add(child); | ||
177 | }); | ||
178 | Refresh(LinksetRoot); | 137 | Refresh(LinksetRoot); |
179 | } | 138 | } |
180 | return; | 139 | return; |
@@ -207,7 +166,6 @@ public sealed class BSLinksetConstraints : BSLinkset | |||
207 | 166 | ||
208 | PhysicsScene.TaintedObject("RemoveChildFromLinkset", delegate() | 167 | PhysicsScene.TaintedObject("RemoveChildFromLinkset", delegate() |
209 | { | 168 | { |
210 | m_taintChildren.Remove(child); | ||
211 | PhysicallyUnlinkAChildFromRoot(rootx, childx); | 169 | PhysicallyUnlinkAChildFromRoot(rootx, childx); |
212 | }); | 170 | }); |
213 | // See that the linkset parameters are recomputed at the end of the taint time. | 171 | // See that the linkset parameters are recomputed at the end of the taint time. |
@@ -225,6 +183,12 @@ public sealed class BSLinksetConstraints : BSLinkset | |||
225 | // Called at taint time! | 183 | // Called at taint time! |
226 | private void PhysicallyLinkAChildToRoot(BSPhysObject rootPrim, BSPhysObject childPrim) | 184 | private void PhysicallyLinkAChildToRoot(BSPhysObject rootPrim, BSPhysObject childPrim) |
227 | { | 185 | { |
186 | // Don't build the constraint when asked. Put it off until just before the simulation step. | ||
187 | Refresh(rootPrim); | ||
188 | } | ||
189 | |||
190 | private BSConstraint BuildConstraint(BSPhysObject rootPrim, BSPhysObject childPrim) | ||
191 | { | ||
228 | // Zero motion for children so they don't interpolate | 192 | // Zero motion for children so they don't interpolate |
229 | childPrim.ZeroMotion(); | 193 | childPrim.ZeroMotion(); |
230 | 194 | ||
@@ -235,7 +199,7 @@ public sealed class BSLinksetConstraints : BSLinkset | |||
235 | // real world coordinate of midpoint between the two objects | 199 | // real world coordinate of midpoint between the two objects |
236 | OMV.Vector3 midPoint = rootPrim.Position + (childRelativePosition / 2); | 200 | OMV.Vector3 midPoint = rootPrim.Position + (childRelativePosition / 2); |
237 | 201 | ||
238 | DetailLog("{0},BSLinksetConstraint.PhysicallyLinkAChildToRoot,taint,root={1},rBody={2},child={3},cBody={4},rLoc={5},cLoc={6},midLoc={7}", | 202 | DetailLog("{0},BSLinksetConstraint.BuildConstraint,taint,root={1},rBody={2},child={3},cBody={4},rLoc={5},cLoc={6},midLoc={7}", |
239 | rootPrim.LocalID, | 203 | rootPrim.LocalID, |
240 | rootPrim.LocalID, rootPrim.BSBody.ptr.ToString("X"), | 204 | rootPrim.LocalID, rootPrim.BSBody.ptr.ToString("X"), |
241 | childPrim.LocalID, childPrim.BSBody.ptr.ToString("X"), | 205 | childPrim.LocalID, childPrim.BSBody.ptr.ToString("X"), |
@@ -297,6 +261,7 @@ public sealed class BSLinksetConstraints : BSLinkset | |||
297 | { | 261 | { |
298 | constrain.SetSolverIterations(PhysicsScene.Params.linkConstraintSolverIterations); | 262 | constrain.SetSolverIterations(PhysicsScene.Params.linkConstraintSolverIterations); |
299 | } | 263 | } |
264 | return constrain; | ||
300 | } | 265 | } |
301 | 266 | ||
302 | // Remove linkage between myself and a particular child | 267 | // Remove linkage between myself and a particular child |
@@ -337,56 +302,47 @@ public sealed class BSLinksetConstraints : BSLinkset | |||
337 | } | 302 | } |
338 | 303 | ||
339 | // Call each of the constraints that make up this linkset and recompute the | 304 | // Call each of the constraints that make up this linkset and recompute the |
340 | // various transforms and variables. Used when objects are added or removed | 305 | // various transforms and variables. Create constraints of not created yet. |
341 | // from a linkset to make sure the constraints know about the new mass and | 306 | // Called before the simulation step to make sure the constraint based linkset |
342 | // geometry. | 307 | // is all initialized. |
343 | // Must only be called at taint time!! | 308 | // Must only be called at taint time!! |
344 | private void RecomputeLinksetConstraintVariables() | 309 | private void RecomputeLinksetConstraints() |
345 | { | 310 | { |
346 | float linksetMass = LinksetMass; | 311 | float linksetMass = LinksetMass; |
347 | foreach (BSPhysObject child in m_taintChildren) | 312 | LinksetRoot.UpdatePhysicalMassProperties(linksetMass); |
313 | |||
314 | // For a multiple object linkset, set everybody's center of mass to the set's center of mass | ||
315 | OMV.Vector3 centerOfMass = ComputeLinksetCenterOfMass(); | ||
316 | BulletSimAPI.SetCenterOfMassByPosRot2(LinksetRoot.BSBody.ptr, centerOfMass, OMV.Quaternion.Identity); | ||
317 | |||
318 | // BulletSimAPI.SetCollisionFilterMask2(LinksetRoot.BSBody.ptr, | ||
319 | // (uint)CollisionFilterGroups.LinksetFilter, (uint)CollisionFilterGroups.LinksetMask); | ||
320 | DetailLog("{0},BSLinksetConstraint.RecomputeLinksetConstraints,setCenterOfMass,COM={1},rBody={2},linksetMass={3}", | ||
321 | LinksetRoot.LocalID, centerOfMass, LinksetRoot.BSBody.ptr.ToString("X"), linksetMass); | ||
322 | |||
323 | foreach (BSPhysObject child in m_children) | ||
348 | { | 324 | { |
325 | // A child in the linkset physically shows the mass of the whole linkset. | ||
326 | // This allows Bullet to apply enough force on the child to move the whole linkset. | ||
327 | // (Also do the mass stuff before recomputing the constraint so mass is not zero.) | ||
328 | BulletSimAPI.SetCenterOfMassByPosRot2(child.BSBody.ptr, centerOfMass, OMV.Quaternion.Identity); | ||
329 | child.UpdatePhysicalMassProperties(linksetMass); | ||
330 | |||
349 | BSConstraint constrain; | 331 | BSConstraint constrain; |
350 | if (PhysicsScene.Constraints.TryGetConstraint(LinksetRoot.BSBody, child.BSBody, out constrain)) | 332 | if (!PhysicsScene.Constraints.TryGetConstraint(LinksetRoot.BSBody, child.BSBody, out constrain)) |
351 | { | ||
352 | // DetailLog("{0},BSLinksetConstraint.RecomputeLinksetConstraintVariables,taint,child={1},mass={2},A={3},B={4}", | ||
353 | // LinksetRoot.LocalID, child.LocalID, linksetMass, constrain.Body1.ID, constrain.Body2.ID); | ||
354 | constrain.RecomputeConstraintVariables(linksetMass); | ||
355 | } | ||
356 | else | ||
357 | { | 333 | { |
358 | // Non-fatal error that happens when children are being added to the linkset but | 334 | // If constraint doesn't exist yet, create it. |
359 | // their constraints have not been created yet. | 335 | constrain = BuildConstraint(LinksetRoot, child); |
360 | break; | ||
361 | } | 336 | } |
362 | } | 337 | constrain.RecomputeConstraintVariables(linksetMass); |
363 | 338 | ||
364 | // If the whole linkset is not here, doesn't make sense to recompute linkset wide values | 339 | // DEBUG: see of inter-linkset collisions are causing problems |
365 | if (m_children.Count == m_taintChildren.Count) | 340 | // BulletSimAPI.SetCollisionFilterMask2(child.BSBody.ptr, |
366 | { | 341 | // (uint)CollisionFilterGroups.LinksetFilter, (uint)CollisionFilterGroups.LinksetMask); |
367 | // If this is a multiple object linkset, set everybody's center of mass to the set's center of mass | ||
368 | OMV.Vector3 centerOfMass = ComputeLinksetCenterOfMass(); | ||
369 | BulletSimAPI.SetCenterOfMassByPosRot2(LinksetRoot.BSBody.ptr, centerOfMass, OMV.Quaternion.Identity); | ||
370 | // BulletSimAPI.SetCollisionFilterMask2(LinksetRoot.BSBody.ptr, | ||
371 | // (uint)CollisionFilterGroups.LinksetFilter, (uint)CollisionFilterGroups.LinksetMask); | ||
372 | DetailLog("{0},BSLinksetConstraint.RecomputeLinksetConstraintVariables,setCenterOfMass,COM={1},rBody={2},linksetMass={3}", | ||
373 | LinksetRoot.LocalID, centerOfMass, LinksetRoot.BSBody.ptr.ToString("X"), linksetMass); | ||
374 | foreach (BSPhysObject child in m_taintChildren) | ||
375 | { | ||
376 | BulletSimAPI.SetCenterOfMassByPosRot2(child.BSBody.ptr, centerOfMass, OMV.Quaternion.Identity); | ||
377 | // A child in the linkset physically shows the mass of the whole linkset. | ||
378 | // This allows Bullet to apply enough force on the child to move the whole linkset. | ||
379 | child.UpdatePhysicalMassProperties(linksetMass); | ||
380 | // DEBUG: see of inter-linkset collisions are causing problems | ||
381 | // BulletSimAPI.SetCollisionFilterMask2(child.BSBody.ptr, | ||
382 | // (uint)CollisionFilterGroups.LinksetFilter, (uint)CollisionFilterGroups.LinksetMask); | ||
383 | } | ||
384 | // Also update the root's physical mass to the whole linkset | ||
385 | LinksetRoot.UpdatePhysicalMassProperties(linksetMass); | ||
386 | 342 | ||
387 | // BulletSimAPI.DumpAllInfo2(PhysicsScene.World.ptr); // DEBUG DEBUG DEBUG | 343 | // BulletSimAPI.DumpConstraint2(PhysicsScene.World.ptr, constrain.Constraint.ptr); // DEBUG DEBUG |
388 | } | 344 | } |
389 | return; | 345 | |
390 | } | 346 | } |
391 | } | 347 | } |
392 | } | 348 | } |