Solvespace
799 строк · 29.8 Кб
1//-----------------------------------------------------------------------------
2// Routines to generate our watertight brep shells from the operations
3// and entities specified by the user in each group; templated to work either
4// on an SShell of ratpoly surfaces or on an SMesh of triangles.
5//
6// Copyright 2008-2013 Jonathan Westhues.
7//-----------------------------------------------------------------------------
8#include "solvespace.h"9
10void Group::AssembleLoops(bool *allClosed,11bool *allCoplanar,12bool *allNonZeroLen)13{
14SBezierList sbl = {};15
16int i;17for(auto &e : SK.entity) {18if(e.group != h)19continue;20if(e.construction)21continue;22if(e.forceHidden)23continue;24
25e.GenerateBezierCurves(&sbl);26}27
28SBezier *sb;29*allNonZeroLen = true;30for(sb = sbl.l.First(); sb; sb = sbl.l.NextAfter(sb)) {31for(i = 1; i <= sb->deg; i++) {32if(!(sb->ctrl[i]).Equals(sb->ctrl[0])) {33break;34}35}36if(i > sb->deg) {37// This is a zero-length edge.38*allNonZeroLen = false;39polyError.errorPointAt = sb->ctrl[0];40goto done;41}42}43
44// Try to assemble all these Beziers into loops. The closed loops go into45// bezierLoops, with the outer loops grouped with their holes. The46// leftovers, if any, go in bezierOpens.47bezierLoops.FindOuterFacesFrom(&sbl, &polyLoops, NULL,48SS.ChordTolMm(),49allClosed, &(polyError.notClosedAt),50allCoplanar, &(polyError.errorPointAt),51&bezierOpens);52done:53sbl.Clear();54}
55
56void Group::GenerateLoops() {57polyLoops.Clear();58bezierLoops.Clear();59bezierOpens.Clear();60
61if(type == Type::DRAWING_3D || type == Type::DRAWING_WORKPLANE ||62type == Type::ROTATE || type == Type::TRANSLATE || type == Type::LINKED)63{64bool allClosed = false, allCoplanar = false, allNonZeroLen = false;65AssembleLoops(&allClosed, &allCoplanar, &allNonZeroLen);66if(!allNonZeroLen) {67polyError.how = PolyError::ZERO_LEN_EDGE;68} else if(!allCoplanar) {69polyError.how = PolyError::NOT_COPLANAR;70} else if(!allClosed) {71polyError.how = PolyError::NOT_CLOSED;72} else {73polyError.how = PolyError::GOOD;74// The self-intersecting check is kind of slow, so don't run it75// unless requested.76if(SS.checkClosedContour) {77if(polyLoops.SelfIntersecting(&(polyError.errorPointAt))) {78polyError.how = PolyError::SELF_INTERSECTING;79}80}81}82}83}
84
85void SShell::RemapFaces(Group *g, int remap) {86for(SSurface &ss : surface){87hEntity face = { ss.face };88if(face == Entity::NO_ENTITY) continue;89
90face = g->Remap(face, remap);91ss.face = face.v;92}93}
94
95void SMesh::RemapFaces(Group *g, int remap) {96STriangle *tr;97for(tr = l.First(); tr; tr = l.NextAfter(tr)) {98hEntity face = { tr->meta.face };99if(face == Entity::NO_ENTITY) continue;100
101face = g->Remap(face, remap);102tr->meta.face = face.v;103}104}
105
106template<class T>107void Group::GenerateForStepAndRepeat(T *steps, T *outs, Group::CombineAs forWhat) {108
109int n = (int)valA, a0 = 0;110if(subtype == Subtype::ONE_SIDED && skipFirst) {111a0++; n++;112}113
114int a;115// create all the transformed copies116std::vector <T> transd(n);117std::vector <T> workA(n);118workA[0] = {};119// first generate a shell/mesh with each transformed copy120#pragma omp parallel for121for(a = a0; a < n; a++) {122transd[a] = {};123workA[a] = {};124int ap = a*2 - (subtype == Subtype::ONE_SIDED ? 0 : (n-1));125
126if(type == Type::TRANSLATE) {127Vector trans = Vector::From(h.param(0), h.param(1), h.param(2));128trans = trans.ScaledBy(ap);129transd[a].MakeFromTransformationOf(steps,130trans, Quaternion::IDENTITY, 1.0);131} else {132Vector trans = Vector::From(h.param(0), h.param(1), h.param(2));133double theta = ap * SK.GetParam(h.param(3))->val;134double c = cos(theta), s = sin(theta);135Vector axis = Vector::From(h.param(4), h.param(5), h.param(6));136Quaternion q = Quaternion::From(c, s*axis.x, s*axis.y, s*axis.z);137// Rotation is centered at t; so A(x - t) + t = Ax + (t - At)138transd[a].MakeFromTransformationOf(steps,139trans.Minus(q.Rotate(trans)), q, 1.0);140}141}142for(a = a0; a < n; a++) {143// We need to rewrite any plane face entities to the transformed ones.144int remap = (a == (n - 1)) ? REMAP_LAST : a;145transd[a].RemapFaces(this, remap);146}147
148std::vector<T> *soFar = &transd;149std::vector<T> *scratch = &workA;150// do the boolean operations on pairs of equal size151while(n > 1) {152for(a = 0; a < n; a+=2) {153scratch->at(a/2).Clear();154// combine a pair of shells155if((a==0) && (a0==1)) { // if the first was skipped just copy the 2nd156scratch->at(a/2).MakeFromCopyOf(&(soFar->at(a+1)));157(soFar->at(a+1)).Clear();158a0 = 0;159} else if (a == n-1) { // for an odd number just copy the last one160scratch->at(a/2).MakeFromCopyOf(&(soFar->at(a)));161(soFar->at(a)).Clear();162} else if(forWhat == CombineAs::ASSEMBLE) {163scratch->at(a/2).MakeFromAssemblyOf(&(soFar->at(a)), &(soFar->at(a+1)));164(soFar->at(a)).Clear();165(soFar->at(a+1)).Clear();166} else {167scratch->at(a/2).MakeFromUnionOf(&(soFar->at(a)), &(soFar->at(a+1)));168(soFar->at(a)).Clear();169(soFar->at(a+1)).Clear();170}171}172swap(scratch, soFar);173n = (n+1)/2;174}175outs->Clear();176*outs = soFar->at(0);177}
178
179template<class T>180void Group::GenerateForBoolean(T *prevs, T *thiss, T *outs, Group::CombineAs how) {181// If this group contributes no new mesh, then our running mesh is the182// same as last time, no combining required. Likewise if we have a mesh183// but it's suppressed.184if(thiss->IsEmpty() || suppress) {185outs->MakeFromCopyOf(prevs);186return;187}188
189// So our group's shell appears in thisShell. Combine this with the190// previous group's shell, using the requested operation.191switch(how) {192case CombineAs::UNION:193outs->MakeFromUnionOf(prevs, thiss);194break;195
196case CombineAs::DIFFERENCE:197outs->MakeFromDifferenceOf(prevs, thiss);198break;199
200case CombineAs::INTERSECTION:201outs->MakeFromIntersectionOf(prevs, thiss);202break;203
204case CombineAs::ASSEMBLE:205outs->MakeFromAssemblyOf(prevs, thiss);206break;207}208}
209
210void Group::GenerateShellAndMesh() {211bool prevBooleanFailed = booleanFailed;212booleanFailed = false;213
214Group *srcg = this;215
216thisShell.Clear();217thisMesh.Clear();218runningShell.Clear();219runningMesh.Clear();220
221// Don't attempt a lathe or extrusion unless the source section is good:222// planar and not self-intersecting.223bool haveSrc = true;224if(type == Type::EXTRUDE || type == Type::LATHE || type == Type::REVOLVE) {225Group *src = SK.GetGroup(opA);226if(src->polyError.how != PolyError::GOOD) {227haveSrc = false;228}229}230
231if(type == Type::TRANSLATE || type == Type::ROTATE) {232// A step and repeat gets merged against the group's previous group,233// not our own previous group.234srcg = SK.GetGroup(opA);235
236if(!srcg->suppress) {237if(!IsForcedToMesh()) {238GenerateForStepAndRepeat<SShell>(&(srcg->thisShell), &thisShell, srcg->meshCombine);239} else {240SMesh prevm = {};241prevm.MakeFromCopyOf(&srcg->thisMesh);242srcg->thisShell.TriangulateInto(&prevm);243GenerateForStepAndRepeat<SMesh> (&prevm, &thisMesh, srcg->meshCombine);244}245}246} else if(type == Type::EXTRUDE && haveSrc) {247Group *src = SK.GetGroup(opA);248Vector translate = Vector::From(h.param(0), h.param(1), h.param(2));249
250Vector tbot, ttop;251if(subtype == Subtype::ONE_SIDED) {252tbot = Vector::From(0, 0, 0); ttop = translate.ScaledBy(2);253} else {254tbot = translate.ScaledBy(-1); ttop = translate.ScaledBy(1);255}256
257SBezierLoopSetSet *sblss = &(src->bezierLoops);258SBezierLoopSet *sbls;259for(sbls = sblss->l.First(); sbls; sbls = sblss->l.NextAfter(sbls)) {260int is = thisShell.surface.n;261// Extrude this outer contour (plus its inner contours, if present)262thisShell.MakeFromExtrusionOf(sbls, tbot, ttop, color);263
264// And for any plane faces, annotate the model with the entity for265// that face, so that the user can select them with the mouse.266Vector onOrig = sbls->point;267int i;268// Not using range-for here because we're starting at a different place and using269// indices for meaning.270for(i = is; i < thisShell.surface.n; i++) {271SSurface *ss = &(thisShell.surface[i]);272hEntity face = Entity::NO_ENTITY;273
274Vector p = ss->PointAt(0, 0),275n = ss->NormalAt(0, 0).WithMagnitude(1);276double d = n.Dot(p);277
278if(i == is || i == (is + 1)) {279// These are the top and bottom of the shell.280if(fabs((onOrig.Plus(ttop)).Dot(n) - d) < LENGTH_EPS) {281face = Remap(Entity::NO_ENTITY, REMAP_TOP);282ss->face = face.v;283}284if(fabs((onOrig.Plus(tbot)).Dot(n) - d) < LENGTH_EPS) {285face = Remap(Entity::NO_ENTITY, REMAP_BOTTOM);286ss->face = face.v;287}288continue;289}290
291// So these are the sides292if(ss->degm != 1 || ss->degn != 1) continue;293
294for(Entity &e : SK.entity) {295if(e.group != opA) continue;296if(e.type != Entity::Type::LINE_SEGMENT) continue;297
298Vector a = SK.GetEntity(e.point[0])->PointGetNum(),299b = SK.GetEntity(e.point[1])->PointGetNum();300a = a.Plus(ttop);301b = b.Plus(ttop);302// Could get taken backwards, so check all cases.303if((a.Equals(ss->ctrl[0][0]) && b.Equals(ss->ctrl[1][0])) ||304(b.Equals(ss->ctrl[0][0]) && a.Equals(ss->ctrl[1][0])) ||305(a.Equals(ss->ctrl[0][1]) && b.Equals(ss->ctrl[1][1])) ||306(b.Equals(ss->ctrl[0][1]) && a.Equals(ss->ctrl[1][1])))307{308face = Remap(e.h, REMAP_LINE_TO_FACE);309ss->face = face.v;310break;311}312}313}314}315} else if(type == Type::LATHE && haveSrc) {316Group *src = SK.GetGroup(opA);317
318Vector pt = SK.GetEntity(predef.origin)->PointGetNum(),319axis = SK.GetEntity(predef.entityB)->VectorGetNum();320axis = axis.WithMagnitude(1);321
322SBezierLoopSetSet *sblss = &(src->bezierLoops);323SBezierLoopSet *sbls;324for(sbls = sblss->l.First(); sbls; sbls = sblss->l.NextAfter(sbls)) {325thisShell.MakeFromRevolutionOf(sbls, pt, axis, color, this);326}327} else if(type == Type::REVOLVE && haveSrc) {328Group *src = SK.GetGroup(opA);329double anglef = SK.GetParam(h.param(3))->val * 4; // why the 4 is needed?330double dists = 0, distf = 0;331double angles = 0.0;332if(subtype != Subtype::ONE_SIDED) {333anglef *= 0.5;334angles = -anglef;335}336Vector pt = SK.GetEntity(predef.origin)->PointGetNum(),337axis = SK.GetEntity(predef.entityB)->VectorGetNum();338axis = axis.WithMagnitude(1);339
340SBezierLoopSetSet *sblss = &(src->bezierLoops);341SBezierLoopSet *sbls;342for(sbls = sblss->l.First(); sbls; sbls = sblss->l.NextAfter(sbls)) {343if(fabs(anglef - angles) < 2 * PI) {344thisShell.MakeFromHelicalRevolutionOf(sbls, pt, axis, color, this,345angles, anglef, dists, distf);346} else {347thisShell.MakeFromRevolutionOf(sbls, pt, axis, color, this);348}349}350} else if(type == Type::HELIX && haveSrc) {351Group *src = SK.GetGroup(opA);352double anglef = SK.GetParam(h.param(3))->val * 4; // why the 4 is needed?353double dists = 0, distf = 0;354double angles = 0.0;355distf = SK.GetParam(h.param(7))->val * 2; // dist is applied twice356if(subtype != Subtype::ONE_SIDED) {357anglef *= 0.5;358angles = -anglef;359distf *= 0.5;360dists = -distf;361}362Vector pt = SK.GetEntity(predef.origin)->PointGetNum(),363axis = SK.GetEntity(predef.entityB)->VectorGetNum();364axis = axis.WithMagnitude(1);365
366SBezierLoopSetSet *sblss = &(src->bezierLoops);367SBezierLoopSet *sbls;368for(sbls = sblss->l.First(); sbls; sbls = sblss->l.NextAfter(sbls)) {369thisShell.MakeFromHelicalRevolutionOf(sbls, pt, axis, color, this,370angles, anglef, dists, distf);371}372} else if(type == Type::LINKED) {373// The imported shell or mesh are copied over, with the appropriate374// transformation applied. We also must remap the face entities.375Vector offset = {376SK.GetParam(h.param(0))->val,377SK.GetParam(h.param(1))->val,378SK.GetParam(h.param(2))->val };379Quaternion q = {380SK.GetParam(h.param(3))->val,381SK.GetParam(h.param(4))->val,382SK.GetParam(h.param(5))->val,383SK.GetParam(h.param(6))->val };384
385thisMesh.MakeFromTransformationOf(&impMesh, offset, q, scale);386thisMesh.RemapFaces(this, 0);387
388thisShell.MakeFromTransformationOf(&impShell, offset, q, scale);389thisShell.RemapFaces(this, 0);390}391
392if(srcg->meshCombine != CombineAs::ASSEMBLE) {393thisShell.MergeCoincidentSurfaces();394}395
396// So now we've got the mesh or shell for this group. Combine it with397// the previous group's mesh or shell with the requested Boolean, and398// we're done.399
400Group *prevg = srcg->RunningMeshGroup();401
402if(!IsForcedToMesh()) {403SShell *prevs = &(prevg->runningShell);404GenerateForBoolean<SShell>(prevs, &thisShell, &runningShell,405srcg->meshCombine);406
407if(srcg->meshCombine != CombineAs::ASSEMBLE) {408runningShell.MergeCoincidentSurfaces();409}410
411// If the Boolean failed, then we should note that in the text screen412// for this group.413booleanFailed = runningShell.booleanFailed;414if(booleanFailed != prevBooleanFailed) {415SS.ScheduleShowTW();416}417} else {418SMesh prevm, thism;419prevm = {};420thism = {};421
422prevm.MakeFromCopyOf(&(prevg->runningMesh));423prevg->runningShell.TriangulateInto(&prevm);424
425thism.MakeFromCopyOf(&thisMesh);426thisShell.TriangulateInto(&thism);427
428SMesh outm = {};429GenerateForBoolean<SMesh>(&prevm, &thism, &outm, srcg->meshCombine);430
431// Remove degenerate triangles; if we don't, they'll get split in SnapToMesh432// in every generated group, resulting in polynomial increase in triangle count,433// and corresponding slowdown.434outm.RemoveDegenerateTriangles();435
436if(srcg->meshCombine != CombineAs::ASSEMBLE) {437// And make sure that the output mesh is vertex-to-vertex.438SKdNode *root = SKdNode::From(&outm);439root->SnapToMesh(&outm);440root->MakeMeshInto(&runningMesh);441} else {442runningMesh.MakeFromCopyOf(&outm);443}444
445outm.Clear();446thism.Clear();447prevm.Clear();448}449
450displayDirty = true;451}
452
453void Group::GenerateDisplayItems() {454// This is potentially slow (since we've got to triangulate a shell, or455// to find the emphasized edges for a mesh), so we will run it only456// if its inputs have changed.457if(displayDirty) {458Group *pg = RunningMeshGroup();459if(pg && thisMesh.IsEmpty() && thisShell.IsEmpty()) {460// We don't contribute any new solid model in this group, so our461// display items are identical to the previous group's; which means462// that we can just display those, and stop ourselves from463// recalculating for those every time we get a change in this group.464//465// Note that this can end up recursing multiple times (if multiple466// groups that contribute no solid model exist in sequence), but467// that's okay.468pg->GenerateDisplayItems();469
470displayMesh.Clear();471displayMesh.MakeFromCopyOf(&(pg->displayMesh));472
473displayOutlines.Clear();474if(SS.GW.showEdges || SS.GW.showOutlines) {475displayOutlines.MakeFromCopyOf(&pg->displayOutlines);476}477} else {478// We do contribute new solid model, so we have to triangulate the479// shell, and edge-find the mesh.480displayMesh.Clear();481runningShell.TriangulateInto(&displayMesh);482STriangle *t;483for(t = runningMesh.l.First(); t; t = runningMesh.l.NextAfter(t)) {484STriangle trn = *t;485Vector n = trn.Normal();486trn.an = n;487trn.bn = n;488trn.cn = n;489displayMesh.AddTriangle(&trn);490}491
492displayOutlines.Clear();493
494if(SS.GW.showEdges || SS.GW.showOutlines) {495SOutlineList rawOutlines = {};496if(!runningMesh.l.IsEmpty()) {497// Triangle mesh only; no shell or emphasized edges.498runningMesh.MakeOutlinesInto(&rawOutlines, EdgeKind::EMPHASIZED);499} else {500displayMesh.MakeOutlinesInto(&rawOutlines, EdgeKind::SHARP);501}502
503PolylineBuilder builder;504builder.MakeFromOutlines(rawOutlines);505builder.GenerateOutlines(&displayOutlines);506rawOutlines.Clear();507}508}509
510// If we render this mesh, we need to know whether it's transparent,511// and we'll want all transparent triangles last, to make the depth test512// work correctly.513displayMesh.PrecomputeTransparency();514
515// Recalculate mass center if needed516if(SS.centerOfMass.draw && SS.centerOfMass.dirty && h == SS.GW.activeGroup) {517SS.UpdateCenterOfMass();518}519displayDirty = false;520}521}
522
523Group *Group::PreviousGroup() const {524Group *prev = nullptr;525for(auto const &gh : SK.groupOrder) {526Group *g = SK.GetGroup(gh);527if(g->h == h) {528return prev;529}530prev = g;531}532return nullptr;533}
534
535Group *Group::RunningMeshGroup() const {536if(type == Type::TRANSLATE || type == Type::ROTATE) {537return SK.GetGroup(opA)->RunningMeshGroup();538} else {539return PreviousGroup();540}541}
542
543bool Group::IsMeshGroup() {544switch(type) {545case Group::Type::EXTRUDE:546case Group::Type::LATHE:547case Group::Type::REVOLVE:548case Group::Type::HELIX:549case Group::Type::ROTATE:550case Group::Type::TRANSLATE:551return true;552
553default:554return false;555}556}
557
558void Group::DrawMesh(DrawMeshAs how, Canvas *canvas) {559if(!(SS.GW.showShaded ||560SS.GW.drawOccludedAs != GraphicsWindow::DrawOccludedAs::VISIBLE)) return;561
562switch(how) {563case DrawMeshAs::DEFAULT: {564// Force the shade color to something dim to not distract from565// the sketch.566Canvas::Fill fillFront = {};567if(!SS.GW.showShaded) {568fillFront.layer = Canvas::Layer::DEPTH_ONLY;569}570if((type == Type::DRAWING_3D || type == Type::DRAWING_WORKPLANE)571&& SS.GW.dimSolidModel) {572fillFront.color = Style::Color(Style::DIM_SOLID);573}574Canvas::hFill hcfFront = canvas->GetFill(fillFront);575
576// The back faces are drawn in red; should never seem them, since we577// draw closed shells, so that's a debugging aid.578Canvas::hFill hcfBack = {};579if(SS.drawBackFaces && !displayMesh.isTransparent) {580Canvas::Fill fillBack = {};581fillBack.layer = fillFront.layer;582fillBack.color = RgbaColor::FromFloat(1.0f, 0.1f, 0.1f);583hcfBack = canvas->GetFill(fillBack);584} else {585hcfBack = hcfFront;586}587
588// Draw the shaded solid into the depth buffer for hidden line removal,589// and if we're actually going to display it, to the color buffer too.590canvas->DrawMesh(displayMesh, hcfFront, hcfBack);591
592// Draw mesh edges, for debugging.593if(SS.GW.showMesh) {594Canvas::Stroke strokeTriangle = {};595strokeTriangle.zIndex = 1;596strokeTriangle.color = RgbaColor::FromFloat(0.0f, 1.0f, 0.0f);597strokeTriangle.width = 1;598strokeTriangle.unit = Canvas::Unit::PX;599Canvas::hStroke hcsTriangle = canvas->GetStroke(strokeTriangle);600SEdgeList edges = {};601for(const STriangle &t : displayMesh.l) {602edges.AddEdge(t.a, t.b);603edges.AddEdge(t.b, t.c);604edges.AddEdge(t.c, t.a);605}606canvas->DrawEdges(edges, hcsTriangle);607edges.Clear();608}609break;610}611
612case DrawMeshAs::HOVERED: {613Canvas::Fill fill = {};614fill.color = Style::Color(Style::HOVERED);615fill.pattern = Canvas::FillPattern::CHECKERED_A;616fill.zIndex = 2;617Canvas::hFill hcf = canvas->GetFill(fill);618
619std::vector<uint32_t> faces;620hEntity he = SS.GW.hover.entity;621if(he.v != 0 && SK.GetEntity(he)->IsFace()) {622faces.push_back(he.v);623}624canvas->DrawFaces(displayMesh, faces, hcf);625break;626}627
628case DrawMeshAs::SELECTED: {629Canvas::Fill fill = {};630fill.color = Style::Color(Style::SELECTED);631fill.pattern = Canvas::FillPattern::CHECKERED_B;632fill.zIndex = 1;633Canvas::hFill hcf = canvas->GetFill(fill);634
635std::vector<uint32_t> faces;636SS.GW.GroupSelection();637auto const &gs = SS.GW.gs;638// See also GraphicsWindow::MakeSelected "if(c >= MAX_SELECTABLE_FACES)"639// and GraphicsWindow::GroupSelection "if(e->IsFace())"640for(auto &fc : gs.face) {641faces.push_back(fc.v);642}643canvas->DrawFaces(displayMesh, faces, hcf);644break;645}646}647}
648
649void Group::Draw(Canvas *canvas) {650// Everything here gets drawn whether or not the group is hidden; we651// can control this stuff independently, with show/hide solids, edges,652// mesh, etc.653
654GenerateDisplayItems();655DrawMesh(DrawMeshAs::DEFAULT, canvas);656
657if(SS.GW.showEdges) {658Canvas::Stroke strokeEdge = Style::Stroke(Style::SOLID_EDGE);659strokeEdge.zIndex = 1;660Canvas::hStroke hcsEdge = canvas->GetStroke(strokeEdge);661
662canvas->DrawOutlines(displayOutlines, hcsEdge,663SS.GW.showOutlines664? Canvas::DrawOutlinesAs::EMPHASIZED_WITHOUT_CONTOUR665: Canvas::DrawOutlinesAs::EMPHASIZED_AND_CONTOUR);666
667if(SS.GW.drawOccludedAs != GraphicsWindow::DrawOccludedAs::INVISIBLE) {668Canvas::Stroke strokeHidden = Style::Stroke(Style::HIDDEN_EDGE);669if(SS.GW.drawOccludedAs == GraphicsWindow::DrawOccludedAs::VISIBLE) {670strokeHidden.stipplePattern = StipplePattern::CONTINUOUS;671}672strokeHidden.layer = Canvas::Layer::OCCLUDED;673Canvas::hStroke hcsHidden = canvas->GetStroke(strokeHidden);674
675canvas->DrawOutlines(displayOutlines, hcsHidden,676Canvas::DrawOutlinesAs::EMPHASIZED_AND_CONTOUR);677}678}679
680if(SS.GW.showOutlines) {681Canvas::Stroke strokeOutline = Style::Stroke(Style::OUTLINE);682strokeOutline.zIndex = 1;683Canvas::hStroke hcsOutline = canvas->GetStroke(strokeOutline);684
685canvas->DrawOutlines(displayOutlines, hcsOutline,686Canvas::DrawOutlinesAs::CONTOUR_ONLY);687}688}
689
690void Group::DrawPolyError(Canvas *canvas) {691const Camera &camera = canvas->GetCamera();692
693Canvas::Stroke strokeUnclosed = Style::Stroke(Style::DRAW_ERROR);694strokeUnclosed.color = strokeUnclosed.color.WithAlpha(50);695Canvas::hStroke hcsUnclosed = canvas->GetStroke(strokeUnclosed);696
697Canvas::Stroke strokeError = Style::Stroke(Style::DRAW_ERROR);698strokeError.layer = Canvas::Layer::FRONT;699strokeError.width = 1.0f;700Canvas::hStroke hcsError = canvas->GetStroke(strokeError);701
702double textHeight = Style::DefaultTextHeight() / camera.scale;703
704// And finally show the polygons too, and any errors if it's not possible705// to assemble the lines into closed polygons.706if(polyError.how == PolyError::NOT_CLOSED) {707// Report this error only in sketch-in-workplane groups; otherwise708// it's just a nuisance.709if(type == Type::DRAWING_WORKPLANE) {710canvas->DrawVectorText(_("not closed contour, or not all same style!"),711textHeight,712polyError.notClosedAt.b, camera.projRight, camera.projUp,713hcsError);714canvas->DrawLine(polyError.notClosedAt.a, polyError.notClosedAt.b, hcsUnclosed);715}716} else if(polyError.how == PolyError::NOT_COPLANAR ||717polyError.how == PolyError::SELF_INTERSECTING ||718polyError.how == PolyError::ZERO_LEN_EDGE) {719// These errors occur at points, not lines720if(type == Type::DRAWING_WORKPLANE) {721const char *msg;722if(polyError.how == PolyError::NOT_COPLANAR) {723msg = _("points not all coplanar!");724} else if(polyError.how == PolyError::SELF_INTERSECTING) {725msg = _("contour is self-intersecting!");726} else {727msg = _("zero-length edge!");728}729canvas->DrawVectorText(msg, textHeight,730polyError.errorPointAt, camera.projRight, camera.projUp,731hcsError);732}733} else {734// The contours will get filled in DrawFilledPaths.735}736}
737
738void Group::DrawFilledPaths(Canvas *canvas) {739for(const SBezierLoopSet &sbls : bezierLoops.l) {740if(sbls.l.IsEmpty() || sbls.l[0].l.IsEmpty())741continue;742
743// In an assembled loop, all the styles should be the same; so doesn't744// matter which one we grab.745const SBezier *sb = &(sbls.l[0].l[0]);746Style *s = Style::Get({ (uint32_t)sb->auxA });747
748Canvas::Fill fill = {};749fill.zIndex = 1;750if(s->filled) {751// This is a filled loop, where the user specified a fill color.752fill.color = s->fillColor;753} else if(h == SS.GW.activeGroup && SS.checkClosedContour &&754polyError.how == PolyError::GOOD) {755// If this is the active group, and we are supposed to check756// for closed contours, and we do indeed have a closed and757// non-intersecting contour, then fill it dimly.758fill.color = Style::Color(Style::CONTOUR_FILL).WithAlpha(127);759} else continue;760Canvas::hFill hcf = canvas->GetFill(fill);761
762SPolygon sp = {};763sbls.MakePwlInto(&sp);764canvas->DrawPolygon(sp, hcf);765sp.Clear();766}767}
768
769void Group::DrawContourAreaLabels(Canvas *canvas) {770const Camera &camera = canvas->GetCamera();771Vector gr = camera.projRight.ScaledBy(1 / camera.scale);772Vector gu = camera.projUp.ScaledBy(1 / camera.scale);773
774for(SBezierLoopSet &sbls : bezierLoops.l) {775if(sbls.l.IsEmpty() || sbls.l[0].l.IsEmpty())776continue;777
778Vector min = sbls.l[0].l[0].ctrl[0];779Vector max = min;780Vector zero = Vector::From(0.0, 0.0, 0.0);781sbls.GetBoundingProjd(Vector::From(1.0, 0.0, 0.0), zero, &min.x, &max.x);782sbls.GetBoundingProjd(Vector::From(0.0, 1.0, 0.0), zero, &min.y, &max.y);783sbls.GetBoundingProjd(Vector::From(0.0, 0.0, 1.0), zero, &min.z, &max.z);784
785Vector mid = min.Plus(max).ScaledBy(0.5);786
787hStyle hs = { Style::CONSTRAINT };788Canvas::Stroke stroke = Style::Stroke(hs);789stroke.layer = Canvas::Layer::FRONT;790
791std::string label = SS.MmToStringSI(fabs(sbls.SignedArea()), /*dim=*/2);792double fontHeight = Style::TextHeight(hs);793double textWidth = VectorFont::Builtin()->GetWidth(fontHeight, label),794textHeight = VectorFont::Builtin()->GetCapHeight(fontHeight);795Vector pos = mid.Minus(gr.ScaledBy(textWidth / 2.0))796.Minus(gu.ScaledBy(textHeight / 2.0));797canvas->DrawVectorText(label, fontHeight, pos, gr, gu, canvas->GetStroke(stroke));798}799}
800
801