Allow rendering hidden solid edges using a distinct style.

Before this change, the two buttons "Show/hide shaded model" (S) and
"Show/hide hidden lines" (H) resulted in drawing the following
elements in the following styles:

  Button | Non-occluded | Non-occluded |  Occluded   |   Occluded
  state  | solid edges  |   entities   | solid edges |   entities
 --------+--------------+--------------+-------------+--------------
  !S !H  |              |              | solid-edge  | entity style
 --------+              |              +-------------+--------------
   S !H  |              |              |         invisible
 --------+  solid-edge  | entity style +-------------+--------------
  !S  H  |              |              |             |
 --------+              |              | solid-edge  | entity style
   S  H  |              |              |             |
 --------+--------------+--------------+-------------+--------------

After this change, they are drawn as follows:

  Button | Non-occluded | Non-occluded |  Occluded   |   Occluded
  state  | solid edges  |   entities   | solid edges |   entities
 --------+--------------+--------------+-------------+--------------
  !S !H  |              |              | solid-edge  | entity style
 --------+              |              +-------------+--------------
   S !H  |              |              |         invisible
 --------+  solid-edge  | entity style +-------------+--------------
  !S  H  |              |              |             |
 --------+              |              | hidden-edge |  stippled¹
   S  H  |              |              |             |
 --------+--------------+--------------+-------------+--------------

  ¹ entity style, but the stipple parameters taken from hidden-edge

In SolveSpace's true WYSIWYG tradition, the 2d view export follows
the rendered view exactly.

Also, it is now possible to edit the stipple parameters of built-in
styles, so that by changing the hidden-edge style to non-stippled
it is possible to regain the old behavior.
This commit is contained in:
EvilSpirit 2016-03-09 10:53:46 +06:00 committed by whitequark
parent d55300232f
commit d1a2eb6d18
12 changed files with 113 additions and 83 deletions

View File

