File indexing completed on 2025-08-03 03:49:55
0001 /* 0002 * Copyright (c) 2007-2009 Erin Catto http://www.gphysics.com 0003 * 0004 * This software is provided 'as-is', without any express or implied 0005 * warranty. In no event will the authors be held liable for any damages 0006 * arising from the use of this software. 0007 * Permission is granted to anyone to use this software for any purpose, 0008 * including commercial applications, and to alter it and redistribute it 0009 * freely, subject to the following restrictions: 0010 * 1. The origin of this software must not be misrepresented; you must not 0011 * claim that you wrote the original software. If you use this software 0012 * in a product, an acknowledgment in the product documentation would be 0013 * appreciated but is not required. 0014 * 2. Altered source versions must be plainly marked as such, and must not be 0015 * misrepresented as being the original software. 0016 * 3. This notice may not be removed or altered from any source distribution. 0017 */ 0018 0019 #include <Box2D/Collision/b2Collision.h> 0020 #include <Box2D/Collision/Shapes/b2CircleShape.h> 0021 #include <Box2D/Collision/Shapes/b2EdgeShape.h> 0022 #include <Box2D/Collision/Shapes/b2PolygonShape.h> 0023 0024 enum b2EdgeType 0025 { 0026 b2_isolated, 0027 b2_concave, 0028 b2_flat, 0029 b2_convex 0030 }; 0031 0032 // Compute contact points for edge versus circle. 0033 // This accounts for edge connectivity. 0034 void b2CollideEdgeAndCircle(b2Manifold* manifold, 0035 const b2EdgeShape* edgeA, const b2Transform& xfA, 0036 const b2CircleShape* circleB, const b2Transform& xfB) 0037 { 0038 manifold->pointCount = 0; 0039 0040 // Compute circle in frame of edge 0041 b2Vec2 Q = b2MulT(xfA, b2Mul(xfB, circleB->m_p)); 0042 0043 b2Vec2 A = edgeA->m_vertex1, B = edgeA->m_vertex2; 0044 b2Vec2 e = B - A; 0045 0046 // Barycentric coordinates 0047 qreal u = b2Dot(e, B - Q); 0048 qreal v = b2Dot(e, Q - A); 0049 0050 qreal radius = edgeA->m_radius + circleB->m_radius; 0051 0052 b2ContactFeature cf; 0053 cf.indexB = 0; 0054 cf.typeB = b2ContactFeature::e_vertex; 0055 0056 // Region A 0057 if (v <= 0.0f) 0058 { 0059 b2Vec2 P = A; 0060 b2Vec2 d = Q - P; 0061 qreal dd = b2Dot(d, d); 0062 if (dd > radius * radius) 0063 { 0064 return; 0065 } 0066 0067 // Is there an edge connected to A? 0068 if (edgeA->m_hasVertex0) 0069 { 0070 b2Vec2 A1 = edgeA->m_vertex0; 0071 b2Vec2 B1 = A; 0072 b2Vec2 e1 = B1 - A1; 0073 qreal u1 = b2Dot(e1, B1 - Q); 0074 0075 // Is the circle in Region AB of the previous edge? 0076 if (u1 > 0.0f) 0077 { 0078 return; 0079 } 0080 } 0081 0082 cf.indexA = 0; 0083 cf.typeA = b2ContactFeature::e_vertex; 0084 manifold->pointCount = 1; 0085 manifold->type = b2Manifold::e_circles; 0086 manifold->localNormal.SetZero(); 0087 manifold->localPoint = P; 0088 manifold->points[0].id.key = 0; 0089 manifold->points[0].id.cf = cf; 0090 manifold->points[0].localPoint = circleB->m_p; 0091 return; 0092 } 0093 0094 // Region B 0095 if (u <= 0.0f) 0096 { 0097 b2Vec2 P = B; 0098 b2Vec2 d = Q - P; 0099 qreal dd = b2Dot(d, d); 0100 if (dd > radius * radius) 0101 { 0102 return; 0103 } 0104 0105 // Is there an edge connected to B? 0106 if (edgeA->m_hasVertex3) 0107 { 0108 b2Vec2 B2 = edgeA->m_vertex3; 0109 b2Vec2 A2 = B; 0110 b2Vec2 e2 = B2 - A2; 0111 qreal v2 = b2Dot(e2, Q - A2); 0112 0113 // Is the circle in Region AB of the next edge? 0114 if (v2 > 0.0f) 0115 { 0116 return; 0117 } 0118 } 0119 0120 cf.indexA = 1; 0121 cf.typeA = b2ContactFeature::e_vertex; 0122 manifold->pointCount = 1; 0123 manifold->type = b2Manifold::e_circles; 0124 manifold->localNormal.SetZero(); 0125 manifold->localPoint = P; 0126 manifold->points[0].id.key = 0; 0127 manifold->points[0].id.cf = cf; 0128 manifold->points[0].localPoint = circleB->m_p; 0129 return; 0130 } 0131 0132 // Region AB 0133 qreal den = b2Dot(e, e); 0134 b2Assert(den > 0.0f); 0135 b2Vec2 P = (1.0f / den) * (u * A + v * B); 0136 b2Vec2 d = Q - P; 0137 qreal dd = b2Dot(d, d); 0138 if (dd > radius * radius) 0139 { 0140 return; 0141 } 0142 0143 b2Vec2 n(-e.y, e.x); 0144 if (b2Dot(n, Q - A) < 0.0f) 0145 { 0146 n.Set(-n.x, -n.y); 0147 } 0148 n.Normalize(); 0149 0150 cf.indexA = 0; 0151 cf.typeA = b2ContactFeature::e_face; 0152 manifold->pointCount = 1; 0153 manifold->type = b2Manifold::e_faceA; 0154 manifold->localNormal = n; 0155 manifold->localPoint = A; 0156 manifold->points[0].id.key = 0; 0157 manifold->points[0].id.cf = cf; 0158 manifold->points[0].localPoint = circleB->m_p; 0159 } 0160 0161 struct b2EPAxis 0162 { 0163 enum Type 0164 { 0165 e_unknown, 0166 e_edgeA, 0167 e_edgeB 0168 }; 0169 0170 Type type; 0171 int32 index; 0172 qreal separation; 0173 }; 0174 0175 // Edge shape plus more stuff. 0176 struct b2FatEdge 0177 { 0178 b2Vec2 v0, v1, v2, v3; 0179 b2Vec2 normal; 0180 bool hasVertex0, hasVertex3; 0181 }; 0182 0183 // This lets us treat an edge shape and a polygon in the same 0184 // way in the SAT collider. 0185 struct b2EPProxy 0186 { 0187 b2Vec2 vertices[b2_maxPolygonVertices]; 0188 b2Vec2 normals[b2_maxPolygonVertices]; 0189 b2Vec2 centroid; 0190 int32 count; 0191 }; 0192 0193 // This class collides and edge and a polygon, taking into account edge adjacency. 0194 struct b2EPCollider 0195 { 0196 b2EPCollider(const b2EdgeShape* edgeA, const b2Transform& xfA, 0197 const b2PolygonShape* polygonB_in, const b2Transform& xfB); 0198 0199 void Collide(b2Manifold* manifold); 0200 0201 void ComputeAdjacency(); 0202 b2EPAxis ComputeEdgeSeparation(); 0203 b2EPAxis ComputePolygonSeparation(); 0204 void FindIncidentEdge(b2ClipVertex c[2], const b2EPProxy* proxy1, int32 edge1, const b2EPProxy* proxy2); 0205 0206 b2FatEdge m_edgeA; 0207 0208 b2EPProxy m_proxyA, m_proxyB; 0209 0210 b2Transform m_xf; 0211 b2Vec2 m_normal0, m_normal2; 0212 b2Vec2 m_limit11, m_limit12; 0213 b2Vec2 m_limit21, m_limit22; 0214 qreal m_radius; 0215 }; 0216 0217 b2EPCollider::b2EPCollider(const b2EdgeShape* edgeA, const b2Transform& xfA, 0218 const b2PolygonShape* polygonB, const b2Transform& xfB) 0219 { 0220 m_xf = b2MulT(xfA, xfB); 0221 0222 // Edge geometry 0223 m_edgeA.v0 = edgeA->m_vertex0; 0224 m_edgeA.v1 = edgeA->m_vertex1; 0225 m_edgeA.v2 = edgeA->m_vertex2; 0226 m_edgeA.v3 = edgeA->m_vertex3; 0227 b2Vec2 e = m_edgeA.v2 - m_edgeA.v1; 0228 0229 // Normal points outwards in CCW order. 0230 m_edgeA.normal.Set(e.y, -e.x); 0231 m_edgeA.normal.Normalize(); 0232 m_edgeA.hasVertex0 = edgeA->m_hasVertex0; 0233 m_edgeA.hasVertex3 = edgeA->m_hasVertex3; 0234 0235 // Proxy for edge 0236 m_proxyA.vertices[0] = m_edgeA.v1; 0237 m_proxyA.vertices[1] = m_edgeA.v2; 0238 m_proxyA.normals[0] = m_edgeA.normal; 0239 m_proxyA.normals[1] = -m_edgeA.normal; 0240 m_proxyA.centroid = 0.5f * (m_edgeA.v1 + m_edgeA.v2); 0241 m_proxyA.count = 2; 0242 0243 // Proxy for polygon 0244 m_proxyB.count = polygonB->m_vertexCount; 0245 m_proxyB.centroid = b2Mul(m_xf, polygonB->m_centroid); 0246 for (int32 i = 0; i < polygonB->m_vertexCount; ++i) 0247 { 0248 m_proxyB.vertices[i] = b2Mul(m_xf, polygonB->m_vertices[i]); 0249 m_proxyB.normals[i] = b2Mul(m_xf.R, polygonB->m_normals[i]); 0250 } 0251 0252 m_radius = 2.0f * b2_polygonRadius; 0253 0254 m_limit11.SetZero(); 0255 m_limit12.SetZero(); 0256 m_limit21.SetZero(); 0257 m_limit22.SetZero(); 0258 } 0259 0260 // Collide an edge and polygon. This uses the SAT and clipping to produce up to 2 contact points. 0261 // Edge adjacency is handle to produce locally valid contact points and normals. This is intended 0262 // to allow the polygon to slide smoothly over an edge chain. 0263 // 0264 // Algorithm 0265 // 1. Classify front-side or back-side collision with edge. 0266 // 2. Compute separation 0267 // 3. Process adjacent edges 0268 // 4. Classify adjacent edge as convex, flat, null, or concave 0269 // 5. Skip null or concave edges. Concave edges get a separate manifold. 0270 // 6. If the edge is flat, compute contact points as normal. Discard boundary points. 0271 // 7. If the edge is convex, compute it's separation. 0272 // 8. Use the minimum separation of up to three edges. If the minimum separation 0273 // is not the primary edge, return. 0274 // 9. If the minimum separation is the primary edge, compute the contact points and return. 0275 void b2EPCollider::Collide(b2Manifold* manifold) 0276 { 0277 manifold->pointCount = 0; 0278 0279 ComputeAdjacency(); 0280 0281 b2EPAxis edgeAxis = ComputeEdgeSeparation(); 0282 0283 // If no valid normal can be found than this edge should not collide. 0284 // This can happen on the middle edge of a 3-edge zig-zag chain. 0285 if (edgeAxis.type == b2EPAxis::e_unknown) 0286 { 0287 return; 0288 } 0289 0290 if (edgeAxis.separation > m_radius) 0291 { 0292 return; 0293 } 0294 0295 b2EPAxis polygonAxis = ComputePolygonSeparation(); 0296 if (polygonAxis.type != b2EPAxis::e_unknown && polygonAxis.separation > m_radius) 0297 { 0298 return; 0299 } 0300 0301 // Use hysteresis for jitter reduction. 0302 const qreal k_relativeTol = 0.98f; 0303 const qreal k_absoluteTol = 0.001f; 0304 0305 b2EPAxis primaryAxis; 0306 if (polygonAxis.type == b2EPAxis::e_unknown) 0307 { 0308 primaryAxis = edgeAxis; 0309 } 0310 else if (polygonAxis.separation > k_relativeTol * edgeAxis.separation + k_absoluteTol) 0311 { 0312 primaryAxis = polygonAxis; 0313 } 0314 else 0315 { 0316 primaryAxis = edgeAxis; 0317 } 0318 0319 b2EPProxy* proxy1; 0320 b2EPProxy* proxy2; 0321 b2ClipVertex incidentEdge[2]; 0322 if (primaryAxis.type == b2EPAxis::e_edgeA) 0323 { 0324 proxy1 = &m_proxyA; 0325 proxy2 = &m_proxyB; 0326 manifold->type = b2Manifold::e_faceA; 0327 } 0328 else 0329 { 0330 proxy1 = &m_proxyB; 0331 proxy2 = &m_proxyA; 0332 manifold->type = b2Manifold::e_faceB; 0333 } 0334 0335 int32 edge1 = primaryAxis.index; 0336 0337 FindIncidentEdge(incidentEdge, proxy1, primaryAxis.index, proxy2); 0338 int32 count1 = proxy1->count; 0339 const b2Vec2* vertices1 = proxy1->vertices; 0340 0341 int32 iv1 = edge1; 0342 int32 iv2 = edge1 + 1 < count1 ? edge1 + 1 : 0; 0343 0344 b2Vec2 v11 = vertices1[iv1]; 0345 b2Vec2 v12 = vertices1[iv2]; 0346 0347 b2Vec2 tangent = v12 - v11; 0348 tangent.Normalize(); 0349 0350 b2Vec2 normal = b2Cross(tangent, 1.0f); 0351 b2Vec2 planePoint = 0.5f * (v11 + v12); 0352 0353 // Face offset. 0354 qreal frontOffset = b2Dot(normal, v11); 0355 0356 // Side offsets, extended by polytope skin thickness. 0357 qreal sideOffset1 = -b2Dot(tangent, v11) + m_radius; 0358 qreal sideOffset2 = b2Dot(tangent, v12) + m_radius; 0359 0360 // Clip incident edge against extruded edge1 side edges. 0361 b2ClipVertex clipPoints1[2]; 0362 b2ClipVertex clipPoints2[2]; 0363 int np; 0364 0365 // Clip to box side 1 0366 np = b2ClipSegmentToLine(clipPoints1, incidentEdge, -tangent, sideOffset1, iv1); 0367 0368 if (np < b2_maxManifoldPoints) 0369 { 0370 return; 0371 } 0372 0373 // Clip to negative box side 1 0374 np = b2ClipSegmentToLine(clipPoints2, clipPoints1, tangent, sideOffset2, iv2); 0375 0376 if (np < b2_maxManifoldPoints) 0377 { 0378 return; 0379 } 0380 0381 // Now clipPoints2 contains the clipped points. 0382 if (primaryAxis.type == b2EPAxis::e_edgeA) 0383 { 0384 manifold->localNormal = normal; 0385 manifold->localPoint = planePoint; 0386 } 0387 else 0388 { 0389 manifold->localNormal = b2MulT(m_xf.R, normal); 0390 manifold->localPoint = b2MulT(m_xf, planePoint); 0391 } 0392 0393 int32 pointCount = 0; 0394 for (int32 i = 0; i < b2_maxManifoldPoints; ++i) 0395 { 0396 qreal separation; 0397 0398 separation = b2Dot(normal, clipPoints2[i].v) - frontOffset; 0399 0400 if (separation <= m_radius) 0401 { 0402 b2ManifoldPoint* cp = manifold->points + pointCount; 0403 0404 if (primaryAxis.type == b2EPAxis::e_edgeA) 0405 { 0406 cp->localPoint = b2MulT(m_xf, clipPoints2[i].v); 0407 cp->id = clipPoints2[i].id; 0408 } 0409 else 0410 { 0411 cp->localPoint = clipPoints2[i].v; 0412 cp->id.cf.typeA = clipPoints2[i].id.cf.typeB; 0413 cp->id.cf.typeB = clipPoints2[i].id.cf.typeA; 0414 cp->id.cf.indexA = clipPoints2[i].id.cf.indexB; 0415 cp->id.cf.indexB = clipPoints2[i].id.cf.indexA; 0416 } 0417 0418 ++pointCount; 0419 } 0420 } 0421 0422 manifold->pointCount = pointCount; 0423 } 0424 0425 // Compute allowable normal ranges based on adjacency. 0426 // A normal n is allowable iff: 0427 // cross(n, n1) >= 0.0f && cross(n2, n) >= 0.0f 0428 // n points from A to B (edge to polygon) 0429 void b2EPCollider::ComputeAdjacency() 0430 { 0431 b2Vec2 v0 = m_edgeA.v0; 0432 b2Vec2 v1 = m_edgeA.v1; 0433 b2Vec2 v2 = m_edgeA.v2; 0434 b2Vec2 v3 = m_edgeA.v3; 0435 0436 // Determine allowable the normal regions based on adjacency. 0437 // Note: it may be possible that no normal is admissable. 0438 b2Vec2 centerB = m_proxyB.centroid; 0439 if (m_edgeA.hasVertex0) 0440 { 0441 b2Vec2 e0 = v1 - v0; 0442 b2Vec2 e1 = v2 - v1; 0443 b2Vec2 n0(e0.y, -e0.x); 0444 b2Vec2 n1(e1.y, -e1.x); 0445 n0.Normalize(); 0446 n1.Normalize(); 0447 0448 bool convex = b2Cross(n0, n1) >= 0.0f; 0449 bool front0 = b2Dot(n0, centerB - v0) >= 0.0f; 0450 bool front1 = b2Dot(n1, centerB - v1) >= 0.0f; 0451 0452 if (convex) 0453 { 0454 if (front0 || front1) 0455 { 0456 m_limit11 = n1; 0457 m_limit12 = n0; 0458 } 0459 else 0460 { 0461 m_limit11 = -n1; 0462 m_limit12 = -n0; 0463 } 0464 } 0465 else 0466 { 0467 if (front0 && front1) 0468 { 0469 m_limit11 = n0; 0470 m_limit12 = n1; 0471 } 0472 else 0473 { 0474 m_limit11 = -n0; 0475 m_limit12 = -n1; 0476 } 0477 } 0478 } 0479 else 0480 { 0481 m_limit11.SetZero(); 0482 m_limit12.SetZero(); 0483 } 0484 0485 if (m_edgeA.hasVertex3) 0486 { 0487 b2Vec2 e1 = v2 - v1; 0488 b2Vec2 e2 = v3 - v2; 0489 b2Vec2 n1(e1.y, -e1.x); 0490 b2Vec2 n2(e2.y, -e2.x); 0491 n1.Normalize(); 0492 n2.Normalize(); 0493 0494 bool convex = b2Cross(n1, n2) >= 0.0f; 0495 bool front1 = b2Dot(n1, centerB - v1) >= 0.0f; 0496 bool front2 = b2Dot(n2, centerB - v2) >= 0.0f; 0497 0498 if (convex) 0499 { 0500 if (front1 || front2) 0501 { 0502 m_limit21 = n2; 0503 m_limit22 = n1; 0504 } 0505 else 0506 { 0507 m_limit21 = -n2; 0508 m_limit22 = -n1; 0509 } 0510 } 0511 else 0512 { 0513 if (front1 && front2) 0514 { 0515 m_limit21 = n1; 0516 m_limit22 = n2; 0517 } 0518 else 0519 { 0520 m_limit21 = -n1; 0521 m_limit22 = -n2; 0522 } 0523 } 0524 } 0525 else 0526 { 0527 m_limit21.SetZero(); 0528 m_limit22.SetZero(); 0529 } 0530 } 0531 0532 b2EPAxis b2EPCollider::ComputeEdgeSeparation() 0533 { 0534 // EdgeA separation 0535 b2EPAxis bestAxis; 0536 bestAxis.type = b2EPAxis::e_unknown; 0537 bestAxis.index = -1; 0538 bestAxis.separation = -FLT_MAX; 0539 b2Vec2 normals[2] = {m_edgeA.normal, -m_edgeA.normal}; 0540 0541 for (int32 i = 0; i < 2; ++i) 0542 { 0543 b2Vec2 n = normals[i]; 0544 0545 // Adjacency 0546 bool valid1 = b2Cross(n, m_limit11) >= -b2_angularSlop && b2Cross(m_limit12, n) >= -b2_angularSlop; 0547 bool valid2 = b2Cross(n, m_limit21) >= -b2_angularSlop && b2Cross(m_limit22, n) >= -b2_angularSlop; 0548 0549 if (valid1 == false || valid2 == false) 0550 { 0551 continue; 0552 } 0553 0554 b2EPAxis axis; 0555 axis.type = b2EPAxis::e_edgeA; 0556 axis.index = i; 0557 axis.separation = FLT_MAX; 0558 0559 for (int32 j = 0; j < m_proxyB.count; ++j) 0560 { 0561 qreal s = b2Dot(n, m_proxyB.vertices[j] - m_edgeA.v1); 0562 if (s < axis.separation) 0563 { 0564 axis.separation = s; 0565 } 0566 } 0567 0568 if (axis.separation > m_radius) 0569 { 0570 return axis; 0571 } 0572 0573 if (axis.separation > bestAxis.separation) 0574 { 0575 bestAxis = axis; 0576 } 0577 } 0578 0579 return bestAxis; 0580 } 0581 0582 b2EPAxis b2EPCollider::ComputePolygonSeparation() 0583 { 0584 b2EPAxis axis; 0585 axis.type = b2EPAxis::e_unknown; 0586 axis.index = -1; 0587 axis.separation = -FLT_MAX; 0588 for (int32 i = 0; i < m_proxyB.count; ++i) 0589 { 0590 b2Vec2 n = -m_proxyB.normals[i]; 0591 0592 // Adjacency 0593 bool valid1 = b2Cross(n, m_limit11) >= -b2_angularSlop && b2Cross(m_limit12, n) >= -b2_angularSlop; 0594 bool valid2 = b2Cross(n, m_limit21) >= -b2_angularSlop && b2Cross(m_limit22, n) >= -b2_angularSlop; 0595 0596 if (valid1 == false && valid2 == false) 0597 { 0598 continue; 0599 } 0600 0601 qreal s1 = b2Dot(n, m_proxyB.vertices[i] - m_edgeA.v1); 0602 qreal s2 = b2Dot(n, m_proxyB.vertices[i] - m_edgeA.v2); 0603 qreal s = b2Min(s1, s2); 0604 0605 if (s > m_radius) 0606 { 0607 axis.type = b2EPAxis::e_edgeB; 0608 axis.index = i; 0609 axis.separation = s; 0610 } 0611 0612 if (s > axis.separation) 0613 { 0614 axis.type = b2EPAxis::e_edgeB; 0615 axis.index = i; 0616 axis.separation = s; 0617 } 0618 } 0619 0620 return axis; 0621 } 0622 0623 void b2EPCollider::FindIncidentEdge(b2ClipVertex c[2], const b2EPProxy* proxy1, int32 edge1, const b2EPProxy* proxy2) 0624 { 0625 int32 count1 = proxy1->count; 0626 const b2Vec2* normals1 = proxy1->normals; 0627 0628 int32 count2 = proxy2->count; 0629 const b2Vec2* vertices2 = proxy2->vertices; 0630 const b2Vec2* normals2 = proxy2->normals; 0631 0632 b2Assert(0 <= edge1 && edge1 < count1); 0633 0634 // Get the normal of the reference edge in proxy2's frame. 0635 b2Vec2 normal1 = normals1[edge1]; 0636 0637 // Find the incident edge on proxy2. 0638 int32 index = 0; 0639 qreal minDot = b2_maxFloat; 0640 for (int32 i = 0; i < count2; ++i) 0641 { 0642 qreal dot = b2Dot(normal1, normals2[i]); 0643 if (dot < minDot) 0644 { 0645 minDot = dot; 0646 index = i; 0647 } 0648 } 0649 0650 // Build the clip vertices for the incident edge. 0651 int32 i1 = index; 0652 int32 i2 = i1 + 1 < count2 ? i1 + 1 : 0; 0653 0654 c[0].v = vertices2[i1]; 0655 c[0].id.cf.indexA = (uint8)edge1; 0656 c[0].id.cf.indexB = (uint8)i1; 0657 c[0].id.cf.typeA = b2ContactFeature::e_face; 0658 c[0].id.cf.typeB = b2ContactFeature::e_vertex; 0659 0660 c[1].v = vertices2[i2]; 0661 c[1].id.cf.indexA = (uint8)edge1; 0662 c[1].id.cf.indexB = (uint8)i2; 0663 c[1].id.cf.typeA = b2ContactFeature::e_face; 0664 c[1].id.cf.typeB = b2ContactFeature::e_vertex; 0665 } 0666 0667 void b2CollideEdgeAndPolygon( b2Manifold* manifold, 0668 const b2EdgeShape* edgeA, const b2Transform& xfA, 0669 const b2PolygonShape* polygonB, const b2Transform& xfB) 0670 { 0671 b2EPCollider collider(edgeA, xfA, polygonB, xfB); 0672 collider.Collide(manifold); 0673 }