
separate groups. The section is swept normal to the trajectory, producing a mesh. I'm doing the triangles only now, not copying over any entities. Also fix a bug in the PNG export; rows are 4-aligned, so that was breaking when the width of the image wasn't divisible by four. Also fix a bug in lathes, where it generated overlapping triangles for one segment. And change the groups to record both "this mesh", the contribution due to the extrude/lathe/whatever, and the "running mesh", that we get after applying the requested Boolean op between "this mesh" and the previous group's "running mesh". I'll use that to make step and repeats step the mesh too. [git-p4: depot-paths = "//depot/solvespace/": change = 1801]
506 lines
17 KiB
C++
506 lines
17 KiB
C++
#include "solvespace.h"
|
|
|
|
#define gs (SS.GW.gs)
|
|
|
|
void Group::GeneratePolygon(void) {
|
|
poly.Clear();
|
|
|
|
if(type == DRAWING_3D || type == DRAWING_WORKPLANE ||
|
|
type == ROTATE || type == TRANSLATE)
|
|
{
|
|
SEdgeList edges; ZERO(&edges);
|
|
int i;
|
|
for(i = 0; i < SS.entity.n; i++) {
|
|
Entity *e = &(SS.entity.elem[i]);
|
|
if(e->group.v != h.v) continue;
|
|
|
|
e->GenerateEdges(&edges);
|
|
}
|
|
SEdge error;
|
|
if(edges.AssemblePolygon(&poly, &error)) {
|
|
polyError.how = POLY_GOOD;
|
|
poly.normal = poly.ComputeNormal();
|
|
poly.FixContourDirections();
|
|
|
|
if(!poly.AllPointsInPlane(&(polyError.notCoplanarAt))) {
|
|
// The edges aren't all coplanar; so not a good polygon
|
|
polyError.how = POLY_NOT_COPLANAR;
|
|
poly.Clear();
|
|
}
|
|
} else {
|
|
polyError.how = POLY_NOT_CLOSED;
|
|
polyError.notClosedAt = error;
|
|
poly.Clear();
|
|
}
|
|
edges.Clear();
|
|
}
|
|
}
|
|
|
|
void Group::GetTrajectory(hGroup hg, SContour *traj, SPolygon *section) {
|
|
if(section->IsEmpty()) return;
|
|
|
|
SEdgeList edges; ZERO(&edges);
|
|
int i, j;
|
|
for(i = 0; i < SS.entity.n; i++) {
|
|
Entity *e = &(SS.entity.elem[i]);
|
|
if(e->group.v != hg.v) continue;
|
|
e->GenerateEdges(&edges);
|
|
}
|
|
|
|
Vector pn = (section->normal).WithMagnitude(1);
|
|
double pd = pn.Dot(section->AnyPoint());
|
|
|
|
// Find the start of the trajectory
|
|
Vector first, last;
|
|
for(i = 0; i < edges.l.n; i++) {
|
|
SEdge *se = &(edges.l.elem[i]);
|
|
|
|
bool startA = true, startB = true;
|
|
for(j = 0; j < edges.l.n; j++) {
|
|
if(i == j) continue;
|
|
SEdge *set = &(edges.l.elem[j]);
|
|
if((set->a).Equals(se->a)) startA = false;
|
|
if((set->b).Equals(se->a)) startA = false;
|
|
if((set->a).Equals(se->b)) startB = false;
|
|
if((set->b).Equals(se->b)) startB = false;
|
|
}
|
|
if(startA || startB) {
|
|
// It's possible for both to be true, if only one segment exists
|
|
if(startA) {
|
|
first = se->a;
|
|
last = se->b;
|
|
} else {
|
|
first = se->b;
|
|
last = se->a;
|
|
}
|
|
se->tag = 1;
|
|
break;
|
|
}
|
|
}
|
|
if(i >= edges.l.n) goto cleanup;
|
|
edges.AssembleContour(first, last, traj, NULL);
|
|
if(traj->l.n < 1) goto cleanup;
|
|
|
|
// Starting and ending points of the trajectory
|
|
Vector ps, pf;
|
|
ps = traj->l.elem[0].p;
|
|
pf = traj->l.elem[traj->l.n - 1].p;
|
|
// Distances of those points to the section plane
|
|
double ds = fabs(pn.Dot(ps) - pd), df = fabs(pn.Dot(pf) - pd);
|
|
if(ds < LENGTH_EPS && df < LENGTH_EPS) {
|
|
if(section->WindingNumberForPoint(pf) > 0) {
|
|
// Both the start and finish lie on the section plane; let the
|
|
// start be the one that's somewhere within the section. Use
|
|
// winding > 0, not odd/even, since it's natural e.g. to sweep
|
|
// a ring to make a pipe, and draw the trajectory through the
|
|
// center of the ring.
|
|
traj->Reverse();
|
|
}
|
|
} else if(ds > df) {
|
|
// The starting point is the endpoint that's closer to the plane
|
|
traj->Reverse();
|
|
}
|
|
|
|
cleanup:
|
|
edges.Clear();
|
|
}
|
|
|
|
void Group::AddQuadWithNormal(STriMeta meta, Vector out,
|
|
Vector a, Vector b, Vector c, Vector d)
|
|
{
|
|
// The quad becomes two triangles
|
|
STriangle quad1 = STriangle::From(meta, a, b, c),
|
|
quad2 = STriangle::From(meta, c, d, a);
|
|
|
|
// Could be only one of the triangles has area; be sure
|
|
// to use that one for normal checking, then.
|
|
Vector n1 = quad1.Normal(), n2 = quad2.Normal();
|
|
Vector n = (n1.Magnitude() > n2.Magnitude()) ? n1 : n2;
|
|
if(n.Dot(out) < 0) {
|
|
quad1.FlipNormal();
|
|
quad2.FlipNormal();
|
|
}
|
|
// One or both of the endpoints might lie on the axis of
|
|
// rotation, in which case its triangle is zero-area.
|
|
if(n1.Magnitude() > LENGTH_EPS) thisMesh.AddTriangle(&quad1);
|
|
if(n2.Magnitude() > LENGTH_EPS) thisMesh.AddTriangle(&quad2);
|
|
}
|
|
|
|
void Group::GenerateMeshForSweep(void) {
|
|
STriMeta meta = { 0, color };
|
|
SEdgeList edges;
|
|
ZERO(&edges);
|
|
int a, i;
|
|
|
|
// The closed section that will be swept along the curve
|
|
Group *section = SS.GetGroup(opB);
|
|
(section->poly).MakeEdgesInto(&edges);
|
|
|
|
// The trajectory along which the section will be swept
|
|
SContour traj;
|
|
ZERO(&traj);
|
|
GetTrajectory(opA, &traj, &(section->poly));
|
|
|
|
if(traj.l.n <= 0) {
|
|
edges.Clear();
|
|
return; // no trajectory, nothing to do
|
|
}
|
|
|
|
// Initial offset/orientation determined by first pwl in trajectory
|
|
Vector origRef = traj.l.elem[0].p;
|
|
Vector origNormal = (traj.l.elem[1].p).Minus(origRef);
|
|
origNormal = origNormal.WithMagnitude(1);
|
|
Vector oldRef = origRef, oldNormal = origNormal;
|
|
Vector oldU = oldNormal.Normal(0), oldV = oldNormal.Normal(1);
|
|
|
|
// The endcap at the start of the curve
|
|
SPolygon cap;
|
|
ZERO(&cap);
|
|
edges.l.ClearTags();
|
|
edges.AssemblePolygon(&cap, NULL);
|
|
cap.normal = cap.ComputeNormal();
|
|
if(oldNormal.Dot(cap.normal) > 0) {
|
|
cap.normal = (cap.normal).ScaledBy(-1);
|
|
}
|
|
cap.TriangulateInto(&thisMesh, meta);
|
|
cap.Clear();
|
|
|
|
// Rewrite the source polygon so that the trajectory is along the
|
|
// z axis, and the poly lies in the xy plane
|
|
for(i = 0; i < edges.l.n; i++) {
|
|
SEdge *e = &(edges.l.elem[i]);
|
|
e->a = ((e->a).Minus(oldRef)).DotInToCsys(oldU, oldV, oldNormal);
|
|
e->b = ((e->b).Minus(oldRef)).DotInToCsys(oldU, oldV, oldNormal);
|
|
}
|
|
Vector polyn =
|
|
(section->poly.normal).DotInToCsys(oldU, oldV, oldNormal);
|
|
|
|
for(a = 1; a < traj.l.n; a++) {
|
|
Vector thisRef = traj.l.elem[a].p;
|
|
Vector thisNormal, useNormal;
|
|
if(a == traj.l.n - 1) {
|
|
thisNormal = oldNormal;
|
|
useNormal = oldNormal;
|
|
} else {
|
|
thisNormal = (traj.l.elem[a+1].p).Minus(thisRef);
|
|
useNormal = (thisNormal.Plus(oldNormal)).ScaledBy(0.5);
|
|
}
|
|
|
|
// Choose a new coordinate system, normal to the trajectory and
|
|
// with the minimum possible twist about the normal.
|
|
useNormal = useNormal.WithMagnitude(1);
|
|
Vector useV = (useNormal.Cross(oldU)).WithMagnitude(1);
|
|
Vector useU = (useV.Cross(useNormal)).WithMagnitude(1);
|
|
|
|
Quaternion qi = Quaternion::From(oldU, oldV);
|
|
Quaternion qf = Quaternion::From(useU, useV);
|
|
|
|
for(i = 0; i < edges.l.n; i++) {
|
|
SEdge *edge = &(edges.l.elem[i]);
|
|
Vector ai, bi, af, bf;
|
|
ai = qi.Rotate(edge->a).Plus(oldRef);
|
|
bi = qi.Rotate(edge->b).Plus(oldRef);
|
|
|
|
af = qf.Rotate(edge->a).Plus(thisRef);
|
|
bf = qf.Rotate(edge->b).Plus(thisRef);
|
|
|
|
Vector ab = (edge->b).Minus(edge->a);
|
|
Vector out = polyn.Cross(ab);
|
|
out = qf.Rotate(out);
|
|
|
|
AddQuadWithNormal(meta, out, ai, bi, bf, af);
|
|
}
|
|
oldRef = thisRef;
|
|
oldNormal = thisNormal;
|
|
oldU = useU;
|
|
oldV = useV;
|
|
}
|
|
|
|
Quaternion q = Quaternion::From(oldU, oldV);
|
|
for(i = 0; i < edges.l.n; i++) {
|
|
SEdge *edge = &(edges.l.elem[i]);
|
|
(edge->a) = q.Rotate(edge->a).Plus(oldRef);
|
|
(edge->b) = q.Rotate(edge->b).Plus(oldRef);
|
|
}
|
|
edges.l.ClearTags();
|
|
edges.AssemblePolygon(&cap, NULL);
|
|
cap.normal = cap.ComputeNormal();
|
|
if(oldNormal.Dot(cap.normal) < 0) {
|
|
cap.normal = (cap.normal).ScaledBy(-1);
|
|
}
|
|
cap.TriangulateInto(&thisMesh, meta);
|
|
cap.Clear();
|
|
|
|
traj.l.Clear();
|
|
edges.Clear();
|
|
}
|
|
|
|
void Group::GenerateMesh(void) {
|
|
thisMesh.Clear();
|
|
STriMeta meta = { 0, color };
|
|
|
|
if(type == EXTRUDE) {
|
|
SEdgeList edges;
|
|
ZERO(&edges);
|
|
int i;
|
|
Group *src = SS.GetGroup(opA);
|
|
Vector translate = Vector::From(h.param(0), h.param(1), h.param(2));
|
|
|
|
Vector tbot, ttop;
|
|
if(subtype == ONE_SIDED) {
|
|
tbot = Vector::From(0, 0, 0); ttop = translate.ScaledBy(2);
|
|
} else {
|
|
tbot = translate.ScaledBy(-1); ttop = translate.ScaledBy(1);
|
|
}
|
|
bool flipBottom = translate.Dot(src->poly.normal) > 0;
|
|
|
|
// Get a triangulation of the source poly; this is not a closed mesh.
|
|
SMesh srcm; ZERO(&srcm);
|
|
(src->poly).TriangulateInto(&srcm);
|
|
|
|
// Do the bottom; that has normal pointing opposite from translate
|
|
meta.face = Remap(Entity::NO_ENTITY, REMAP_BOTTOM).v;
|
|
for(i = 0; i < srcm.l.n; i++) {
|
|
STriangle *st = &(srcm.l.elem[i]);
|
|
Vector at = (st->a).Plus(tbot),
|
|
bt = (st->b).Plus(tbot),
|
|
ct = (st->c).Plus(tbot);
|
|
if(flipBottom) {
|
|
thisMesh.AddTriangle(meta, ct, bt, at);
|
|
} else {
|
|
thisMesh.AddTriangle(meta, at, bt, ct);
|
|
}
|
|
}
|
|
// And the top; that has the normal pointing the same dir as translate
|
|
meta.face = Remap(Entity::NO_ENTITY, REMAP_TOP).v;
|
|
for(i = 0; i < srcm.l.n; i++) {
|
|
STriangle *st = &(srcm.l.elem[i]);
|
|
Vector at = (st->a).Plus(ttop),
|
|
bt = (st->b).Plus(ttop),
|
|
ct = (st->c).Plus(ttop);
|
|
if(flipBottom) {
|
|
thisMesh.AddTriangle(meta, at, bt, ct);
|
|
} else {
|
|
thisMesh.AddTriangle(meta, ct, bt, at);
|
|
}
|
|
}
|
|
srcm.Clear();
|
|
// Get the source polygon to extrude, and break it down to edges
|
|
edges.Clear();
|
|
(src->poly).MakeEdgesInto(&edges);
|
|
|
|
edges.l.ClearTags();
|
|
TagEdgesFromLineSegments(&edges);
|
|
// The sides; these are quads, represented as two triangles.
|
|
for(i = 0; i < edges.l.n; i++) {
|
|
SEdge *edge = &(edges.l.elem[i]);
|
|
Vector abot = (edge->a).Plus(tbot), bbot = (edge->b).Plus(tbot);
|
|
Vector atop = (edge->a).Plus(ttop), btop = (edge->b).Plus(ttop);
|
|
// We tagged the edges that came from line segments; their
|
|
// triangles should be associated with that plane face.
|
|
if(edge->tag) {
|
|
hEntity hl = { edge->tag };
|
|
hEntity hf = Remap(hl, REMAP_LINE_TO_FACE);
|
|
meta.face = hf.v;
|
|
} else {
|
|
meta.face = 0;
|
|
}
|
|
if(flipBottom) {
|
|
thisMesh.AddTriangle(meta, bbot, abot, atop);
|
|
thisMesh.AddTriangle(meta, bbot, atop, btop);
|
|
} else {
|
|
thisMesh.AddTriangle(meta, abot, bbot, atop);
|
|
thisMesh.AddTriangle(meta, bbot, btop, atop);
|
|
}
|
|
}
|
|
edges.Clear();
|
|
} else if(type == LATHE) {
|
|
SEdgeList edges;
|
|
ZERO(&edges);
|
|
int a, i;
|
|
|
|
Group *src = SS.GetGroup(opA);
|
|
(src->poly).MakeEdgesInto(&edges);
|
|
|
|
Vector orig = SS.GetEntity(predef.origin)->PointGetNum();
|
|
Vector axis = SS.GetEntity(predef.entityB)->VectorGetNum();
|
|
axis = axis.WithMagnitude(1);
|
|
|
|
// Calculate the max radius, to determine fineness of mesh
|
|
double r, rmax = 0;
|
|
for(i = 0; i < edges.l.n; i++) {
|
|
SEdge *edge = &(edges.l.elem[i]);
|
|
r = (edge->a).DistanceToLine(orig, axis);
|
|
rmax = max(r, rmax);
|
|
r = (edge->b).DistanceToLine(orig, axis);
|
|
rmax = max(r, rmax);
|
|
}
|
|
|
|
int n = SS.CircleSides(rmax);
|
|
for(a = 0; a < n; a++) {
|
|
double thetai = (2*PI*WRAP(a-1, n))/n, thetaf = (2*PI*a)/n;
|
|
for(i = 0; i < edges.l.n; i++) {
|
|
SEdge *edge = &(edges.l.elem[i]);
|
|
|
|
Vector ai = (edge->a).RotatedAbout(orig, axis, thetai);
|
|
Vector bi = (edge->b).RotatedAbout(orig, axis, thetai);
|
|
Vector af = (edge->a).RotatedAbout(orig, axis, thetaf);
|
|
Vector bf = (edge->b).RotatedAbout(orig, axis, thetaf);
|
|
|
|
Vector ab = (edge->b).Minus(edge->a);
|
|
Vector out = ((src->poly).normal).Cross(ab);
|
|
// This is a vector, not a point, so no origin for rotation
|
|
out = out.RotatedAbout(axis, thetai);
|
|
|
|
AddQuadWithNormal(meta, out, ai, bi, bf, af);
|
|
}
|
|
}
|
|
} else if(type == SWEEP) {
|
|
GenerateMeshForSweep();
|
|
} else if(type == IMPORTED) {
|
|
// Triangles are just copied over, with the appropriate transformation
|
|
// applied.
|
|
Vector offset = {
|
|
SS.GetParam(h.param(0))->val,
|
|
SS.GetParam(h.param(1))->val,
|
|
SS.GetParam(h.param(2))->val };
|
|
Quaternion q = {
|
|
SS.GetParam(h.param(3))->val,
|
|
SS.GetParam(h.param(4))->val,
|
|
SS.GetParam(h.param(5))->val,
|
|
SS.GetParam(h.param(6))->val };
|
|
|
|
for(int i = 0; i < impMesh.l.n; i++) {
|
|
STriangle st = impMesh.l.elem[i];
|
|
|
|
if(st.meta.face != 0) {
|
|
hEntity he = { st.meta.face };
|
|
st.meta.face = Remap(he, 0).v;
|
|
}
|
|
st.a = q.Rotate(st.a).Plus(offset);
|
|
st.b = q.Rotate(st.b).Plus(offset);
|
|
st.c = q.Rotate(st.c).Plus(offset);
|
|
thisMesh.AddTriangle(&st);
|
|
}
|
|
}
|
|
|
|
// So our group's mesh appears in thisMesh. Combine this with the previous
|
|
// group's mesh, using the requested operation.
|
|
runningMesh.Clear();
|
|
bool prevMeshError = meshError.yes;
|
|
meshError.yes = false;
|
|
meshError.interferesAt.Clear();
|
|
SMesh *a = PreviousGroupMesh();
|
|
if(meshCombine == COMBINE_AS_UNION) {
|
|
runningMesh.MakeFromUnion(a, &thisMesh);
|
|
} else if(meshCombine == COMBINE_AS_DIFFERENCE) {
|
|
runningMesh.MakeFromDifference(a, &thisMesh);
|
|
} else {
|
|
if(!runningMesh.MakeFromInterferenceCheck(a, &thisMesh,
|
|
&(meshError.interferesAt)))
|
|
{
|
|
meshError.yes = true;
|
|
// And the list of failed triangles goes in meshError.interferesAt
|
|
}
|
|
}
|
|
if(prevMeshError != meshError.yes) {
|
|
// The error is reported in the text window for the group.
|
|
SS.later.showTW = true;
|
|
}
|
|
}
|
|
|
|
SMesh *Group::PreviousGroupMesh(void) {
|
|
int i;
|
|
for(i = 0; i < SS.group.n; i++) {
|
|
Group *g = &(SS.group.elem[i]);
|
|
if(g->h.v == h.v) break;
|
|
}
|
|
if(i == 0 || i >= SS.group.n) oops();
|
|
return &(SS.group.elem[i-1].runningMesh);
|
|
}
|
|
|
|
void Group::Draw(void) {
|
|
// Show this even if the group is not visible. It's already possible
|
|
// to show or hide just this with the "show solids" flag.
|
|
|
|
int specColor;
|
|
if(type != EXTRUDE && type != IMPORTED && type != LATHE && type != SWEEP) {
|
|
specColor = RGB(25, 25, 25); // force the color to something dim
|
|
} else {
|
|
specColor = -1; // use the model color
|
|
}
|
|
// The back faces are drawn in red; should never seem them, since we
|
|
// draw closed shells, so that's a debugging aid.
|
|
GLfloat mpb[] = { 1.0f, 0.1f, 0.1f, 1.0 };
|
|
glMaterialfv(GL_BACK, GL_AMBIENT_AND_DIFFUSE, mpb);
|
|
|
|
// When we fill the mesh, we need to know which triangles are selected
|
|
// or hovered, in order to draw them differently.
|
|
DWORD mh = 0, ms1 = 0, ms2 = 0;
|
|
hEntity he = SS.GW.hover.entity;
|
|
if(he.v != 0 && SS.GetEntity(he)->IsFace()) {
|
|
mh = he.v;
|
|
}
|
|
SS.GW.GroupSelection();
|
|
if(gs.faces > 0) ms1 = gs.face[0].v;
|
|
if(gs.faces > 1) ms2 = gs.face[1].v;
|
|
|
|
glEnable(GL_LIGHTING);
|
|
if(SS.GW.showShaded) glxFillMesh(specColor, &runningMesh, mh, ms1, ms2);
|
|
glDisable(GL_LIGHTING);
|
|
|
|
if(meshError.yes) {
|
|
// Draw the error triangles in bright red stripes, with no Z buffering
|
|
GLubyte mask[32*32/8];
|
|
memset(mask, 0xf0, sizeof(mask));
|
|
glPolygonStipple(mask);
|
|
|
|
int specColor = 0;
|
|
glDisable(GL_DEPTH_TEST);
|
|
glColor3d(0, 0, 0);
|
|
glxFillMesh(0, &meshError.interferesAt, 0, 0, 0);
|
|
glEnable(GL_POLYGON_STIPPLE);
|
|
glColor3d(1, 0, 0);
|
|
glxFillMesh(0, &meshError.interferesAt, 0, 0, 0);
|
|
glEnable(GL_DEPTH_TEST);
|
|
glDisable(GL_POLYGON_STIPPLE);
|
|
}
|
|
|
|
if(SS.GW.showMesh) glxDebugMesh(&runningMesh);
|
|
|
|
// And finally show the polygons too
|
|
if(!SS.GW.showShaded) return;
|
|
if(polyError.how == POLY_NOT_CLOSED) {
|
|
glDisable(GL_DEPTH_TEST);
|
|
glxColor4d(1, 0, 0, 0.2);
|
|
glLineWidth(10);
|
|
glBegin(GL_LINES);
|
|
glxVertex3v(polyError.notClosedAt.a);
|
|
glxVertex3v(polyError.notClosedAt.b);
|
|
glEnd();
|
|
glLineWidth(1);
|
|
glxColor3d(1, 0, 0);
|
|
glPushMatrix();
|
|
glxTranslatev(polyError.notClosedAt.b);
|
|
glxOntoWorkplane(SS.GW.projRight, SS.GW.projUp);
|
|
glxWriteText("not closed contour!");
|
|
glPopMatrix();
|
|
glEnable(GL_DEPTH_TEST);
|
|
} else if(polyError.how == POLY_NOT_COPLANAR) {
|
|
glDisable(GL_DEPTH_TEST);
|
|
glxColor3d(1, 0, 0);
|
|
glPushMatrix();
|
|
glxTranslatev(polyError.notCoplanarAt);
|
|
glxOntoWorkplane(SS.GW.projRight, SS.GW.projUp);
|
|
glxWriteText("points not all coplanar!");
|
|
glPopMatrix();
|
|
glEnable(GL_DEPTH_TEST);
|
|
} else {
|
|
glxColor4d(0, 0.1, 0.1, 0.5);
|
|
glxDepthRangeOffset(1);
|
|
glxFillPolygon(&poly);
|
|
glxDepthRangeOffset(0);
|
|
}
|
|
}
|
|
|