@ -34,7 +34,7 @@ void GraphicsWindow::Selection::Draw(void) {
Vector refp = Vector::From(0, 0, 0); Vector refp = Vector::From(0, 0, 0);
if(entity.v) { if(entity.v) {
Entity *e = SK.GetEntity(entity); Entity *e = SK.GetEntity(entity);
e->Draw(); e->Draw(/*drawAsHidden=*/false);
if(emphasized) refp = e->GetReferencePos(); if(emphasized) refp = e->GetReferencePos();
} }
if(constraint.v) { if(constraint.v) {
@ -686,9 +686,15 @@ nogrid:;
// Draw the active group; this does stuff like the mesh and edges. // Draw the active group; this does stuff like the mesh and edges.
(SK.GetGroup(activeGroup))->Draw(); (SK.GetGroup(activeGroup))->Draw();
// Now draw the entities // Now draw the entities.
if(showHdnLines) glDisable(GL_DEPTH_TEST); if(SS.GW.showHdnLines) {
Entity::DrawAll(); ssglDepthRangeOffset(2);
glDepthFunc(GL_GREATER);
Entity::DrawAll(/*drawAsHidden=*/true);
glDepthFunc(GL_LEQUAL);
}
ssglDepthRangeOffset(0);
Entity::DrawAll(/*drawAsHidden=*/false);
// Draw filled paths in all groups, when those filled paths were requested // Draw filled paths in all groups, when those filled paths were requested
// specially by assigning a style with a fill color, or when the filled // specially by assigning a style with a fill color, or when the filled
@ -737,9 +743,7 @@ nogrid:;
glEnd(); glEnd();
// And the naked edges, if the user did Analyze -> Show Naked Edges. // And the naked edges, if the user did Analyze -> Show Naked Edges.
ssglLineWidth(Style::Width(Style::DRAW_ERROR)); ssglDrawEdges(&(SS.nakedEdges), true, { Style::DRAW_ERROR });
ssglColorRGB(Style::Color(Style::DRAW_ERROR));
ssglDrawEdges(&(SS.nakedEdges), true);
// Then redraw whatever the mouse is hovering over, highlighted. // Then redraw whatever the mouse is hovering over, highlighted.
glDisable(GL_DEPTH_TEST); glDisable(GL_DEPTH_TEST);

View File

@ -40,10 +40,9 @@ void Entity::LineDrawOrGetDistance(Vector a, Vector b, bool maybeFat, int data)
dogd.refp = (a.Plus(b)).ScaledBy(0.5); dogd.refp = (a.Plus(b)).ScaledBy(0.5);
} }
void Entity::DrawAll(void) { void Entity::DrawAll(bool drawAsHidden) {
// This handles points and line segments as a special case, because I // This handles points as a special case, because I seem to be able
// seem to be able to get a huge speedup that way, by consolidating // to get a huge speedup that way, by consolidating stuff to gl.
// stuff to gl.
int i; int i;
if(SS.GW.showPoints) { if(SS.GW.showPoints) {
double s = 3.5/SS.GW.scale; double s = 3.5/SS.GW.scale;
@ -98,19 +97,23 @@ void Entity::DrawAll(void) {
for(i = 0; i < SK.entity.n; i++) { for(i = 0; i < SK.entity.n; i++) {
Entity *e = &(SK.entity.elem[i]); Entity *e = &(SK.entity.elem[i]);
if(e->IsPoint()) if(e->IsPoint()) {
{
continue; // already handled continue; // already handled
} }
e->Draw(); e->Draw(drawAsHidden);
} }
} }
void Entity::Draw(void) { void Entity::Draw(bool drawAsHidden) {
hStyle hs = Style::ForEntity(h); hStyle hs = Style::ForEntity(h);
dogd.lineWidth = Style::Width(hs); dogd.lineWidth = Style::Width(hs);
dogd.stippleType = Style::PatternType(hs); if(drawAsHidden) {
dogd.stippleScale = Style::StippleScaleMm(hs); dogd.stippleType = Style::PatternType({ Style::HIDDEN_EDGE });
dogd.stippleScale = Style::StippleScaleMm({ Style::HIDDEN_EDGE });
} else {
dogd.stippleType = Style::PatternType(hs);
dogd.stippleScale = Style::StippleScaleMm(hs);
}
ssglLineWidth((float)dogd.lineWidth); ssglLineWidth((float)dogd.lineWidth);
ssglColorRGB(Style::Color(hs)); ssglColorRGB(Style::Color(hs));

View File

@ -122,7 +122,7 @@ void SolveSpaceUI::ExportViewOrWireframeTo(const std::string &filename, bool wir
GenerateAll(GENERATE_ALL); GenerateAll(GENERATE_ALL);
SMesh *sm = NULL; SMesh *sm = NULL;
if(SS.GW.showShaded) { if(SS.GW.showShaded || SS.GW.showHdnLines) {
Group *g = SK.GetGroup(SS.GW.activeGroup); Group *g = SK.GetGroup(SS.GW.activeGroup);
g->GenerateDisplayItems(); g->GenerateDisplayItems();
sm = &(g->displayMesh); sm = &(g->displayMesh);
@ -136,8 +136,7 @@ void SolveSpaceUI::ExportViewOrWireframeTo(const std::string &filename, bool wir
if(!e->IsVisible()) continue; if(!e->IsVisible()) continue;
if(e->construction) continue; if(e->construction) continue;
if(SS.exportPwlCurves || (sm && !SS.GW.showHdnLines) || if(SS.exportPwlCurves || sm || fabs(SS.exportOffset) > LENGTH_EPS)
fabs(SS.exportOffset) > LENGTH_EPS)
{ {
// We will be doing hidden line removal, which we can't do on // We will be doing hidden line removal, which we can't do on
// exact curves; so we need things broken down to pwls. Same // exact curves; so we need things broken down to pwls. Same
@ -322,7 +321,7 @@ void SolveSpaceUI::ExportLinesAndMesh(SEdgeList *sel, SBezierList *sbl, SMesh *s
// And now we perform hidden line removal if requested // And now we perform hidden line removal if requested
SEdgeList hlrd = {}; SEdgeList hlrd = {};
if(sm && !SS.GW.showHdnLines) { if(sm) {
SKdNode *root = SKdNode::From(&smp); SKdNode *root = SKdNode::From(&smp);
// Generate the edges where a curved surface turns from front-facing // Generate the edges where a curved surface turns from front-facing
@ -344,19 +343,19 @@ void SolveSpaceUI::ExportLinesAndMesh(SEdgeList *sel, SBezierList *sbl, SMesh *s
continue; continue;
} }
SEdgeList out = {}; SEdgeList edges = {};
// Split the original edge against the mesh // Split the original edge against the mesh
out.AddEdge(se->a, se->b, se->auxA); edges.AddEdge(se->a, se->b, se->auxA);
root->OcclusionTestLine(*se, &out, cnt); root->OcclusionTestLine(*se, &edges, cnt, /*removeHidden=*/!SS.GW.showHdnLines);
// the occlusion test splits unnecessarily; so fix those // the occlusion test splits unnecessarily; so fix those
out.MergeCollinearSegments(se->a, se->b); edges.MergeCollinearSegments(se->a, se->b);
cnt++; cnt++;
// And add the results to our output // And add the results to our output
SEdge *sen; SEdge *sen;
for(sen = out.l.First(); sen; sen = out.l.NextAfter(sen)) { for(sen = edges.l.First(); sen; sen = edges.l.NextAfter(sen)) {
hlrd.AddEdge(sen->a, sen->b, sen->auxA); hlrd.AddEdge(sen->a, sen->b, sen->auxA);
} }
out.Clear(); edges.Clear();
} }
sel = &hlrd; sel = &hlrd;
@ -516,6 +515,12 @@ void SolveSpaceUI::ExportLinesAndMesh(SEdgeList *sel, SBezierList *sbl, SMesh *s
sblss.AddOpenPath(b); sblss.AddOpenPath(b);
} }
// We need the mesh for occlusion testing, but if we don't export it,
// erase it now.
if(!SS.GW.showShaded) {
sms.Clear();
}
// Now write the lines and triangles to the output file // Now write the lines and triangles to the output file
out->OutputLinesAndMesh(&sblss, &sms); out->OutputLinesAndMesh(&sblss, &sms);

View File

@ -562,15 +562,19 @@ void ssglDebugPolygon(SPolygon *p)
} }
} }
void ssglDrawEdges(SEdgeList *el, bool endpointsToo) void ssglDrawEdges(SEdgeList *el, bool endpointsToo, hStyle hs)
{ {
double lineWidth = Style::Width(hs);
int stippleType = Style::PatternType(hs);
double stippleScale = Style::StippleScaleMm(hs);
ssglLineWidth(float(lineWidth));
ssglColorRGB(Style::Color(hs));
SEdge *se; SEdge *se;
glBegin(GL_LINES);
for(se = el->l.First(); se; se = el->l.NextAfter(se)) { for(se = el->l.First(); se; se = el->l.NextAfter(se)) {
ssglVertex3v(se->a); ssglStippledLine(se->a, se->b, lineWidth, stippleType, stippleScale,
ssglVertex3v(se->b); /*maybeFat=*/true);
} }
glEnd();
if(endpointsToo) { if(endpointsToo) {
glPointSize(12); glPointSize(12);

View File

@ -476,7 +476,7 @@ void Group::DrawDisplayItems(int t) {
if(gs.faces > 0) ms1 = gs.face[0].v; if(gs.faces > 0) ms1 = gs.face[0].v;
if(gs.faces > 1) ms2 = gs.face[1].v; if(gs.faces > 1) ms2 = gs.face[1].v;
if(SS.GW.showShaded) { if(SS.GW.showShaded || SS.GW.showHdnLines) {
if(SS.drawBackFaces && !displayMesh.isTransparent) { if(SS.drawBackFaces && !displayMesh.isTransparent) {
// For debugging, draw the backs of the triangles in red, so that we // For debugging, draw the backs of the triangles in red, so that we
// notice when a shell is open // notice when a shell is open
@ -485,16 +485,26 @@ void Group::DrawDisplayItems(int t) {
glLightModeli(GL_LIGHT_MODEL_TWO_SIDE, 0); glLightModeli(GL_LIGHT_MODEL_TWO_SIDE, 0);
} }
// Draw the shaded solid into the depth buffer for hidden line removal,
// and if we're actually going to display it, to the color buffer too.
glEnable(GL_LIGHTING); glEnable(GL_LIGHTING);
if(!SS.GW.showShaded) glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE);
ssglFillMesh(useSpecColor, specColor, &displayMesh, mh, ms1, ms2); ssglFillMesh(useSpecColor, specColor, &displayMesh, mh, ms1, ms2);
if(!SS.GW.showShaded) glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
glDisable(GL_LIGHTING); glDisable(GL_LIGHTING);
} }
if(SS.GW.showEdges) { if(SS.GW.showEdges) {
glDepthMask(GL_FALSE);
if(SS.GW.showHdnLines) {
ssglDepthRangeOffset(0);
glDepthFunc(GL_GREATER);
ssglDrawEdges(&displayEdges, false, { Style::HIDDEN_EDGE });
glDepthFunc(GL_LEQUAL);
}
ssglDepthRangeOffset(2); ssglDepthRangeOffset(2);
ssglColorRGB(Style::Color(Style::SOLID_EDGE)); ssglDrawEdges(&displayEdges, false, { Style::SOLID_EDGE });
ssglLineWidth(Style::Width(Style::SOLID_EDGE)); glDepthMask(GL_TRUE);
ssglDrawEdges(&displayEdges, false);
} }
if(SS.GW.showMesh) ssglDebugMesh(&displayMesh); if(SS.GW.showMesh) ssglDebugMesh(&displayMesh);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 544 B

View File

@ -653,7 +653,7 @@ void SKdNode::SnapToMesh(SMesh *m) {
// them for occlusion. Keep only the visible segments. sel is both our input // them for occlusion. Keep only the visible segments. sel is both our input
// and our output. // and our output.
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
void SKdNode::SplitLinesAgainstTriangle(SEdgeList *sel, STriangle *tr) { void SKdNode::SplitLinesAgainstTriangle(SEdgeList *sel, STriangle *tr, bool removeHidden) {
SEdgeList seln = {}; SEdgeList seln = {};
Vector tn = tr->Normal().WithMagnitude(1); Vector tn = tr->Normal().WithMagnitude(1);
@ -754,8 +754,11 @@ void SKdNode::SplitLinesAgainstTriangle(SEdgeList *sel, STriangle *tr) {
if(n[i].Dot(pt) - d[i] > LENGTH_EPS) se->tag = 0; if(n[i].Dot(pt) - d[i] > LENGTH_EPS) se->tag = 0;
} }
} }
if(!removeHidden && se->tag == 1)
se->auxA = Style::HIDDEN_EDGE;
} }
sel->l.RemoveTagged(); if(removeHidden)
sel->l.RemoveTagged();
} }
} }
@ -763,7 +766,7 @@ void SKdNode::SplitLinesAgainstTriangle(SEdgeList *sel, STriangle *tr) {
// Given an edge orig, occlusion test it against our mesh. We output an edge // Given an edge orig, occlusion test it against our mesh. We output an edge
// list in sel, containing the visible portions of that edge. // list in sel, containing the visible portions of that edge.
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
void SKdNode::OcclusionTestLine(SEdge orig, SEdgeList *sel, int cnt) { void SKdNode::OcclusionTestLine(SEdge orig, SEdgeList *sel, int cnt, bool removeHidden) {
if(gt && lt) { if(gt && lt) {
double ac = (orig.a).Element(which), double ac = (orig.a).Element(which),
bc = (orig.b).Element(which); bc = (orig.b).Element(which);
@ -773,13 +776,13 @@ void SKdNode::OcclusionTestLine(SEdge orig, SEdgeList *sel, int cnt) {
bc < c + KDTREE_EPS || bc < c + KDTREE_EPS ||
which == 2) which == 2)
{ {
lt->OcclusionTestLine(orig, sel, cnt); lt->OcclusionTestLine(orig, sel, cnt, removeHidden);
} }
if(ac > c - KDTREE_EPS || if(ac > c - KDTREE_EPS ||
bc > c - KDTREE_EPS || bc > c - KDTREE_EPS ||
which == 2) which == 2)
{ {
gt->OcclusionTestLine(orig, sel, cnt); gt->OcclusionTestLine(orig, sel, cnt, removeHidden);
} }
} else { } else {
STriangleLl *ll; STriangleLl *ll;
@ -788,7 +791,7 @@ void SKdNode::OcclusionTestLine(SEdge orig, SEdgeList *sel, int cnt) {
if(tr->tag == cnt) continue; if(tr->tag == cnt) continue;
SplitLinesAgainstTriangle(sel, tr); SplitLinesAgainstTriangle(sel, tr, removeHidden);
tr->tag = cnt; tr->tag = cnt;
} }
} }

View File

@ -453,7 +453,7 @@ void SEdgeList::MergeCollinearSegments(Vector a, Vector b) {
SEdge *prev = &(l.elem[i-1]), SEdge *prev = &(l.elem[i-1]),
*now = &(l.elem[i]); *now = &(l.elem[i]);
if((prev->b).Equals(now->a)) { if((prev->b).Equals(now->a) && prev->auxA == now->auxA) {
// The previous segment joins up to us; so merge it into us. // The previous segment joins up to us; so merge it into us.
prev->tag = 1; prev->tag = 1;
now->a = prev->a; now->a = prev->a;

View File

@ -306,8 +306,8 @@ public:
void MakeCertainEdgesInto(SEdgeList *sel, int how, bool coplanarIsInter, void MakeCertainEdgesInto(SEdgeList *sel, int how, bool coplanarIsInter,
bool *inter, bool *leaky); bool *inter, bool *leaky);
void OcclusionTestLine(SEdge orig, SEdgeList *sel, int cnt); void OcclusionTestLine(SEdge orig, SEdgeList *sel, int cnt, bool removeHidden);
void SplitLinesAgainstTriangle(SEdgeList *sel, STriangle *tr); void SplitLinesAgainstTriangle(SEdgeList *sel, STriangle *tr, bool removeHidden);
void SnapToMesh(SMesh *m); void SnapToMesh(SMesh *m);
void SnapToVertex(Vector v, SMesh *extras); void SnapToVertex(Vector v, SMesh *extras);

View File

@ -505,8 +505,8 @@ public:
void GenerateBezierCurves(SBezierList *sbl); void GenerateBezierCurves(SBezierList *sbl);
void GenerateEdges(SEdgeList *el, bool includingConstruction=false); void GenerateEdges(SEdgeList *el, bool includingConstruction=false);
static void DrawAll(void); static void DrawAll(bool drawAsHidden);
void Draw(void); void Draw(bool drawAsHidden);
double GetDistance(Point2d mp); double GetDistance(Point2d mp);
Vector GetReferencePos(void); Vector GetReferencePos(void);
@ -767,6 +767,7 @@ public:
ANALYZE = 11, ANALYZE = 11,
DRAW_ERROR = 12, DRAW_ERROR = 12,
DIM_SOLID = 13, DIM_SOLID = 13,
HIDDEN_EDGE = 14,
FIRST_CUSTOM = 0x100 FIRST_CUSTOM = 0x100
}; };

View File

@ -345,7 +345,7 @@ void ssglFillPolygon(SPolygon *p);
void ssglFillMesh(bool useSpecColor, RgbaColor color, void ssglFillMesh(bool useSpecColor, RgbaColor color,
SMesh *m, uint32_t h, uint32_t s1, uint32_t s2); SMesh *m, uint32_t h, uint32_t s1, uint32_t s2);
void ssglDebugPolygon(SPolygon *p); void ssglDebugPolygon(SPolygon *p);
void ssglDrawEdges(SEdgeList *l, bool endpointsToo); void ssglDrawEdges(SEdgeList *l, bool endpointsToo, hStyle hs);
void ssglDebugMesh(SMesh *m); void ssglDebugMesh(SMesh *m);
void ssglMarkPolygonNormal(SPolygon *p); void ssglMarkPolygonNormal(SPolygon *p);
typedef void ssglLineFn(void *data, Vector a, Vector b); typedef void ssglLineFn(void *data, Vector a, Vector b);

View File

@ -24,6 +24,7 @@ const Style::Default Style::Defaults[] = {
{ { ANALYZE }, "Analyze", RGBf(0.0, 1.0, 1.0), 1.0, 0 }, { { ANALYZE }, "Analyze", RGBf(0.0, 1.0, 1.0), 1.0, 0 },
{ { DRAW_ERROR }, "DrawError", RGBf(1.0, 0.0, 0.0), 8.0, 0 }, { { DRAW_ERROR }, "DrawError", RGBf(1.0, 0.0, 0.0), 8.0, 0 },
{ { DIM_SOLID }, "DimSolid", RGBf(0.1, 0.1, 0.1), 1.0, 0 }, { { DIM_SOLID }, "DimSolid", RGBf(0.1, 0.1, 0.1), 1.0, 0 },
{ { HIDDEN_EDGE }, "HiddenEdge", RGBf(0.8, 0.8, 0.8), 2.0, 1 },
{ { 0 }, NULL, RGBf(0.0, 0.0, 0.0), 0.0, 0 } { { 0 }, NULL, RGBf(0.0, 0.0, 0.0), 0.0, 0 }
}; };
@ -94,7 +95,8 @@ void Style::FillDefaultStyle(Style *s, const Default *d) {
s->exportable = true; s->exportable = true;
s->filled = false; s->filled = false;
s->fillColor = RGBf(0.3, 0.3, 0.3); s->fillColor = RGBf(0.3, 0.3, 0.3);
s->stippleType = Style::STIPPLE_CONTINUOUS; s->stippleType = (d->h.v == Style::HIDDEN_EDGE) ? Style::STIPPLE_DASH
: Style::STIPPLE_CONTINUOUS;
s->stippleScale = 15.0; s->stippleScale = 15.0;
s->zIndex = d->zIndex; s->zIndex = d->zIndex;
} }
@ -828,43 +830,41 @@ void TextWindow::ShowStyleInfo(void) {
SS.UnitName()); SS.UnitName());
} }
if(s->h.v >= Style::FIRST_CUSTOM) { Printf(false,"%Ba %Ftstipple type:%E");
Printf(false,"%Ba %Ftstipple type:%E");
const size_t patternCount = Style::LAST_STIPPLE + 1; const size_t patternCount = Style::LAST_STIPPLE + 1;
const char *patternsSource[patternCount] = { const char *patternsSource[patternCount] = {
"___________", "___________",
"- - - - - -", "- - - - - -",
"__ __ __ __", "__ __ __ __",
"-.-.-.-.-.-", "-.-.-.-.-.-",
"..-..-..-..", "..-..-..-..",
"...........", "...........",
"~~~~~~~~~~~", "~~~~~~~~~~~",
"__~__~__~__" "__~__~__~__"
}; };
std::string patterns[patternCount]; std::string patterns[patternCount];
for(int i = 0; i <= Style::LAST_STIPPLE; i++) { for(int i = 0; i <= Style::LAST_STIPPLE; i++) {
const char *str = patternsSource[i]; const char *str = patternsSource[i];
do { do {
switch(*str) { switch(*str) {
case ' ': patterns[i] += " "; break; case ' ': patterns[i] += " "; break;
case '.': patterns[i] += "\xEE\x80\x84"; break; case '.': patterns[i] += "\xEE\x80\x84"; break;
case '_': patterns[i] += "\xEE\x80\x85"; break; case '_': patterns[i] += "\xEE\x80\x85"; break;
case '-': patterns[i] += "\xEE\x80\x86"; break; case '-': patterns[i] += "\xEE\x80\x86"; break;
case '~': patterns[i] += "\xEE\x80\x87"; break; case '~': patterns[i] += "\xEE\x80\x87"; break;
default: oops(); default: oops();
} }
} while(*(++str)); } while(*(++str));
} }
for(int i = 0; i <= Style::LAST_STIPPLE; i++) { for(int i = 0; i <= Style::LAST_STIPPLE; i++) {
const char *radio = s->stippleType == i ? RADIO_TRUE : RADIO_FALSE; const char *radio = s->stippleType == i ? RADIO_TRUE : RADIO_FALSE;
Printf(false, "%Bp %D%f%Lp%s %s%E", Printf(false, "%Bp %D%f%Lp%s %s%E",
(i % 2 == 0) ? 'd' : 'a', (i % 2 == 0) ? 'd' : 'a',
s->h.v, &ScreenChangeStylePatternType, s->h.v, &ScreenChangeStylePatternType,
i + 1, radio, patterns[i].c_str()); i + 1, radio, patterns[i].c_str());
}
} }
if(s->h.v >= Style::FIRST_CUSTOM) { if(s->h.v >= Style::FIRST_CUSTOM) {