diff options
author | UbitUmarov | 2012-08-28 03:21:03 +0100 |
---|---|---|
committer | UbitUmarov | 2012-08-28 03:21:03 +0100 |
commit | ef6e007a4c7301dbd7a1a0392a07664e7034201b (patch) | |
tree | 090ee81b2784f6e3f21fe8e0df72c089d5ea9f5c /OpenSim/Region/Framework/Scenes/KeyframeMotion.cs | |
parent | fix incoerence btw KFM_TRANSLATION and ROTATION LSL constants and internal (diff) | |
download | opensim-SC_OLD-ef6e007a4c7301dbd7a1a0392a07664e7034201b.zip opensim-SC_OLD-ef6e007a4c7301dbd7a1a0392a07664e7034201b.tar.gz opensim-SC_OLD-ef6e007a4c7301dbd7a1a0392a07664e7034201b.tar.bz2 opensim-SC_OLD-ef6e007a4c7301dbd7a1a0392a07664e7034201b.tar.xz |
[possible still very broken] mess around keyframes. timer events
threads overlaps, some null objects exceptions, region crossing...
Diffstat (limited to 'OpenSim/Region/Framework/Scenes/KeyframeMotion.cs')
-rw-r--r-- | OpenSim/Region/Framework/Scenes/KeyframeMotion.cs | 368 |
1 files changed, 265 insertions, 103 deletions
diff --git a/OpenSim/Region/Framework/Scenes/KeyframeMotion.cs b/OpenSim/Region/Framework/Scenes/KeyframeMotion.cs index 6ee09b7..4e6425f 100644 --- a/OpenSim/Region/Framework/Scenes/KeyframeMotion.cs +++ b/OpenSim/Region/Framework/Scenes/KeyframeMotion.cs | |||
@@ -58,12 +58,31 @@ namespace OpenSim.Region.Framework.Scenes | |||
58 | private Vector3 m_serializedPosition; | 58 | private Vector3 m_serializedPosition; |
59 | 59 | ||
60 | private Keyframe m_currentFrame; | 60 | private Keyframe m_currentFrame; |
61 | |||
61 | private List<Keyframe> m_frames = new List<Keyframe>(); | 62 | private List<Keyframe> m_frames = new List<Keyframe>(); |
62 | 63 | ||
63 | private Keyframe[] m_keyframes; | 64 | private Keyframe[] m_keyframes; |
64 | 65 | ||
65 | [NonSerialized()] | 66 | [NonSerialized()] |
66 | protected Timer m_timer = new Timer(); | 67 | protected Timer m_timer = null; |
68 | |||
69 | // timer lock | ||
70 | [NonSerialized()] | ||
71 | private object m_onTimerLock; | ||
72 | |||
73 | // timer overrun detect | ||
74 | // prevents overlap or timer events threads frozen on the lock | ||
75 | [NonSerialized()] | ||
76 | private bool m_inOnTimer; | ||
77 | |||
78 | // skip timer events. | ||
79 | //timer.stop doesn't assure there aren't event threads still being fired | ||
80 | [NonSerialized()] | ||
81 | private bool m_skipOnTimer; | ||
82 | |||
83 | // retry position for cross fail | ||
84 | [NonSerialized()] | ||
85 | private Vector3 m_nextPosition; | ||
67 | 86 | ||
68 | [NonSerialized()] | 87 | [NonSerialized()] |
69 | private SceneObjectGroup m_group; | 88 | private SceneObjectGroup m_group; |
@@ -88,7 +107,7 @@ namespace OpenSim.Region.Framework.Scenes | |||
88 | { | 107 | { |
89 | set | 108 | set |
90 | { | 109 | { |
91 | if (value) | 110 | if (!value) |
92 | { | 111 | { |
93 | // Once we're let go, recompute positions | 112 | // Once we're let go, recompute positions |
94 | if (m_selected) | 113 | if (m_selected) |
@@ -100,7 +119,8 @@ namespace OpenSim.Region.Framework.Scenes | |||
100 | if (!m_selected) | 119 | if (!m_selected) |
101 | m_serializedPosition = m_group.AbsolutePosition; | 120 | m_serializedPosition = m_group.AbsolutePosition; |
102 | } | 121 | } |
103 | m_selected = value; } | 122 | m_selected = value; |
123 | } | ||
104 | } | 124 | } |
105 | 125 | ||
106 | public static KeyframeMotion FromData(SceneObjectGroup grp, Byte[] data) | 126 | public static KeyframeMotion FromData(SceneObjectGroup grp, Byte[] data) |
@@ -111,31 +131,65 @@ namespace OpenSim.Region.Framework.Scenes | |||
111 | 131 | ||
112 | KeyframeMotion newMotion = (KeyframeMotion)fmt.Deserialize(ms); | 132 | KeyframeMotion newMotion = (KeyframeMotion)fmt.Deserialize(ms); |
113 | 133 | ||
134 | /* | ||
135 | * create timer in start() | ||
136 | * this was creating unneeded timers | ||
137 | |||
114 | // This will be started when position is updated | 138 | // This will be started when position is updated |
139 | |||
115 | newMotion.m_timer = new Timer(); | 140 | newMotion.m_timer = new Timer(); |
116 | newMotion.m_timer.Interval = (int)timerInterval; | 141 | newMotion.m_timer.Interval = (int)timerInterval; |
117 | newMotion.m_timer.AutoReset = true; | 142 | newMotion.m_timer.AutoReset = true; |
118 | newMotion.m_timer.Elapsed += newMotion.OnTimer; | 143 | newMotion.m_timer.Elapsed += newMotion.OnTimer; |
144 | */ | ||
145 | newMotion.m_group = grp; | ||
119 | 146 | ||
147 | if (grp != null && grp.IsSelected) | ||
148 | newMotion.m_selected = true; | ||
149 | |||
150 | newMotion.m_onTimerLock = new object(); | ||
151 | newMotion.m_skipOnTimer = false; | ||
152 | newMotion.m_inOnTimer = false; | ||
120 | return newMotion; | 153 | return newMotion; |
121 | } | 154 | } |
122 | 155 | ||
123 | public void UpdateSceneObject(SceneObjectGroup grp) | 156 | public void UpdateSceneObject(SceneObjectGroup grp) |
124 | { | 157 | { |
125 | m_group = grp; | 158 | // lock (m_onTimerLock) |
126 | Vector3 offset = grp.AbsolutePosition - m_serializedPosition; | ||
127 | |||
128 | m_basePosition += offset; | ||
129 | m_currentFrame.Position += offset; | ||
130 | for (int i = 0 ; i < m_frames.Count ; i++) | ||
131 | { | 159 | { |
132 | Keyframe k = m_frames[i]; | 160 | m_skipOnTimer = true; |
133 | k.Position += offset; | 161 | if (m_timer != null) |
134 | m_frames[i] = k; | 162 | m_timer.Stop(); |
135 | } | 163 | |
164 | m_group = grp; | ||
165 | Vector3 grppos = grp.AbsolutePosition; | ||
166 | Vector3 offset = grppos - m_serializedPosition; | ||
167 | // avoid doing it more than once | ||
168 | // current this will happen draging a prim to other region | ||
169 | m_serializedPosition = grppos; | ||
170 | |||
171 | m_basePosition += offset; | ||
172 | m_currentFrame.Position += offset; | ||
173 | |||
174 | m_nextPosition += offset; | ||
175 | /* | ||
176 | for (int i = 0; i < m_frames.Count; i++) | ||
177 | { | ||
178 | Keyframe k = m_frames[i]; | ||
179 | k.Position += offset; | ||
180 | m_frames[i] = k; | ||
181 | } | ||
182 | */ | ||
183 | for (int i = 0; i < m_frames.Count; i++) | ||
184 | { | ||
185 | Keyframe k = m_frames[i]; | ||
186 | k.Position += offset; | ||
187 | m_frames[i]=k; | ||
188 | } | ||
136 | 189 | ||
137 | if (m_running) | 190 | if (m_running) |
138 | Start(); | 191 | Start(); |
192 | } | ||
139 | } | 193 | } |
140 | 194 | ||
141 | public KeyframeMotion(SceneObjectGroup grp, PlayMode mode, DataFormat data) | 195 | public KeyframeMotion(SceneObjectGroup grp, PlayMode mode, DataFormat data) |
@@ -143,13 +197,16 @@ namespace OpenSim.Region.Framework.Scenes | |||
143 | m_mode = mode; | 197 | m_mode = mode; |
144 | m_data = data; | 198 | m_data = data; |
145 | 199 | ||
200 | m_onTimerLock = new object(); | ||
201 | |||
146 | m_group = grp; | 202 | m_group = grp; |
147 | m_basePosition = grp.AbsolutePosition; | 203 | m_basePosition = grp.AbsolutePosition; |
148 | m_baseRotation = grp.GroupRotation; | 204 | m_baseRotation = grp.GroupRotation; |
149 | 205 | /* | |
150 | m_timer.Interval = (int)timerInterval; | 206 | m_timer.Interval = (int)timerInterval; |
151 | m_timer.AutoReset = true; | 207 | m_timer.AutoReset = true; |
152 | m_timer.Elapsed += OnTimer; | 208 | m_timer.Elapsed += OnTimer; |
209 | */ | ||
153 | } | 210 | } |
154 | 211 | ||
155 | public void SetKeyframes(Keyframe[] frames) | 212 | public void SetKeyframes(Keyframe[] frames) |
@@ -157,19 +214,63 @@ namespace OpenSim.Region.Framework.Scenes | |||
157 | m_keyframes = frames; | 214 | m_keyframes = frames; |
158 | } | 215 | } |
159 | 216 | ||
217 | public void Delete() | ||
218 | { | ||
219 | m_skipOnTimer = true; | ||
220 | m_frames.Clear(); | ||
221 | m_keyframes = null; | ||
222 | m_running = false; | ||
223 | |||
224 | if (m_timer == null) | ||
225 | return; | ||
226 | |||
227 | m_timer.Stop(); | ||
228 | m_timer.Elapsed -= OnTimer; | ||
229 | m_timer = null; | ||
230 | } | ||
231 | |||
160 | public void Start() | 232 | public void Start() |
161 | { | 233 | { |
162 | if (m_keyframes.Length > 0) | 234 | if (m_keyframes.Length > 0) |
235 | { | ||
236 | if (m_timer == null) | ||
237 | { | ||
238 | m_timer = new Timer(); | ||
239 | m_timer.Interval = (int)timerInterval; | ||
240 | m_timer.AutoReset = true; | ||
241 | m_timer.Elapsed += OnTimer; | ||
242 | } | ||
243 | |||
244 | m_skipOnTimer = false; | ||
245 | m_inOnTimer = false; | ||
246 | |||
163 | m_timer.Start(); | 247 | m_timer.Start(); |
164 | m_running = true; | 248 | m_running = true; |
249 | } | ||
250 | else | ||
251 | { | ||
252 | m_running = false; | ||
253 | m_skipOnTimer = true; | ||
254 | if (m_timer != null) | ||
255 | { | ||
256 | m_timer.Stop(); | ||
257 | m_timer.Elapsed -= OnTimer; | ||
258 | m_timer = null; | ||
259 | } | ||
260 | } | ||
165 | } | 261 | } |
166 | 262 | ||
167 | public void Stop() | 263 | public void Stop() |
168 | { | 264 | { |
265 | m_skipOnTimer = true; | ||
266 | |||
169 | // Failed object creation | 267 | // Failed object creation |
170 | if (m_timer == null) | 268 | if (m_timer == null) |
171 | return; | 269 | return; |
270 | |||
172 | m_timer.Stop(); | 271 | m_timer.Stop(); |
272 | m_timer.Elapsed -= OnTimer; | ||
273 | m_timer = null; | ||
173 | 274 | ||
174 | m_basePosition = m_group.AbsolutePosition; | 275 | m_basePosition = m_group.AbsolutePosition; |
175 | m_baseRotation = m_group.GroupRotation; | 276 | m_baseRotation = m_group.GroupRotation; |
@@ -184,6 +285,8 @@ namespace OpenSim.Region.Framework.Scenes | |||
184 | 285 | ||
185 | public void Pause() | 286 | public void Pause() |
186 | { | 287 | { |
288 | m_skipOnTimer = true; | ||
289 | |||
187 | m_group.RootPart.Velocity = Vector3.Zero; | 290 | m_group.RootPart.Velocity = Vector3.Zero; |
188 | m_group.RootPart.UpdateAngularVelocity(Vector3.Zero); | 291 | m_group.RootPart.UpdateAngularVelocity(Vector3.Zero); |
189 | m_group.SendGroupRootTerseUpdate(); | 292 | m_group.SendGroupRootTerseUpdate(); |
@@ -284,138 +387,197 @@ namespace OpenSim.Region.Framework.Scenes | |||
284 | 387 | ||
285 | protected void OnTimer(object sender, ElapsedEventArgs e) | 388 | protected void OnTimer(object sender, ElapsedEventArgs e) |
286 | { | 389 | { |
287 | if (m_frames.Count == 0) | 390 | if (m_inOnTimer) |
288 | { | ||
289 | GetNextList(); | ||
290 | |||
291 | if (m_frames.Count == 0) | ||
292 | { | ||
293 | Stop(); | ||
294 | return; | ||
295 | } | ||
296 | |||
297 | m_currentFrame = m_frames[0]; | ||
298 | } | ||
299 | |||
300 | if (m_selected) | ||
301 | { | 391 | { |
302 | if (m_group.RootPart.Velocity != Vector3.Zero) | 392 | m_log.Error("[KeyFrame]: timer overrun"); |
303 | { | ||
304 | m_group.RootPart.Velocity = Vector3.Zero; | ||
305 | m_group.SendGroupRootTerseUpdate(); | ||
306 | } | ||
307 | return; | 393 | return; |
308 | } | 394 | } |
309 | 395 | ||
310 | // Do the frame processing | 396 | // lock (m_onTimerLock) |
311 | double steps = (double)m_currentFrame.TimeMS / timerInterval; | ||
312 | float complete = ((float)m_currentFrame.TimeTotal - (float)m_currentFrame.TimeMS) / (float)m_currentFrame.TimeTotal; | ||
313 | |||
314 | if (steps <= 1.0) | ||
315 | { | 397 | { |
316 | m_currentFrame.TimeMS = 0; | 398 | if (m_skipOnTimer) |
317 | 399 | return; | |
318 | m_group.AbsolutePosition = (Vector3)m_currentFrame.Position; | ||
319 | m_group.UpdateGroupRotationR((Quaternion)m_currentFrame.Rotation); | ||
320 | } | ||
321 | else | ||
322 | { | ||
323 | Vector3 v = (Vector3)m_currentFrame.Position - m_group.AbsolutePosition; | ||
324 | Vector3 motionThisFrame = v / (float)steps; | ||
325 | v = v * 1000 / m_currentFrame.TimeMS; | ||
326 | |||
327 | bool update = false; | ||
328 | 400 | ||
329 | if (Vector3.Mag(motionThisFrame) >= 0.05f) | 401 | m_inOnTimer = true; |
402 | try | ||
330 | { | 403 | { |
331 | m_group.AbsolutePosition += motionThisFrame; | 404 | if (m_frames.Count == 0) |
332 | m_group.RootPart.Velocity = v; | 405 | { |
333 | update = true; | 406 | GetNextList(); |
334 | } | ||
335 | 407 | ||
336 | if ((Quaternion)m_currentFrame.Rotation != m_group.GroupRotation) | 408 | if (m_frames.Count == 0) |
337 | { | 409 | { |
338 | Quaternion current = m_group.GroupRotation; | 410 | Stop(); |
411 | m_inOnTimer = false; | ||
412 | return; | ||
413 | } | ||
339 | 414 | ||
340 | Quaternion step = Quaternion.Slerp(m_currentFrame.StartRotation, (Quaternion)m_currentFrame.Rotation, complete); | 415 | m_currentFrame = m_frames[0]; |
416 | } | ||
341 | 417 | ||
342 | float angle = 0; | 418 | if (m_selected) |
419 | { | ||
420 | if (m_group.RootPart.Velocity != Vector3.Zero) | ||
421 | { | ||
422 | m_group.RootPart.Velocity = Vector3.Zero; | ||
423 | m_group.SendGroupRootTerseUpdate(); | ||
424 | } | ||
425 | m_inOnTimer = false; | ||
426 | return; | ||
427 | } | ||
343 | 428 | ||
344 | float aa = current.X * current.X + current.Y * current.Y + current.Z * current.Z + current.W * current.W; | 429 | // Do the frame processing |
345 | float bb = step.X * step.X + step.Y * step.Y + step.Z * step.Z + step.W * step.W; | 430 | double steps = (double)m_currentFrame.TimeMS / timerInterval; |
346 | float aa_bb = aa * bb; | 431 | float complete = ((float)m_currentFrame.TimeTotal - (float)m_currentFrame.TimeMS) / (float)m_currentFrame.TimeTotal; |
347 | 432 | ||
348 | if (aa_bb == 0) | 433 | if (steps <= 1.0) |
349 | { | 434 | { |
350 | angle = 0; | 435 | m_currentFrame.TimeMS = 0; |
436 | |||
437 | // m_group.AbsolutePosition = (Vector3)m_currentFrame.Position; | ||
438 | m_nextPosition = (Vector3)m_currentFrame.Position; | ||
439 | m_group.AbsolutePosition = m_nextPosition; | ||
440 | |||
441 | m_group.UpdateGroupRotationR((Quaternion)m_currentFrame.Rotation); | ||
351 | } | 442 | } |
352 | else | 443 | else |
353 | { | 444 | { |
354 | float ab = current.X * step.X + | 445 | Vector3 v = (Vector3)m_currentFrame.Position - m_group.AbsolutePosition; |
355 | current.Y * step.Y + | 446 | Vector3 motionThisFrame = v / (float)steps; |
356 | current.Z * step.Z + | 447 | v = v * 1000 / m_currentFrame.TimeMS; |
357 | current.W * step.W; | ||
358 | float q = (ab * ab) / aa_bb; | ||
359 | 448 | ||
360 | if (q > 1.0f) | 449 | bool update = false; |
450 | |||
451 | if (Vector3.Mag(motionThisFrame) >= 0.05f) | ||
361 | { | 452 | { |
362 | angle = 0; | 453 | // m_group.AbsolutePosition += motionThisFrame; |
454 | m_nextPosition = m_group.AbsolutePosition + motionThisFrame; | ||
455 | m_group.AbsolutePosition = m_nextPosition; | ||
456 | |||
457 | m_group.RootPart.Velocity = v; | ||
458 | update = true; | ||
363 | } | 459 | } |
364 | else | 460 | |
461 | if ((Quaternion)m_currentFrame.Rotation != m_group.GroupRotation) | ||
365 | { | 462 | { |
366 | angle = (float)Math.Acos(2 * q - 1); | 463 | Quaternion current = m_group.GroupRotation; |
464 | |||
465 | Quaternion step = Quaternion.Slerp(m_currentFrame.StartRotation, (Quaternion)m_currentFrame.Rotation, complete); | ||
466 | |||
467 | float angle = 0; | ||
468 | |||
469 | float aa = current.X * current.X + current.Y * current.Y + current.Z * current.Z + current.W * current.W; | ||
470 | float bb = step.X * step.X + step.Y * step.Y + step.Z * step.Z + step.W * step.W; | ||
471 | float aa_bb = aa * bb; | ||
472 | |||
473 | if (aa_bb == 0) | ||
474 | { | ||
475 | angle = 0; | ||
476 | } | ||
477 | else | ||
478 | { | ||
479 | float ab = current.X * step.X + | ||
480 | current.Y * step.Y + | ||
481 | current.Z * step.Z + | ||
482 | current.W * step.W; | ||
483 | float q = (ab * ab) / aa_bb; | ||
484 | |||
485 | if (q > 1.0f) | ||
486 | { | ||
487 | angle = 0; | ||
488 | } | ||
489 | else | ||
490 | { | ||
491 | angle = (float)Math.Acos(2 * q - 1); | ||
492 | } | ||
493 | } | ||
494 | |||
495 | if (angle > 0.01f) | ||
496 | { | ||
497 | m_group.UpdateGroupRotationR(step); | ||
498 | //m_group.RootPart.UpdateAngularVelocity(m_currentFrame.AngularVelocity / 2); | ||
499 | update = true; | ||
500 | } | ||
367 | } | 501 | } |
502 | |||
503 | if (update) | ||
504 | m_group.SendGroupRootTerseUpdate(); | ||
368 | } | 505 | } |
369 | 506 | ||
370 | if (angle > 0.01f) | 507 | m_currentFrame.TimeMS -= (int)timerInterval; |
508 | |||
509 | if (m_currentFrame.TimeMS <= 0) | ||
371 | { | 510 | { |
372 | m_group.UpdateGroupRotationR(step); | 511 | m_group.RootPart.Velocity = Vector3.Zero; |
373 | //m_group.RootPart.UpdateAngularVelocity(m_currentFrame.AngularVelocity / 2); | 512 | m_group.RootPart.UpdateAngularVelocity(Vector3.Zero); |
374 | update = true; | 513 | m_group.SendGroupRootTerseUpdate(); |
514 | |||
515 | m_frames.RemoveAt(0); | ||
516 | if (m_frames.Count > 0) | ||
517 | m_currentFrame = m_frames[0]; | ||
375 | } | 518 | } |
376 | } | 519 | } |
377 | 520 | finally | |
378 | if (update) | 521 | { |
379 | m_group.SendGroupRootTerseUpdate(); | 522 | m_inOnTimer = false; |
523 | } | ||
380 | } | 524 | } |
525 | } | ||
381 | 526 | ||
382 | m_currentFrame.TimeMS -= (int)timerInterval; | 527 | public Byte[] Serialize(bool StopMoveTimer) |
383 | 528 | { | |
384 | if (m_currentFrame.TimeMS <= 0) | 529 | MemoryStream ms = new MemoryStream(); |
530 | if (StopMoveTimer && m_timer != null) | ||
385 | { | 531 | { |
386 | m_group.RootPart.Velocity = Vector3.Zero; | 532 | m_skipOnTimer = true; |
387 | m_group.RootPart.UpdateAngularVelocity(Vector3.Zero); | 533 | m_timer.Stop(); |
388 | m_group.SendGroupRootTerseUpdate(); | 534 | } |
389 | 535 | ||
390 | m_frames.RemoveAt(0); | 536 | // lock (m_onTimerLock) |
391 | if (m_frames.Count > 0) | 537 | { |
392 | m_currentFrame = m_frames[0]; | 538 | BinaryFormatter fmt = new BinaryFormatter(); |
539 | SceneObjectGroup tmp = m_group; | ||
540 | m_group = null; | ||
541 | if(!m_selected) | ||
542 | m_serializedPosition = tmp.AbsolutePosition; | ||
543 | fmt.Serialize(ms, this); | ||
544 | m_group = tmp; | ||
545 | return ms.ToArray(); | ||
393 | } | 546 | } |
394 | } | 547 | } |
395 | 548 | ||
396 | public Byte[] Serialize() | 549 | public void StartCrossingCheck() |
397 | { | 550 | { |
398 | MemoryStream ms = new MemoryStream(); | 551 | m_skipOnTimer = true; |
399 | m_timer.Stop(); | 552 | if (m_timer != null) |
553 | m_timer.Stop(); | ||
400 | 554 | ||
401 | BinaryFormatter fmt = new BinaryFormatter(); | 555 | if (m_group.RootPart.Velocity != Vector3.Zero) |
402 | SceneObjectGroup tmp = m_group; | 556 | { |
403 | m_group = null; | 557 | m_group.RootPart.Velocity = Vector3.Zero; |
404 | m_serializedPosition = tmp.AbsolutePosition; | 558 | m_group.SendGroupRootTerseUpdate(); |
405 | fmt.Serialize(ms, this); | 559 | } |
406 | m_group = tmp; | ||
407 | return ms.ToArray(); | ||
408 | } | 560 | } |
409 | 561 | ||
410 | public void CrossingFailure() | 562 | public void CrossingFailure() |
411 | { | 563 | { |
412 | // The serialization has stopped the timer, so let's wait a moment | 564 | // The serialization has stopped the timer, so let's wait a moment |
413 | // then retry the crossing. We'll get back here if it fails. | 565 | // then retry the crossing. We'll get back here if it fails. |
566 | // if it is a open border there is no serialization | ||
567 | // so make sure timer is actually stopped | ||
568 | |||
569 | m_group.RootPart.Velocity = Vector3.Zero; | ||
570 | m_group.SendGroupRootTerseUpdate(); | ||
571 | |||
414 | Util.FireAndForget(delegate (object x) | 572 | Util.FireAndForget(delegate (object x) |
415 | { | 573 | { |
416 | Thread.Sleep(60000); | 574 | Thread.Sleep(60000); |
417 | if (m_running) | 575 | if (m_running) |
576 | { | ||
577 | m_skipOnTimer = false; | ||
418 | m_timer.Start(); | 578 | m_timer.Start(); |
579 | m_group.AbsolutePosition = m_nextPosition; | ||
580 | } | ||
419 | }); | 581 | }); |
420 | } | 582 | } |
421 | } | 583 | } |