Add sweeps. The user specifies a trajectory and a section, in two
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]solver
parent
eb0b63f5dd
commit
5a22982e05
4
Makefile
4
Makefile
|
@ -1,5 +1,7 @@
|
|||
DEFINES = /D_WIN32_WINNT=0x500 /DISOLATION_AWARE_ENABLED /D_WIN32_IE=0x500 /DWIN32_LEAN_AND_MEAN /DWIN32
|
||||
CFLAGS = /W3 /nologo -Iextlib -I..\common\win32 /D_DEBUG /D_CRT_SECURE_NO_WARNINGS /I. /Zi /EHs
|
||||
# Use the multi-threaded static libc because libpng and zlib do; not sure if anything bad
|
||||
# happens if those mix, but don't want to risk it.
|
||||
CFLAGS = /W3 /nologo -MT -Iextlib -I..\common\win32 /D_DEBUG /D_CRT_SECURE_NO_WARNINGS /I. /Zi /EHs
|
||||
|
||||
HEADERS = ..\common\win32\freeze.h ui.h solvespace.h dsc.h sketch.h expr.h polygon.h
|
||||
|
||||
|
|
2
draw.cpp
2
draw.cpp
|
@ -804,7 +804,7 @@ void GraphicsWindow::HitTestMakeSelection(Point2d mp) {
|
|||
|
||||
// Faces, from the triangle mesh; these are lowest priority
|
||||
if(s.constraint.v == 0 && s.entity.v == 0 && showShaded && showFaces) {
|
||||
SMesh *m = &((SS.GetGroup(activeGroup))->mesh);
|
||||
SMesh *m = &((SS.GetGroup(activeGroup))->runningMesh);
|
||||
DWORD v = m->FirstIntersectionWith(mp);
|
||||
if(v) {
|
||||
s.entity.v = v;
|
||||
|
|
2
dsc.h
2
dsc.h
|
@ -55,6 +55,8 @@ public:
|
|||
Vector Normal(int which);
|
||||
Vector RotatedAbout(Vector orig, Vector axis, double theta);
|
||||
Vector RotatedAbout(Vector axis, double theta);
|
||||
Vector DotInToCsys(Vector u, Vector v, Vector n);
|
||||
Vector ScaleOutOfCsys(Vector u, Vector v, Vector n);
|
||||
double DistanceToLine(Vector p0, Vector dp);
|
||||
Vector ClosestPointOnLine(Vector p0, Vector dp);
|
||||
double Magnitude(void);
|
||||
|
|
3
file.cpp
3
file.cpp
|
@ -65,6 +65,7 @@ const SolveSpace::SaveTable SolveSpace::SAVED[] = {
|
|||
{ 'g', "Group.name", 'N', &(SS.sv.g.name) },
|
||||
{ 'g', "Group.activeWorkplane.v", 'x', &(SS.sv.g.activeWorkplane.v) },
|
||||
{ 'g', "Group.opA.v", 'x', &(SS.sv.g.opA.v) },
|
||||
{ 'g', "Group.opB.v", 'x', &(SS.sv.g.opB.v) },
|
||||
{ 'g', "Group.valA", 'f', &(SS.sv.g.valA) },
|
||||
{ 'g', "Group.color", 'x', &(SS.sv.g.color) },
|
||||
{ 'g', "Group.subtype", 'd', &(SS.sv.g.subtype) },
|
||||
|
@ -219,7 +220,7 @@ bool SolveSpace::SaveToFile(char *filename) {
|
|||
fprintf(fh, "AddConstraint\n\n");
|
||||
}
|
||||
|
||||
SMesh *m = &(group.elem[group.n-1].mesh);
|
||||
SMesh *m = &(group.elem[group.n-1].runningMesh);
|
||||
for(i = 0; i < m->l.n; i++) {
|
||||
STriangle *tr = &(m->l.elem[i]);
|
||||
fprintf(fh, "Triangle %08x %08x "
|
||||
|
|
|
@ -39,14 +39,15 @@ const GraphicsWindow::MenuEntry GraphicsWindow::menu[] = {
|
|||
{ 1, "Dimensions in &Millimeters", MNU_UNITS_MM, 0, mView },
|
||||
|
||||
{ 0, "&New Group", 0, 0, NULL },
|
||||
{ 1, "&Drawing in 3d\tShift+Ctrl+D", MNU_GROUP_3D, 'D'|S|C, mGrp },
|
||||
{ 1, "Drawing in Workplane\tShift+Ctrl+W", MNU_GROUP_WRKPL, 'W'|S|C, mGrp },
|
||||
{ 1, "&Drawing in 3d\tShift+Ctrl+D", MNU_GROUP_3D, 'D'|S|C,mGrp },
|
||||
{ 1, "Drawing in Workplane\tShift+Ctrl+W", MNU_GROUP_WRKPL, 'W'|S|C,mGrp },
|
||||
{ 1, NULL, 0, NULL },
|
||||
{ 1, "Step &Translating\tShift+Ctrl+R", MNU_GROUP_TRANS, 'T'|S|C,mGrp },
|
||||
{ 1, "Step &Rotating\tShift+Ctrl+T", MNU_GROUP_ROT, 'R'|S|C,mGrp },
|
||||
{ 1, NULL, 0, 0, NULL },
|
||||
{ 1, "Extrude\tShift+Ctrl+X", MNU_GROUP_EXTRUDE, 'X'|S|C,mGrp },
|
||||
{ 1, "Lathe\tShift+Ctrl+L", MNU_GROUP_LATHE, 'L'|S|C,mGrp },
|
||||
{ 1, "E&xtrude\tShift+Ctrl+X", MNU_GROUP_EXTRUDE, 'X'|S|C,mGrp },
|
||||
{ 1, "&Lathe\tShift+Ctrl+L", MNU_GROUP_LATHE, 'L'|S|C,mGrp },
|
||||
{ 1, "&Sweep\tShift+Ctrl+S", MNU_GROUP_SWEEP, 'S'|S|C,mGrp },
|
||||
{ 1, NULL, 0, 0, NULL },
|
||||
{ 1, "Import / Assemble...\tShift+Ctrl+I", MNU_GROUP_IMPORT, 'I'|S|C,mGrp },
|
||||
{11, "Import Recent", MNU_GROUP_RECENT, 0, mGrp },
|
||||
|
@ -235,8 +236,8 @@ void GraphicsWindow::LoopOverPoints(
|
|||
HandlePointForZoomToFit(e->PointGetNum(), pmax, pmin, wmin, div);
|
||||
}
|
||||
Group *g = SS.GetGroup(activeGroup);
|
||||
for(i = 0; i < g->mesh.l.n; i++) {
|
||||
STriangle *tr = &(g->mesh.l.elem[i]);
|
||||
for(i = 0; i < g->runningMesh.l.n; i++) {
|
||||
STriangle *tr = &(g->runningMesh.l.elem[i]);
|
||||
HandlePointForZoomToFit(tr->a, pmax, pmin, wmin, div);
|
||||
HandlePointForZoomToFit(tr->b, pmax, pmin, wmin, div);
|
||||
HandlePointForZoomToFit(tr->c, pmax, pmin, wmin, div);
|
||||
|
|
28
group.cpp
28
group.cpp
|
@ -100,6 +100,30 @@ void Group::MenuGroup(int id) {
|
|||
SS.GW.ClearSelection();
|
||||
break;
|
||||
|
||||
case GraphicsWindow::MNU_GROUP_SWEEP: {
|
||||
g.type = SWEEP;
|
||||
// Get the group one before the active group; that's our
|
||||
// trajectory
|
||||
int i;
|
||||
for(i = 1; i < SS.group.n - 1; i++) {
|
||||
Group *gnext = &(SS.group.elem[i+1]);
|
||||
if(gnext->h.v == SS.GW.activeGroup.v) {
|
||||
g.opA = SS.group.elem[i].h;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(i >= SS.group.n - 1) {
|
||||
Error("At least one sketch before the active sketch must "
|
||||
"exist; that specifies the sweep trajectory.");
|
||||
return;
|
||||
}
|
||||
// The active group is our section
|
||||
g.opB = SS.GW.activeGroup;
|
||||
g.color = RGB(100, 100, 100);
|
||||
g.name.strcpy("sweep");
|
||||
break;
|
||||
}
|
||||
|
||||
case GraphicsWindow::MNU_GROUP_ROT: {
|
||||
if(gs.points == 1 && gs.n == 1 && SS.GW.LockedInWorkplane()) {
|
||||
g.predef.origin = gs.point[0];
|
||||
|
@ -287,6 +311,10 @@ void Group::Generate(IdList<Entity,hEntity> *entity,
|
|||
break;
|
||||
}
|
||||
|
||||
case SWEEP: {
|
||||
break;
|
||||
}
|
||||
|
||||
case TRANSLATE: {
|
||||
// The translation vector
|
||||
AddParam(param, h.param(0), gp.x);
|
||||
|
|
273
groupmesh.cpp
273
groupmesh.cpp
|
@ -36,9 +36,209 @@ void Group::GeneratePolygon(void) {
|
|||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
SMesh outm;
|
||||
ZERO(&outm);
|
||||
thisMesh.Clear();
|
||||
STriMeta meta = { 0, color };
|
||||
|
||||
if(type == EXTRUDE) {
|
||||
SEdgeList edges;
|
||||
ZERO(&edges);
|
||||
|
@ -58,8 +258,6 @@ void Group::GenerateMesh(void) {
|
|||
SMesh srcm; ZERO(&srcm);
|
||||
(src->poly).TriangulateInto(&srcm);
|
||||
|
||||
STriMeta meta = { 0, color };
|
||||
|
||||
// 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++) {
|
||||
|
@ -68,9 +266,9 @@ void Group::GenerateMesh(void) {
|
|||
bt = (st->b).Plus(tbot),
|
||||
ct = (st->c).Plus(tbot);
|
||||
if(flipBottom) {
|
||||
outm.AddTriangle(meta, ct, bt, at);
|
||||
thisMesh.AddTriangle(meta, ct, bt, at);
|
||||
} else {
|
||||
outm.AddTriangle(meta, at, bt, ct);
|
||||
thisMesh.AddTriangle(meta, at, bt, ct);
|
||||
}
|
||||
}
|
||||
// And the top; that has the normal pointing the same dir as translate
|
||||
|
@ -81,9 +279,9 @@ void Group::GenerateMesh(void) {
|
|||
bt = (st->b).Plus(ttop),
|
||||
ct = (st->c).Plus(ttop);
|
||||
if(flipBottom) {
|
||||
outm.AddTriangle(meta, at, bt, ct);
|
||||
thisMesh.AddTriangle(meta, at, bt, ct);
|
||||
} else {
|
||||
outm.AddTriangle(meta, ct, bt, at);
|
||||
thisMesh.AddTriangle(meta, ct, bt, at);
|
||||
}
|
||||
}
|
||||
srcm.Clear();
|
||||
|
@ -108,11 +306,11 @@ void Group::GenerateMesh(void) {
|
|||
meta.face = 0;
|
||||
}
|
||||
if(flipBottom) {
|
||||
outm.AddTriangle(meta, bbot, abot, atop);
|
||||
outm.AddTriangle(meta, bbot, atop, btop);
|
||||
thisMesh.AddTriangle(meta, bbot, abot, atop);
|
||||
thisMesh.AddTriangle(meta, bbot, atop, btop);
|
||||
} else {
|
||||
outm.AddTriangle(meta, abot, bbot, atop);
|
||||
outm.AddTriangle(meta, bbot, btop, atop);
|
||||
thisMesh.AddTriangle(meta, abot, bbot, atop);
|
||||
thisMesh.AddTriangle(meta, bbot, btop, atop);
|
||||
}
|
||||
}
|
||||
edges.Clear();
|
||||
|
@ -124,7 +322,6 @@ void Group::GenerateMesh(void) {
|
|||
Group *src = SS.GetGroup(opA);
|
||||
(src->poly).MakeEdgesInto(&edges);
|
||||
|
||||
STriMeta meta = { 0, color };
|
||||
Vector orig = SS.GetEntity(predef.origin)->PointGetNum();
|
||||
Vector axis = SS.GetEntity(predef.entityB)->VectorGetNum();
|
||||
axis = axis.WithMagnitude(1);
|
||||
|
@ -140,14 +337,11 @@ void Group::GenerateMesh(void) {
|
|||
}
|
||||
|
||||
int n = SS.CircleSides(rmax);
|
||||
for(a = 0; a <= n; a++) {
|
||||
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]);
|
||||
|
||||
double da = (edge->a).DistanceToLine(orig, axis);
|
||||
double db = (edge->b).DistanceToLine(orig, axis);
|
||||
|
||||
Vector ai = (edge->a).RotatedAbout(orig, axis, thetai);
|
||||
Vector bi = (edge->b).RotatedAbout(orig, axis, thetai);
|
||||
Vector af = (edge->a).RotatedAbout(orig, axis, thetaf);
|
||||
|
@ -158,24 +352,11 @@ void Group::GenerateMesh(void) {
|
|||
// This is a vector, not a point, so no origin for rotation
|
||||
out = out.RotatedAbout(axis, thetai);
|
||||
|
||||
// The line sweeps out a quad, so two triangles
|
||||
STriangle quad1 = STriangle::From(meta, ai, bi, af),
|
||||
quad2 = STriangle::From(meta, af, bi, bf);
|
||||
|
||||
// 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(da >= LENGTH_EPS) outm.AddTriangle(&quad1);
|
||||
if(db >= LENGTH_EPS) outm.AddTriangle(&quad2);
|
||||
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.
|
||||
|
@ -199,31 +380,33 @@ void Group::GenerateMesh(void) {
|
|||
st.a = q.Rotate(st.a).Plus(offset);
|
||||
st.b = q.Rotate(st.b).Plus(offset);
|
||||
st.c = q.Rotate(st.c).Plus(offset);
|
||||
outm.AddTriangle(&st);
|
||||
thisMesh.AddTriangle(&st);
|
||||
}
|
||||
}
|
||||
|
||||
// So our group's mesh appears in outm. Combine this with the previous
|
||||
// So our group's mesh appears in thisMesh. Combine this with the previous
|
||||
// group's mesh, using the requested operation.
|
||||
mesh.Clear();
|
||||
runningMesh.Clear();
|
||||
bool prevMeshError = meshError.yes;
|
||||
meshError.yes = false;
|
||||
meshError.interferesAt.Clear();
|
||||
SMesh *a = PreviousGroupMesh();
|
||||
if(meshCombine == COMBINE_AS_UNION) {
|
||||
mesh.MakeFromUnion(a, &outm);
|
||||
runningMesh.MakeFromUnion(a, &thisMesh);
|
||||
} else if(meshCombine == COMBINE_AS_DIFFERENCE) {
|
||||
mesh.MakeFromDifference(a, &outm);
|
||||
runningMesh.MakeFromDifference(a, &thisMesh);
|
||||
} else {
|
||||
if(!mesh.MakeFromInterferenceCheck(a, &outm, &(meshError.interferesAt)))
|
||||
if(!runningMesh.MakeFromInterferenceCheck(a, &thisMesh,
|
||||
&(meshError.interferesAt)))
|
||||
{
|
||||
meshError.yes = true;
|
||||
// And the list of failed triangles appears in meshError.interferesAt
|
||||
// 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;
|
||||
}
|
||||
outm.Clear();
|
||||
}
|
||||
|
||||
SMesh *Group::PreviousGroupMesh(void) {
|
||||
|
@ -233,7 +416,7 @@ SMesh *Group::PreviousGroupMesh(void) {
|
|||
if(g->h.v == h.v) break;
|
||||
}
|
||||
if(i == 0 || i >= SS.group.n) oops();
|
||||
return &(SS.group.elem[i-1].mesh);
|
||||
return &(SS.group.elem[i-1].runningMesh);
|
||||
}
|
||||
|
||||
void Group::Draw(void) {
|
||||
|
@ -241,7 +424,7 @@ void Group::Draw(void) {
|
|||
// to show or hide just this with the "show solids" flag.
|
||||
|
||||
int specColor;
|
||||
if(type != EXTRUDE && type != IMPORTED && type != LATHE) {
|
||||
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
|
||||
|
@ -263,7 +446,7 @@ void Group::Draw(void) {
|
|||
if(gs.faces > 1) ms2 = gs.face[1].v;
|
||||
|
||||
glEnable(GL_LIGHTING);
|
||||
if(SS.GW.showShaded) glxFillMesh(specColor, &mesh, mh, ms1, ms2);
|
||||
if(SS.GW.showShaded) glxFillMesh(specColor, &runningMesh, mh, ms1, ms2);
|
||||
glDisable(GL_LIGHTING);
|
||||
|
||||
if(meshError.yes) {
|
||||
|
@ -283,7 +466,7 @@ void Group::Draw(void) {
|
|||
glDisable(GL_POLYGON_STIPPLE);
|
||||
}
|
||||
|
||||
if(SS.GW.showMesh) glxDebugMesh(&mesh);
|
||||
if(SS.GW.showMesh) glxDebugMesh(&runningMesh);
|
||||
|
||||
// And finally show the polygons too
|
||||
if(!SS.GW.showShaded) return;
|
||||
|
|
3
mesh.cpp
3
mesh.cpp
|
@ -4,14 +4,13 @@ void SMesh::Clear(void) {
|
|||
l.Clear();
|
||||
}
|
||||
|
||||
void SMesh::AddTriangle(Vector n, Vector a, Vector b, Vector c) {
|
||||
void SMesh::AddTriangle(STriMeta meta, Vector n, Vector a, Vector b, Vector c) {
|
||||
Vector ab = b.Minus(a), bc = c.Minus(b);
|
||||
Vector np = ab.Cross(bc);
|
||||
if(np.Magnitude() < 1e-10) {
|
||||
// ugh; gl sometimes tesselates to collinear triangles
|
||||
return;
|
||||
}
|
||||
STriMeta meta; ZERO(&meta);
|
||||
if(np.Dot(n) > 0) {
|
||||
AddTriangle(meta, a, b, c);
|
||||
} else {
|
||||
|
|
133
polygon.cpp
133
polygon.cpp
|
@ -54,6 +54,46 @@ void SEdgeList::AddEdge(Vector a, Vector b) {
|
|||
l.Add(&e);
|
||||
}
|
||||
|
||||
bool SEdgeList::AssembleContour(Vector first, Vector last,
|
||||
SContour *dest, SEdge *errorAt)
|
||||
{
|
||||
int i;
|
||||
|
||||
dest->AddPoint(first);
|
||||
dest->AddPoint(last);
|
||||
|
||||
do {
|
||||
for(i = 0; i < l.n; i++) {
|
||||
SEdge *se = &(l.elem[i]);
|
||||
if(se->tag) continue;
|
||||
|
||||
if(se->a.Equals(last)) {
|
||||
dest->AddPoint(se->b);
|
||||
last = se->b;
|
||||
se->tag = 1;
|
||||
break;
|
||||
}
|
||||
if(se->b.Equals(last)) {
|
||||
dest->AddPoint(se->a);
|
||||
last = se->a;
|
||||
se->tag = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(i >= l.n) {
|
||||
// Couldn't assemble a closed contour; mark where.
|
||||
if(errorAt) {
|
||||
errorAt->a = first;
|
||||
errorAt->b = last;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
} while(!last.Equals(first));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SEdgeList::AssemblePolygon(SPolygon *dest, SEdge *errorAt) {
|
||||
dest->Clear();
|
||||
|
||||
|
@ -72,40 +112,22 @@ bool SEdgeList::AssemblePolygon(SPolygon *dest, SEdge *errorAt) {
|
|||
return true;
|
||||
}
|
||||
|
||||
// Create a new empty contour in our polygon, and finish assembling
|
||||
// into that contour.
|
||||
dest->AddEmptyContour();
|
||||
dest->AddPoint(first);
|
||||
dest->AddPoint(last);
|
||||
do {
|
||||
for(i = 0; i < l.n; i++) {
|
||||
SEdge *se = &(l.elem[i]);
|
||||
if(se->tag) continue;
|
||||
|
||||
if(se->a.Equals(last)) {
|
||||
dest->AddPoint(se->b);
|
||||
last = se->b;
|
||||
se->tag = 1;
|
||||
break;
|
||||
}
|
||||
if(se->b.Equals(last)) {
|
||||
dest->AddPoint(se->a);
|
||||
last = se->a;
|
||||
se->tag = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(i >= l.n) {
|
||||
// Couldn't assemble a closed contour; mark where.
|
||||
if(errorAt) {
|
||||
errorAt->a = first;
|
||||
errorAt->b = last;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
} while(!last.Equals(first));
|
||||
if(!AssembleContour(first, last, &(dest->l.elem[dest->l.n-1]), errorAt))
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void SContour::AddPoint(Vector p) {
|
||||
SPoint sp;
|
||||
sp.tag = 0;
|
||||
sp.p = p;
|
||||
|
||||
l.Add(&sp);
|
||||
}
|
||||
|
||||
void SContour::MakeEdgesInto(SEdgeList *el) {
|
||||
int i;
|
||||
for(i = 0; i < (l.n-1); i++) {
|
||||
|
@ -216,17 +238,6 @@ void SPolygon::AddEmptyContour(void) {
|
|||
l.Add(&c);
|
||||
}
|
||||
|
||||
void SPolygon::AddPoint(Vector p) {
|
||||
if(l.n < 1) oops();
|
||||
|
||||
SPoint sp;
|
||||
sp.tag = 0;
|
||||
sp.p = p;
|
||||
|
||||
// Add to the last contour in the list
|
||||
(l.elem[l.n-1]).l.Add(&sp);
|
||||
}
|
||||
|
||||
void SPolygon::MakeEdgesInto(SEdgeList *el) {
|
||||
int i;
|
||||
for(i = 0; i < l.n; i++) {
|
||||
|
@ -240,15 +251,19 @@ Vector SPolygon::ComputeNormal(void) {
|
|||
}
|
||||
|
||||
bool SPolygon::ContainsPoint(Vector p) {
|
||||
bool inside = false;
|
||||
return (WindingNumberForPoint(p) % 2) == 1;
|
||||
}
|
||||
|
||||
int SPolygon::WindingNumberForPoint(Vector p) {
|
||||
int winding = 0;
|
||||
int i;
|
||||
for(i = 0; i < l.n; i++) {
|
||||
SContour *sc = &(l.elem[i]);
|
||||
if(sc->ContainsPointProjdToNormal(normal, p)) {
|
||||
inside = !inside;
|
||||
winding++;
|
||||
}
|
||||
}
|
||||
return inside;
|
||||
return winding;
|
||||
}
|
||||
|
||||
void SPolygon::FixContourDirections(void) {
|
||||
|
@ -275,10 +290,20 @@ void SPolygon::FixContourDirections(void) {
|
|||
}
|
||||
}
|
||||
|
||||
bool SPolygon::AllPointsInPlane(Vector *notCoplanarAt) {
|
||||
bool SPolygon::IsEmpty(void) {
|
||||
if(l.n == 0 || l.elem[0].l.n == 0) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
Vector p0 = l.elem[0].l.elem[0].p;
|
||||
Vector SPolygon::AnyPoint(void) {
|
||||
if(IsEmpty()) oops();
|
||||
return l.elem[0].l.elem[0].p;
|
||||
}
|
||||
|
||||
bool SPolygon::AllPointsInPlane(Vector *notCoplanarAt) {
|
||||
if(IsEmpty()) return true;
|
||||
|
||||
Vector p0 = AnyPoint();
|
||||
double d = normal.Dot(p0);
|
||||
|
||||
for(int i = 0; i < l.n; i++) {
|
||||
|
@ -293,6 +318,7 @@ static int TriMode, TriVertexCount;
|
|||
static Vector Tri1, TriNMinus1, TriNMinus2;
|
||||
static Vector TriNormal;
|
||||
static SMesh *TriMesh;
|
||||
static STriMeta TriMeta;
|
||||
static void GLX_CALLBACK TriBegin(int mode)
|
||||
{
|
||||
TriMode = mode;
|
||||
|
@ -308,15 +334,18 @@ static void GLX_CALLBACK TriVertex(Vector *triN)
|
|||
}
|
||||
if(TriMode == GL_TRIANGLES) {
|
||||
if((TriVertexCount % 3) == 2) {
|
||||
TriMesh->AddTriangle(TriNormal, TriNMinus2, TriNMinus1, *triN);
|
||||
TriMesh->AddTriangle(
|
||||
TriMeta, TriNormal, TriNMinus2, TriNMinus1, *triN);
|
||||
}
|
||||
} else if(TriMode == GL_TRIANGLE_FAN) {
|
||||
if(TriVertexCount >= 2) {
|
||||
TriMesh->AddTriangle(TriNormal, Tri1, TriNMinus1, *triN);
|
||||
TriMesh->AddTriangle(
|
||||
TriMeta, TriNormal, Tri1, TriNMinus1, *triN);
|
||||
}
|
||||
} else if(TriMode == GL_TRIANGLE_STRIP) {
|
||||
if(TriVertexCount >= 2) {
|
||||
TriMesh->AddTriangle(TriNormal, TriNMinus2, TriNMinus1, *triN);
|
||||
TriMesh->AddTriangle(
|
||||
TriMeta, TriNormal, TriNMinus2, TriNMinus1, *triN);
|
||||
}
|
||||
} else oops();
|
||||
|
||||
|
@ -325,8 +354,14 @@ static void GLX_CALLBACK TriVertex(Vector *triN)
|
|||
TriVertexCount++;
|
||||
}
|
||||
void SPolygon::TriangulateInto(SMesh *m) {
|
||||
STriMeta meta;
|
||||
ZERO(&meta);
|
||||
TriangulateInto(m, meta);
|
||||
}
|
||||
void SPolygon::TriangulateInto(SMesh *m, STriMeta meta) {
|
||||
TriMesh = m;
|
||||
TriNormal = normal;
|
||||
TriMeta = meta;
|
||||
|
||||
GLUtesselator *gt = gluNewTess();
|
||||
gluTessCallback(gt, GLU_TESS_BEGIN, (glxCallbackFptr *)TriBegin);
|
||||
|
|
21
polygon.h
21
polygon.h
|
@ -3,6 +3,7 @@
|
|||
#define __POLYGON_H
|
||||
|
||||
class SPolygon;
|
||||
class SContour;
|
||||
class SMesh;
|
||||
class SBsp3;
|
||||
|
||||
|
@ -67,6 +68,8 @@ public:
|
|||
void Clear(void);
|
||||
void AddEdge(Vector a, Vector b);
|
||||
bool AssemblePolygon(SPolygon *dest, SEdge *errorAt);
|
||||
bool AssembleContour(Vector first, Vector last, SContour *dest,
|
||||
SEdge *errorAt);
|
||||
};
|
||||
|
||||
class SPoint {
|
||||
|
@ -79,6 +82,7 @@ class SContour {
|
|||
public:
|
||||
SList<SPoint> l;
|
||||
|
||||
void AddPoint(Vector p);
|
||||
void MakeEdgesInto(SEdgeList *el);
|
||||
void Reverse(void);
|
||||
Vector ComputeNormal(void);
|
||||
|
@ -87,6 +91,11 @@ public:
|
|||
bool AllPointsInPlane(Vector n, double d, Vector *notCoplanarAt);
|
||||
};
|
||||
|
||||
typedef struct {
|
||||
DWORD face;
|
||||
int color;
|
||||
} STriMeta;
|
||||
|
||||
class SPolygon {
|
||||
public:
|
||||
SList<SContour> l;
|
||||
|
@ -94,19 +103,18 @@ public:
|
|||
|
||||
Vector ComputeNormal(void);
|
||||
void AddEmptyContour(void);
|
||||
void AddPoint(Vector p);
|
||||
int WindingNumberForPoint(Vector p);
|
||||
bool ContainsPoint(Vector p);
|
||||
void MakeEdgesInto(SEdgeList *el);
|
||||
void FixContourDirections(void);
|
||||
void TriangulateInto(SMesh *m);
|
||||
void TriangulateInto(SMesh *m, STriMeta meta);
|
||||
void Clear(void);
|
||||
bool AllPointsInPlane(Vector *notCoplanarAt);
|
||||
bool IsEmpty(void);
|
||||
Vector AnyPoint(void);
|
||||
};
|
||||
|
||||
typedef struct {
|
||||
DWORD face;
|
||||
int color;
|
||||
} STriMeta;
|
||||
class STriangle {
|
||||
public:
|
||||
int tag;
|
||||
|
@ -116,6 +124,7 @@ public:
|
|||
static STriangle From(STriMeta meta, Vector a, Vector b, Vector c);
|
||||
Vector Normal(void);
|
||||
void FlipNormal(void);
|
||||
int WindingNumberForPoint(Vector p);
|
||||
bool ContainsPoint(Vector p);
|
||||
bool ContainsPointProjd(Vector n, Vector p);
|
||||
};
|
||||
|
@ -185,7 +194,7 @@ public:
|
|||
void Clear(void);
|
||||
void AddTriangle(STriangle *st);
|
||||
void AddTriangle(STriMeta meta, Vector a, Vector b, Vector c);
|
||||
void AddTriangle(Vector n, Vector a, Vector b, Vector c);
|
||||
void AddTriangle(STriMeta meta, Vector n, Vector a, Vector b, Vector c);
|
||||
void DoBounding(Vector v, Vector *vmax, Vector *vmin);
|
||||
void GetBounding(Vector *vmax, Vector *vmin);
|
||||
|
||||
|
|
13
sketch.h
13
sketch.h
|
@ -82,12 +82,14 @@ public:
|
|||
static const int DRAWING_WORKPLANE = 5001;
|
||||
static const int EXTRUDE = 5100;
|
||||
static const int LATHE = 5101;
|
||||
static const int SWEEP = 5102;
|
||||
static const int ROTATE = 5200;
|
||||
static const int TRANSLATE = 5201;
|
||||
static const int IMPORTED = 5300;
|
||||
int type;
|
||||
|
||||
hGroup opA;
|
||||
hGroup opB;
|
||||
bool visible;
|
||||
bool clean;
|
||||
hEntity activeWorkplane;
|
||||
|
@ -130,7 +132,8 @@ public:
|
|||
Vector notCoplanarAt;
|
||||
} polyError;
|
||||
|
||||
SMesh mesh;
|
||||
SMesh thisMesh;
|
||||
SMesh runningMesh;
|
||||
struct {
|
||||
SMesh interferesAt;
|
||||
bool yes;
|
||||
|
@ -179,8 +182,14 @@ public:
|
|||
void AddEq(IdList<Equation,hEquation> *l, Expr *expr, int index);
|
||||
void GenerateEquations(IdList<Equation,hEquation> *l);
|
||||
|
||||
SMesh *PreviousGroupMesh(void);
|
||||
// Assembling piecewise linear sections into polygons
|
||||
void GeneratePolygon(void);
|
||||
// And the mesh stuff
|
||||
SMesh *PreviousGroupMesh(void);
|
||||
void GetTrajectory(hGroup hg, SContour *traj, SPolygon *section);
|
||||
void AddQuadWithNormal(STriMeta meta, Vector out,
|
||||
Vector a, Vector b, Vector c, Vector d);
|
||||
void GenerateMeshForSweep(void);
|
||||
void GenerateMesh(void);
|
||||
void Draw(void);
|
||||
|
||||
|
|
|
@ -133,10 +133,15 @@ void SolveSpace::AfterNewFile(void) {
|
|||
GetGraphicsWindowSize(&w, &h);
|
||||
GW.width = w;
|
||||
GW.height = h;
|
||||
|
||||
// The triangles haven't been generated yet, but zoom to fit the entities
|
||||
// roughly in the window, since that sets the mesh tolerance.
|
||||
GW.ZoomToFit();
|
||||
|
||||
GenerateAll(0, INT_MAX);
|
||||
later.showTW = true;
|
||||
// Then zoom to fit again, to fit the triangles
|
||||
GW.ZoomToFit();
|
||||
}
|
||||
|
||||
void SolveSpace::MarkGroupDirtyByEntity(hEntity he) {
|
||||
|
@ -481,6 +486,11 @@ void SolveSpace::ExportAsPngTo(char *filename) {
|
|||
|
||||
png_init_io(png_ptr, f);
|
||||
|
||||
// glReadPixels wants to align things on 4-boundaries, and there's 3
|
||||
// bytes per pixel. As long as the row width is divisible by 4, all
|
||||
// works out.
|
||||
w &= ~3; h &= ~3;
|
||||
|
||||
png_set_IHDR(png_ptr, info_ptr, w, h,
|
||||
8, PNG_COLOR_TYPE_RGB, PNG_INTERLACE_NONE,
|
||||
PNG_COMPRESSION_TYPE_DEFAULT,PNG_FILTER_TYPE_DEFAULT);
|
||||
|
|
|
@ -734,6 +734,7 @@ void TextWindow::ShowGroupInfo(void) {
|
|||
|
||||
if(g->type == Group::EXTRUDE ||
|
||||
g->type == Group::LATHE ||
|
||||
g->type == Group::SWEEP ||
|
||||
g->type == Group::IMPORTED)
|
||||
{
|
||||
bool un = (g->meshCombine == Group::COMBINE_AS_UNION);
|
||||
|
@ -759,7 +760,10 @@ void TextWindow::ShowGroupInfo(void) {
|
|||
Printf(false, "%Fx the parts interfere!");
|
||||
}
|
||||
|
||||
if(g->type == Group::EXTRUDE || g->type == Group::LATHE) {
|
||||
if(g->type == Group::EXTRUDE ||
|
||||
g->type == Group::LATHE ||
|
||||
g->type == Group::SWEEP)
|
||||
{
|
||||
#define TWOX(v) v v
|
||||
Printf(true, "%FtM_COLOR%E " TWOX(TWOX(TWOX("%Bp%D%f%Ln %Bd%E "))),
|
||||
0x80000000 | SS.modelColor[0], 0, &TextWindow::ScreenColor,
|
||||
|
@ -913,7 +917,7 @@ void TextWindow::ShowConfiguration(void) {
|
|||
Printf(false, "%Ba %2 %Fl%Ll%f%D[change]%E; now %d triangles",
|
||||
SS.meshTol,
|
||||
&ScreenChangeMeshTolerance, 0,
|
||||
SS.group.elem[SS.group.n-1].mesh.l.n);
|
||||
SS.group.elem[SS.group.n-1].runningMesh.l.n);
|
||||
|
||||
Printf(false, "");
|
||||
Printf(false, "%Ft perspective factor (0 for isometric)%E");
|
||||
|
|
1
ui.h
1
ui.h
|
@ -163,6 +163,7 @@ public:
|
|||
MNU_GROUP_WRKPL,
|
||||
MNU_GROUP_EXTRUDE,
|
||||
MNU_GROUP_LATHE,
|
||||
MNU_GROUP_SWEEP,
|
||||
MNU_GROUP_ROT,
|
||||
MNU_GROUP_TRANS,
|
||||
MNU_GROUP_IMPORT,
|
||||
|
|
|
@ -48,7 +48,8 @@ void SolveSpace::PushFromCurrentOnto(UndoStack *uk) {
|
|||
ZERO(&(dest.solved));
|
||||
ZERO(&(dest.poly));
|
||||
ZERO(&(dest.polyError));
|
||||
ZERO(&(dest.mesh));
|
||||
ZERO(&(dest.thisMesh));
|
||||
ZERO(&(dest.runningMesh));
|
||||
ZERO(&(dest.meshError));
|
||||
|
||||
ZERO(&(dest.remap));
|
||||
|
@ -87,7 +88,8 @@ void SolveSpace::PopOntoCurrentFrom(UndoStack *uk) {
|
|||
for(i = 0; i < group.n; i++) {
|
||||
Group *g = &(group.elem[i]);
|
||||
g->poly.Clear();
|
||||
g->mesh.Clear();
|
||||
g->thisMesh.Clear();
|
||||
g->runningMesh.Clear();
|
||||
g->meshError.interferesAt.Clear();
|
||||
g->remap.Clear();
|
||||
g->impMesh.Clear();
|
||||
|
|
16
util.cpp
16
util.cpp
|
@ -310,6 +310,22 @@ Vector Vector::RotatedAbout(Vector axis, double theta) {
|
|||
return r;
|
||||
}
|
||||
|
||||
Vector Vector::DotInToCsys(Vector u, Vector v, Vector n) {
|
||||
Vector r = {
|
||||
this->Dot(u),
|
||||
this->Dot(v),
|
||||
this->Dot(n)
|
||||
};
|
||||
return r;
|
||||
}
|
||||
|
||||
Vector Vector::ScaleOutOfCsys(Vector u, Vector v, Vector n) {
|
||||
Vector r = u.ScaledBy(x).Plus(
|
||||
v.ScaledBy(y).Plus(
|
||||
n.ScaledBy(z)));
|
||||
return r;
|
||||
}
|
||||
|
||||
double Vector::DistanceToLine(Vector p0, Vector dp) {
|
||||
double m = dp.Magnitude();
|
||||
return ((this->Minus(p0)).Cross(dp)).Magnitude() / m;
|
||||
|
|
|
@ -7,9 +7,9 @@ some kind of rounding / chamfer
|
|||
remove back button in browser?
|
||||
relative paths for import
|
||||
auto-generate circles and faces when lathing
|
||||
copy the section geometry to other end when sweeping
|
||||
|
||||
partitioned subsystems in the solver
|
||||
sweep tool
|
||||
|
||||
|
||||
long term
|
||||
|
|
Loading…
Reference in New Issue