A big nasty change, originally just to add paste transformed. So it
does that, and adds a scale factor to that transformation (instead of just mirroring, as before), but also: * Replace the "import mirrored" mechanism with a scale factor, which if negative corresponds to a reflection as well. * Fix self-intersection checker to report a meaningful point when edges are collinear. * Don't blow an assertion on some types of invalid file; instead provide a nice error message to the user. * Clear the naked edges before each regen. * Don't create zero-length line segments by snapping a line segment's end to its beginning. [git-p4: depot-paths = "//depot/solvespace/": change = 2086]solver
parent
9723f4e44f
commit
b974a4adeb
186
clipboard.cpp
186
clipboard.cpp
|
@ -114,14 +114,14 @@ void GraphicsWindow::CopySelection(void) {
|
|||
}
|
||||
}
|
||||
|
||||
void GraphicsWindow::PasteClipboard(Vector trans, double theta, bool mirror) {
|
||||
SS.UndoRemember();
|
||||
void GraphicsWindow::PasteClipboard(Vector trans, double theta, double scale) {
|
||||
ClearSelection();
|
||||
|
||||
Entity *wrkpl = SK.GetEntity(ActiveWorkplane());
|
||||
Entity *wrkpln = SK.GetEntity(wrkpl->normal);
|
||||
Vector u = wrkpln->NormalU(),
|
||||
v = wrkpln->NormalV(),
|
||||
n = wrkpln->NormalN(),
|
||||
p = SK.GetEntity(wrkpl->point[0])->PointGetNum();
|
||||
|
||||
|
||||
|
@ -144,17 +144,23 @@ void GraphicsWindow::PasteClipboard(Vector trans, double theta, bool mirror) {
|
|||
NULL, &pts, NULL, &hasDistance);
|
||||
for(int i = 0; i < pts; i++) {
|
||||
Vector pt = cr->point[i];
|
||||
if(mirror) pt.x *= -1;
|
||||
pt = Vector::From( cos(theta)*pt.x + sin(theta)*pt.y,
|
||||
-sin(theta)*pt.x + cos(theta)*pt.y,
|
||||
0);
|
||||
// We need the reflection to occur within the workplane; it may
|
||||
// otherwise correspond to just a rotation as projected.
|
||||
if(scale < 0) {
|
||||
pt.x *= -1;
|
||||
}
|
||||
// Likewise the scale, which could otherwise take us out of the
|
||||
// workplane.
|
||||
pt = pt.ScaledBy(fabs(scale));
|
||||
pt = pt.ScaleOutOfCsys(u, v, Vector::From(0, 0, 0));
|
||||
pt = pt.Plus(p);
|
||||
pt = pt.RotatedAbout(n, theta);
|
||||
pt = pt.Plus(trans);
|
||||
SK.GetEntity(hr.entity(i+1))->PointForceTo(pt);
|
||||
}
|
||||
if(hasDistance) {
|
||||
SK.GetEntity(hr.entity(64))->DistanceForceTo(cr->distance);
|
||||
SK.GetEntity(hr.entity(64))->DistanceForceTo(
|
||||
cr->distance*fabs(scale));
|
||||
}
|
||||
|
||||
cr->newReq = hr;
|
||||
|
@ -181,14 +187,31 @@ void GraphicsWindow::MenuClipboard(int id) {
|
|||
|
||||
switch(id) {
|
||||
case MNU_PASTE: {
|
||||
SS.UndoRemember();
|
||||
Vector trans = SS.GW.projRight.ScaledBy(80/SS.GW.scale).Plus(
|
||||
SS.GW.projUp .ScaledBy(40/SS.GW.scale));
|
||||
SS.GW.PasteClipboard(trans, 0, false);
|
||||
SS.GW.PasteClipboard(trans, 0, 1);
|
||||
break;
|
||||
}
|
||||
|
||||
case MNU_PASTE_TRANSFORM:
|
||||
case MNU_PASTE_TRANSFORM: {
|
||||
if(SS.clipboard.r.n == 0) {
|
||||
Error("Clipboard is empty; nothing to paste.");
|
||||
break;
|
||||
}
|
||||
|
||||
Entity *wrkpl = SK.GetEntity(SS.GW.ActiveWorkplane());
|
||||
Vector p = SK.GetEntity(wrkpl->point[0])->PointGetNum();
|
||||
SS.TW.shown.paste.times = 1;
|
||||
SS.TW.shown.paste.trans = Vector::From(0, 0, 0);
|
||||
SS.TW.shown.paste.theta = 0;
|
||||
SS.TW.shown.paste.origin = p;
|
||||
SS.TW.shown.paste.scale = 1;
|
||||
SS.TW.GoToScreen(TextWindow::SCREEN_PASTE_TRANSFORMED);
|
||||
SS.GW.ForceTextWindowShown();
|
||||
SS.later.showTW = true;
|
||||
break;
|
||||
}
|
||||
|
||||
case MNU_COPY:
|
||||
SS.GW.CopySelection();
|
||||
|
@ -210,3 +233,148 @@ void GraphicsWindow::MenuClipboard(int id) {
|
|||
}
|
||||
}
|
||||
|
||||
bool TextWindow::EditControlDoneForPaste(char *s) {
|
||||
switch(edit.meaning) {
|
||||
case EDIT_PASTE_TIMES_REPEATED: {
|
||||
int v = atoi(s);
|
||||
if(v > 0) {
|
||||
shown.paste.times = v;
|
||||
} else {
|
||||
Error("Number of copies to paste must be at least one.");
|
||||
}
|
||||
break;
|
||||
}
|
||||
case EDIT_PASTE_ANGLE:
|
||||
shown.paste.theta = WRAP_SYMMETRIC(atof(s)*PI/180, 2*PI);
|
||||
break;
|
||||
|
||||
case EDIT_PASTE_SCALE: {
|
||||
double v = atof(s);
|
||||
if(fabs(v) > 1e-6) {
|
||||
shown.paste.scale = v;
|
||||
} else {
|
||||
Error("Scale cannot be zero.");
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void TextWindow::ScreenChangePasteTransformed(int link, DWORD v) {
|
||||
char str[300];
|
||||
switch(link) {
|
||||
case 't':
|
||||
sprintf(str, "%d", SS.TW.shown.paste.times);
|
||||
ShowTextEditControl(10, 12, str);
|
||||
SS.TW.edit.meaning = EDIT_PASTE_TIMES_REPEATED;
|
||||
break;
|
||||
|
||||
case 'r':
|
||||
sprintf(str, "%.3f", SS.TW.shown.paste.theta*180/PI);
|
||||
ShowTextEditControl(12, 12, str);
|
||||
SS.TW.edit.meaning = EDIT_PASTE_ANGLE;
|
||||
break;
|
||||
|
||||
case 's':
|
||||
sprintf(str, "%.3f", SS.TW.shown.paste.scale);
|
||||
ShowTextEditControl(18, 12, str);
|
||||
SS.TW.edit.meaning = EDIT_PASTE_SCALE;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void TextWindow::ScreenPasteTransformed(int link, DWORD v) {
|
||||
SS.GW.GroupSelection();
|
||||
switch(link) {
|
||||
case 'o':
|
||||
if(SS.GW.gs.points == 1 && SS.GW.gs.n == 1) {
|
||||
Entity *e = SK.GetEntity(SS.GW.gs.point[0]);
|
||||
SS.TW.shown.paste.origin = e->PointGetNum();
|
||||
} else {
|
||||
Error("Select one point to define origin of rotation.");
|
||||
}
|
||||
break;
|
||||
|
||||
case 't':
|
||||
if(SS.GW.gs.points == 2 && SS.GW.gs.n == 2) {
|
||||
Entity *pa = SK.GetEntity(SS.GW.gs.point[0]),
|
||||
*pb = SK.GetEntity(SS.GW.gs.point[1]);
|
||||
SS.TW.shown.paste.trans =
|
||||
(pb->PointGetNum()).Minus(pa->PointGetNum());
|
||||
} else {
|
||||
Error("Select two points to define translation vector.");
|
||||
}
|
||||
break;
|
||||
|
||||
case 'g': {
|
||||
if(fabs(SS.TW.shown.paste.theta) < LENGTH_EPS &&
|
||||
SS.TW.shown.paste.trans.Magnitude() < LENGTH_EPS &&
|
||||
SS.TW.shown.paste.times != 1)
|
||||
{
|
||||
Message("Transformation is identity. So all copies will be "
|
||||
"exactly on top of each other.");
|
||||
}
|
||||
if(SS.TW.shown.paste.times*SS.clipboard.r.n > 100) {
|
||||
Error("Too many items to paste; split this into smaller "
|
||||
"pastes.");
|
||||
break;
|
||||
}
|
||||
if(!SS.GW.LockedInWorkplane()) {
|
||||
Error("No workplane active.");
|
||||
break;
|
||||
}
|
||||
Entity *wrkpl = SK.GetEntity(SS.GW.ActiveWorkplane());
|
||||
Entity *wrkpln = SK.GetEntity(wrkpl->normal);
|
||||
Vector wn = wrkpln->NormalN();
|
||||
SS.UndoRemember();
|
||||
for(int i = 0; i < SS.TW.shown.paste.times; i++) {
|
||||
Vector trans = SS.TW.shown.paste.trans.ScaledBy(i+1),
|
||||
origin = SS.TW.shown.paste.origin;
|
||||
double theta = SS.TW.shown.paste.theta*(i+1);
|
||||
// desired transformation is Q*(p - o) + o + t =
|
||||
// Q*p - Q*o + o + t = Q*p + (t + o - Q*o)
|
||||
Vector t = trans.Plus(
|
||||
origin).Minus(
|
||||
origin.RotatedAbout(wn, theta));
|
||||
|
||||
SS.GW.PasteClipboard(t, theta, SS.TW.shown.paste.scale);
|
||||
}
|
||||
SS.TW.GoToScreen(SCREEN_LIST_OF_GROUPS);
|
||||
SS.later.showTW = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
SS.GW.ClearSelection();
|
||||
}
|
||||
|
||||
void TextWindow::ShowPasteTransformed(void) {
|
||||
Printf(true, "%FtPASTE TRANSFORMED%E");
|
||||
Printf(true, "%Ba %FtREPEAT%E %d time%s %Fl%Lt%f[change]%E",
|
||||
shown.paste.times, (shown.paste.times == 1) ? "" : "s",
|
||||
&ScreenChangePasteTransformed);
|
||||
Printf(false, "%Bd %FtROTATE%E %@° %Fl%Lr%f[change]%E",
|
||||
shown.paste.theta*180/PI,
|
||||
&ScreenChangePasteTransformed);
|
||||
Printf(false, "%Ba %FtABOUT PT%E (%s, %s, %s) %Fl%Lo%f[use selected]%E",
|
||||
SS.MmToString(shown.paste.origin.x),
|
||||
SS.MmToString(shown.paste.origin.y),
|
||||
SS.MmToString(shown.paste.origin.z),
|
||||
&ScreenPasteTransformed);
|
||||
Printf(false, "%Bd %FtTRANSLATE%E (%s, %s, %s) %Fl%Lt%f[use selected]%E",
|
||||
SS.MmToString(shown.paste.trans.x),
|
||||
SS.MmToString(shown.paste.trans.y),
|
||||
SS.MmToString(shown.paste.trans.z),
|
||||
&ScreenPasteTransformed);
|
||||
Printf(false, "%Ba %FtSCALE%E %@ %Fl%Ls%f[change]%E",
|
||||
shown.paste.scale,
|
||||
&ScreenChangePasteTransformed);
|
||||
|
||||
Printf(true, " %Fl%Lg%fpaste transformed now%E", &ScreenPasteTransformed);
|
||||
|
||||
Printf(true, "(or %Fl%Ll%fcancel operation%E)", &ScreenHome);
|
||||
}
|
||||
|
||||
|
|
2
dsc.h
2
dsc.h
|
@ -39,7 +39,7 @@ public:
|
|||
Quaternion ToThe(double p);
|
||||
Quaternion Inverse(void);
|
||||
Quaternion Times(Quaternion b);
|
||||
Quaternion MirrorZ(void);
|
||||
Quaternion Mirror(void);
|
||||
};
|
||||
|
||||
class Vector {
|
||||
|
|
16
file.cpp
16
file.cpp
|
@ -93,7 +93,7 @@ const SolveSpace::SaveTable SolveSpace::SAVED[] = {
|
|||
{ 'g', "Group.visible", 'b', &(SS.sv.g.visible) },
|
||||
{ 'g', "Group.suppress", 'b', &(SS.sv.g.suppress) },
|
||||
{ 'g', "Group.relaxConstraints", 'b', &(SS.sv.g.relaxConstraints) },
|
||||
{ 'g', "Group.mirror", 'b', &(SS.sv.g.mirror) },
|
||||
{ 'g', "Group.scale", 'f', &(SS.sv.g.scale) },
|
||||
{ 'g', "Group.remap", 'M', &(SS.sv.g.remap) },
|
||||
{ 'g', "Group.impFile", 'P', &(SS.sv.g.impFile) },
|
||||
{ 'g', "Group.impFileRel", 'P', &(SS.sv.g.impFileRel) },
|
||||
|
@ -378,11 +378,14 @@ void SolveSpace::LoadUsingTable(char *key, char *val) {
|
|||
break;
|
||||
}
|
||||
}
|
||||
if(SAVED[i].type == 0) oops();
|
||||
if(SAVED[i].type == 0) {
|
||||
fileLoadError = true;
|
||||
}
|
||||
}
|
||||
|
||||
bool SolveSpace::LoadFromFile(char *filename) {
|
||||
allConsistent = false;
|
||||
fileLoadError = false;
|
||||
|
||||
fh = fopen(filename, "rb");
|
||||
if(!fh) {
|
||||
|
@ -400,6 +403,7 @@ bool SolveSpace::LoadFromFile(char *filename) {
|
|||
SK.param.Clear();
|
||||
SK.style.Clear();
|
||||
memset(&sv, 0, sizeof(sv));
|
||||
sv.g.scale = 1; // default is 1, not 0; so legacy files need this
|
||||
|
||||
char line[1024];
|
||||
while(fgets(line, sizeof(line), fh)) {
|
||||
|
@ -420,6 +424,7 @@ bool SolveSpace::LoadFromFile(char *filename) {
|
|||
} else if(strcmp(line, "AddGroup")==0) {
|
||||
SK.group.Add(&(sv.g));
|
||||
ZERO(&(sv.g));
|
||||
sv.g.scale = 1; // default is 1, not 0; so legacy files need this
|
||||
} else if(strcmp(line, "AddParam")==0) {
|
||||
// params are regenerated, but we want to preload the values
|
||||
// for initial guesses
|
||||
|
@ -450,12 +455,17 @@ bool SolveSpace::LoadFromFile(char *filename) {
|
|||
{
|
||||
// ignore the mesh or shell, since we regenerate that
|
||||
} else {
|
||||
oops();
|
||||
fileLoadError = true;
|
||||
}
|
||||
}
|
||||
|
||||
fclose(fh);
|
||||
|
||||
if(fileLoadError) {
|
||||
Error("Unrecognized data in file. This file may be corrupt, or "
|
||||
"from a new version of the program.");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
@ -152,6 +152,7 @@ void SolveSpace::GenerateAll(void) {
|
|||
// All clean; so just regenerate the entities, and don't solve anything.
|
||||
GenerateAll(-1, -1);
|
||||
} else {
|
||||
SS.nakedEdges.Clear();
|
||||
GenerateAll(firstDirty, lastVisible);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -43,7 +43,7 @@ const GraphicsWindow::MenuEntry GraphicsWindow::menu[] = {
|
|||
{ 1, "Paste &Transformed...\tCtrl+T", MNU_PASTE_TRANSFORM,'T'|C, mClip },
|
||||
{ 1, "&Delete\tDel", MNU_DELETE, 127, mClip },
|
||||
{ 1, NULL, 0, NULL },
|
||||
{ 1, "Select Edge Cha&in\tCtrl+I", MNU_SELECT_CHAIN, 'I'|C, mEdit },
|
||||
{ 1, "Select &Edge Chain\tCtrl+E", MNU_SELECT_CHAIN, 'E'|C, mEdit },
|
||||
{ 1, "Invert &Selection\tCtrl+A", MNU_INVERT_SEL, 'A'|C, mEdit },
|
||||
{ 1, "&Unselect All\tEsc", MNU_UNSELECT_ALL, 27, mEdit },
|
||||
|
||||
|
|
17
group.cpp
17
group.cpp
|
@ -27,6 +27,7 @@ void Group::MenuGroup(int id) {
|
|||
ZERO(&g);
|
||||
g.visible = true;
|
||||
g.color = RGB(100, 100, 100);
|
||||
g.scale = 1;
|
||||
|
||||
if(id >= RECENT_IMPORT && id < (RECENT_IMPORT + MAX_RECENT)) {
|
||||
strcpy(g.impFile, RecentFile[id-RECENT_IMPORT]);
|
||||
|
@ -681,8 +682,7 @@ void Group::CopyEntity(IdList<Entity,hEntity> *el,
|
|||
en.param[5] = qvy;
|
||||
en.param[6] = qvz;
|
||||
}
|
||||
en.numPoint = ep->actPoint;
|
||||
if(mirror) en.numPoint.z *= -1;
|
||||
en.numPoint = (ep->actPoint).ScaledBy(scale);
|
||||
break;
|
||||
|
||||
case Entity::NORMAL_N_COPY:
|
||||
|
@ -704,7 +704,7 @@ void Group::CopyEntity(IdList<Entity,hEntity> *el,
|
|||
en.param[3] = qvz;
|
||||
}
|
||||
en.numNormal = ep->actNormal;
|
||||
if(mirror) en.numNormal = en.numNormal.MirrorZ();
|
||||
if(scale < 0) en.numNormal = en.numNormal.Mirror();
|
||||
|
||||
en.point[0] = Remap(ep->point[0], remap);
|
||||
break;
|
||||
|
@ -712,7 +712,7 @@ void Group::CopyEntity(IdList<Entity,hEntity> *el,
|
|||
case Entity::DISTANCE_N_COPY:
|
||||
case Entity::DISTANCE:
|
||||
en.type = Entity::DISTANCE_N_COPY;
|
||||
en.numDistance = ep->actDistance;
|
||||
en.numDistance = ep->actDistance*fabs(scale);
|
||||
break;
|
||||
|
||||
case Entity::FACE_NORMAL_PT:
|
||||
|
@ -739,13 +739,8 @@ void Group::CopyEntity(IdList<Entity,hEntity> *el,
|
|||
en.param[5] = qvy;
|
||||
en.param[6] = qvz;
|
||||
}
|
||||
en.numPoint = ep->actPoint;
|
||||
en.numNormal = ep->actNormal;
|
||||
if(mirror) {
|
||||
if(en.type != Entity::FACE_N_ROT_TRANS) oops();
|
||||
en.numPoint.z *= -1;
|
||||
en.numNormal.vz *= -1;
|
||||
}
|
||||
en.numPoint = (ep->actPoint).ScaledBy(scale);
|
||||
en.numNormal = (ep->actNormal).ScaledBy(scale);
|
||||
break;
|
||||
|
||||
default: {
|
||||
|
|
|
@ -119,7 +119,7 @@ void Group::GenerateForStepAndRepeat(T *steps, T *outs) {
|
|||
Vector trans = Vector::From(h.param(0), h.param(1), h.param(2));
|
||||
trans = trans.ScaledBy(ap);
|
||||
transd.MakeFromTransformationOf(steps,
|
||||
trans, Quaternion::IDENTITY, false);
|
||||
trans, Quaternion::IDENTITY, 1.0);
|
||||
} else {
|
||||
Vector trans = Vector::From(h.param(0), h.param(1), h.param(2));
|
||||
double theta = ap * SK.GetParam(h.param(3))->val;
|
||||
|
@ -128,7 +128,7 @@ void Group::GenerateForStepAndRepeat(T *steps, T *outs) {
|
|||
Quaternion q = Quaternion::From(c, s*axis.x, s*axis.y, s*axis.z);
|
||||
// Rotation is centered at t; so A(x - t) + t = Ax + (t - At)
|
||||
transd.MakeFromTransformationOf(steps,
|
||||
trans.Minus(q.Rotate(trans)), q, false);
|
||||
trans.Minus(q.Rotate(trans)), q, 1.0);
|
||||
}
|
||||
|
||||
// We need to rewrite any plane face entities to the transformed ones.
|
||||
|
@ -292,10 +292,10 @@ void Group::GenerateShellAndMesh(void) {
|
|||
SK.GetParam(h.param(5))->val,
|
||||
SK.GetParam(h.param(6))->val };
|
||||
|
||||
thisMesh.MakeFromTransformationOf(&impMesh, offset, q, mirror);
|
||||
thisMesh.MakeFromTransformationOf(&impMesh, offset, q, scale);
|
||||
thisMesh.RemapFaces(this, 0);
|
||||
|
||||
thisShell.MakeFromTransformationOf(&impShell, offset, q, mirror);
|
||||
thisShell.MakeFromTransformationOf(&impShell, offset, q, scale);
|
||||
thisShell.RemapFaces(this, 0);
|
||||
}
|
||||
|
||||
|
|
12
mesh.cpp
12
mesh.cpp
|
@ -293,16 +293,16 @@ void SMesh::MakeFromAssemblyOf(SMesh *a, SMesh *b) {
|
|||
MakeFromCopyOf(b);
|
||||
}
|
||||
|
||||
void SMesh::MakeFromTransformationOf(SMesh *a, Vector trans, Quaternion q,
|
||||
bool mirror)
|
||||
void SMesh::MakeFromTransformationOf(SMesh *a,
|
||||
Vector trans, Quaternion q, double scale)
|
||||
{
|
||||
STriangle *tr;
|
||||
for(tr = a->l.First(); tr; tr = a->l.NextAfter(tr)) {
|
||||
STriangle tt = *tr;
|
||||
if(mirror) {
|
||||
tt.a.z *= -1;
|
||||
tt.b.z *= -1;
|
||||
tt.c.z *= -1;
|
||||
tt.a = (tt.a).ScaledBy(scale);
|
||||
tt.b = (tt.b).ScaledBy(scale);
|
||||
tt.c = (tt.c).ScaledBy(scale);
|
||||
if(scale < 0) {
|
||||
// The mirroring would otherwise turn a closed mesh inside out.
|
||||
SWAP(Vector, tt.a, tt.b);
|
||||
}
|
||||
|
|
18
mouse.cpp
18
mouse.cpp
|
@ -980,10 +980,28 @@ void GraphicsWindow::MouseLeftDown(double mx, double my) {
|
|||
}
|
||||
|
||||
case DRAGGING_NEW_LINE_POINT: {
|
||||
if(hover.entity.v) {
|
||||
Entity *e = SK.GetEntity(hover.entity);
|
||||
if(e->IsPoint()) {
|
||||
hRequest hrl = pending.point.request();
|
||||
Entity *sp = SK.GetEntity(hrl.entity(1));
|
||||
if(( e->PointGetNum()).Equals(
|
||||
(sp->PointGetNum())))
|
||||
{
|
||||
// If we constrained by the hovered point, then we
|
||||
// would create a zero-length line segment. That's
|
||||
// not good, so just stop drawing.
|
||||
ClearPending();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(ConstrainPointByHovered(pending.point)) {
|
||||
ClearPending();
|
||||
break;
|
||||
}
|
||||
|
||||
// Create a new line segment, so that we continue drawing.
|
||||
hRequest hr = AddRequest(Request::LINE_SEGMENT);
|
||||
SK.GetEntity(hr.entity(1))->PointForceTo(v);
|
||||
|
|
29
polygon.cpp
29
polygon.cpp
|
@ -70,8 +70,13 @@ bool SEdge::EdgeCrosses(Vector ea, Vector eb, Vector *ppi, SPointList *spl) {
|
|||
Vector dthis = b.Minus(a);
|
||||
double tthis_eps = LENGTH_EPS/dthis.Magnitude();
|
||||
|
||||
if(ea.Equals(a) && eb.Equals(b)) return true;
|
||||
if(eb.Equals(a) && ea.Equals(b)) return true;
|
||||
if((ea.Equals(a) && eb.Equals(b)) ||
|
||||
(eb.Equals(a) && ea.Equals(b)))
|
||||
{
|
||||
if(ppi) *ppi = a;
|
||||
if(spl) spl->Add(a);
|
||||
return true;
|
||||
}
|
||||
|
||||
dist_a = a.DistanceToLine(ea, d),
|
||||
dist_b = b.DistanceToLine(ea, d);
|
||||
|
@ -87,17 +92,25 @@ bool SEdge::EdgeCrosses(Vector ea, Vector eb, Vector *ppi, SPointList *spl) {
|
|||
}
|
||||
// The edges are coincident. Make sure that neither endpoint lies
|
||||
// on the other
|
||||
bool inters = false;
|
||||
double t;
|
||||
t = a.Minus(ea).DivPivoting(d);
|
||||
if(t > t_eps && t < (1 - t_eps)) return true;
|
||||
if(t > t_eps && t < (1 - t_eps)) inters = true;
|
||||
t = b.Minus(ea).DivPivoting(d);
|
||||
if(t > t_eps && t < (1 - t_eps)) return true;
|
||||
if(t > t_eps && t < (1 - t_eps)) inters = true;
|
||||
t = ea.Minus(a).DivPivoting(dthis);
|
||||
if(t > tthis_eps && t < (1 - tthis_eps)) return true;
|
||||
if(t > tthis_eps && t < (1 - tthis_eps)) inters = true;
|
||||
t = eb.Minus(a).DivPivoting(dthis);
|
||||
if(t > tthis_eps && t < (1 - tthis_eps)) return true;
|
||||
// So coincident but disjoint, okay.
|
||||
return false;
|
||||
if(t > tthis_eps && t < (1 - tthis_eps)) inters = true;
|
||||
|
||||
if(inters) {
|
||||
if(ppi) *ppi = a;
|
||||
if(spl) spl->Add(a);
|
||||
return true;
|
||||
} else {
|
||||
// So coincident but disjoint, okay.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Lines are not parallel, so look for an intersection.
|
||||
|
|
|
@ -231,8 +231,8 @@ public:
|
|||
void MakeFromDifferenceOf(SMesh *a, SMesh *b);
|
||||
|
||||
void MakeFromCopyOf(SMesh *a);
|
||||
void MakeFromTransformationOf(SMesh *a, Vector trans, Quaternion q,
|
||||
bool mirror);
|
||||
void MakeFromTransformationOf(SMesh *a,
|
||||
Vector trans, Quaternion q, double scale);
|
||||
void MakeFromAssemblyOf(SMesh *a, SMesh *b);
|
||||
|
||||
void MakeEdgesInPlaneInto(SEdgeList *sel, Vector n, double d);
|
||||
|
|
2
sketch.h
2
sketch.h
|
@ -102,7 +102,7 @@ public:
|
|||
bool visible;
|
||||
bool suppress;
|
||||
bool relaxConstraints;
|
||||
bool mirror;
|
||||
double scale;
|
||||
|
||||
bool clean;
|
||||
bool vvMeshClean;
|
||||
|
|
|
@ -625,6 +625,7 @@ public:
|
|||
static void RemoveFromRecentList(char *file);
|
||||
static void AddToRecentList(char *file);
|
||||
char saveFile[MAX_PATH];
|
||||
bool fileLoadError;
|
||||
bool unsaved;
|
||||
typedef struct {
|
||||
char type;
|
||||
|
|
|
@ -687,7 +687,7 @@ void SShell::MakeFromAssemblyOf(SShell *a, SShell *b) {
|
|||
for(i = 0; i < 2; i++) {
|
||||
ab = (i == 0) ? a : b;
|
||||
for(c = ab->curve.First(); c; c = ab->curve.NextAfter(c)) {
|
||||
cn = SCurve::FromTransformationOf(c, t, q, false);
|
||||
cn = SCurve::FromTransformationOf(c, t, q, 1.0);
|
||||
cn.source = (i == 0) ? SCurve::FROM_A : SCurve::FROM_B;
|
||||
// surfA and surfB are wrong now, and we can't fix them until
|
||||
// we've assigned IDs to the surfaces. So we'll get that later.
|
||||
|
@ -700,7 +700,7 @@ void SShell::MakeFromAssemblyOf(SShell *a, SShell *b) {
|
|||
for(i = 0; i < 2; i++) {
|
||||
ab = (i == 0) ? a : b;
|
||||
for(s = ab->surface.First(); s; s = ab->surface.NextAfter(s)) {
|
||||
sn = SSurface::FromTransformationOf(s, t, q, false, true);
|
||||
sn = SSurface::FromTransformationOf(s, t, q, 1.0, true);
|
||||
// All the trim curve IDs get rewritten; we know the new handles
|
||||
// to the curves since we recorded them in the previous step.
|
||||
STrimBy *stb;
|
||||
|
|
|
@ -95,11 +95,11 @@ void SBezier::GetBoundingProjd(Vector u, Vector orig,
|
|||
}
|
||||
}
|
||||
|
||||
SBezier SBezier::TransformedBy(Vector t, Quaternion q, bool mirror) {
|
||||
SBezier SBezier::TransformedBy(Vector t, Quaternion q, double scale) {
|
||||
SBezier ret = *this;
|
||||
int i;
|
||||
for(i = 0; i <= deg; i++) {
|
||||
if(mirror) ret.ctrl[i].z *= -1;
|
||||
ret.ctrl[i] = (ret.ctrl[i]).ScaledBy(scale);
|
||||
ret.ctrl[i] = (q.Rotate(ret.ctrl[i])).Plus(t);
|
||||
}
|
||||
return ret;
|
||||
|
@ -189,7 +189,7 @@ SBezier SBezier::InPerspective(Vector u, Vector v, Vector n,
|
|||
Quaternion q = Quaternion::From(u, v);
|
||||
q = q.Inverse();
|
||||
// we want Q*(p - o) = Q*p - Q*o
|
||||
SBezier ret = this->TransformedBy(q.Rotate(origin).ScaledBy(-1), q, false);
|
||||
SBezier ret = this->TransformedBy(q.Rotate(origin).ScaledBy(-1), q, 1.0);
|
||||
int i;
|
||||
for(i = 0; i <= deg; i++) {
|
||||
Vector4 ct = Vector4::From(ret.weight[i], ret.ctrl[i]);
|
||||
|
@ -740,22 +740,22 @@ void SBezierLoopSetSet::Clear(void) {
|
|||
l.Clear();
|
||||
}
|
||||
|
||||
SCurve SCurve::FromTransformationOf(SCurve *a, Vector t, Quaternion q,
|
||||
bool mirror)
|
||||
SCurve SCurve::FromTransformationOf(SCurve *a,
|
||||
Vector t, Quaternion q, double scale)
|
||||
{
|
||||
SCurve ret;
|
||||
ZERO(&ret);
|
||||
|
||||
ret.h = a->h;
|
||||
ret.isExact = a->isExact;
|
||||
ret.exact = (a->exact).TransformedBy(t, q, mirror);
|
||||
ret.exact = (a->exact).TransformedBy(t, q, scale);
|
||||
ret.surfA = a->surfA;
|
||||
ret.surfB = a->surfB;
|
||||
|
||||
SCurvePt *p;
|
||||
for(p = a->pts.First(); p; p = a->pts.NextAfter(p)) {
|
||||
SCurvePt pp = *p;
|
||||
if(mirror) pp.p.z *= -1;
|
||||
pp.p = (pp.p).ScaledBy(scale);
|
||||
pp.p = (q.Rotate(pp.p)).Plus(t);
|
||||
ret.pts.Add(&pp);
|
||||
}
|
||||
|
|
|
@ -132,8 +132,8 @@ SSurface SSurface::FromPlane(Vector pt, Vector u, Vector v) {
|
|||
return ret;
|
||||
}
|
||||
|
||||
SSurface SSurface::FromTransformationOf(SSurface *a, Vector t, Quaternion q,
|
||||
bool mirror,
|
||||
SSurface SSurface::FromTransformationOf(SSurface *a,
|
||||
Vector t, Quaternion q, double scale,
|
||||
bool includingTrims)
|
||||
{
|
||||
SSurface ret;
|
||||
|
@ -149,7 +149,7 @@ SSurface SSurface::FromTransformationOf(SSurface *a, Vector t, Quaternion q,
|
|||
for(i = 0; i <= 3; i++) {
|
||||
for(j = 0; j <= 3; j++) {
|
||||
ret.ctrl[i][j] = a->ctrl[i][j];
|
||||
if(mirror) ret.ctrl[i][j].z *= -1;
|
||||
ret.ctrl[i][j] = (ret.ctrl[i][j]).ScaledBy(scale);
|
||||
ret.ctrl[i][j] = (q.Rotate(ret.ctrl[i][j])).Plus(t);
|
||||
|
||||
ret.weight[i][j] = a->weight[i][j];
|
||||
|
@ -160,17 +160,15 @@ SSurface SSurface::FromTransformationOf(SSurface *a, Vector t, Quaternion q,
|
|||
STrimBy *stb;
|
||||
for(stb = a->trim.First(); stb; stb = a->trim.NextAfter(stb)) {
|
||||
STrimBy n = *stb;
|
||||
if(mirror) {
|
||||
n.start.z *= -1;
|
||||
n.finish.z *= -1;
|
||||
}
|
||||
n.start = n.start.ScaledBy(scale);
|
||||
n.finish = n.finish.ScaledBy(scale);
|
||||
n.start = (q.Rotate(n.start)) .Plus(t);
|
||||
n.finish = (q.Rotate(n.finish)).Plus(t);
|
||||
ret.trim.Add(&n);
|
||||
}
|
||||
}
|
||||
|
||||
if(mirror) {
|
||||
if(scale < 0) {
|
||||
// If we mirror every surface of a shell, then it will end up inside
|
||||
// out. So fix that here.
|
||||
ret.Reverse();
|
||||
|
@ -543,7 +541,7 @@ void SShell::MakeFromExtrusionOf(SBezierLoopSet *sbls, Vector t0, Vector t1,
|
|||
SCurve sc;
|
||||
ZERO(&sc);
|
||||
sc.isExact = true;
|
||||
sc.exact = sb->TransformedBy(t0, Quaternion::IDENTITY, false);
|
||||
sc.exact = sb->TransformedBy(t0, Quaternion::IDENTITY, 1.0);
|
||||
(sc.exact).MakePwlInto(&(sc.pts));
|
||||
sc.surfA = hs0;
|
||||
sc.surfB = hsext;
|
||||
|
@ -551,7 +549,7 @@ void SShell::MakeFromExtrusionOf(SBezierLoopSet *sbls, Vector t0, Vector t1,
|
|||
|
||||
ZERO(&sc);
|
||||
sc.isExact = true;
|
||||
sc.exact = sb->TransformedBy(t1, Quaternion::IDENTITY, false);
|
||||
sc.exact = sb->TransformedBy(t1, Quaternion::IDENTITY, 1.0);
|
||||
(sc.exact).MakePwlInto(&(sc.pts));
|
||||
sc.surfA = hs1;
|
||||
sc.surfB = hsext;
|
||||
|
@ -689,7 +687,7 @@ void SShell::MakeFromRevolutionOf(SBezierLoopSet *sbls, Vector pt, Vector axis,
|
|||
if(revs.d[j].v) {
|
||||
ZERO(&sc);
|
||||
sc.isExact = true;
|
||||
sc.exact = sb->TransformedBy(ts, qs, false);
|
||||
sc.exact = sb->TransformedBy(ts, qs, 1.0);
|
||||
(sc.exact).MakePwlInto(&(sc.pts));
|
||||
sc.surfA = revs.d[j];
|
||||
sc.surfB = revs.d[WRAP(j-1, 4)];
|
||||
|
@ -816,25 +814,25 @@ void SShell::MakeFromRevolutionOf(SBezierLoopSet *sbls, Vector pt, Vector axis,
|
|||
|
||||
void SShell::MakeFromCopyOf(SShell *a) {
|
||||
MakeFromTransformationOf(a,
|
||||
Vector::From(0, 0, 0), Quaternion::IDENTITY, false);
|
||||
Vector::From(0, 0, 0), Quaternion::IDENTITY, 1.0);
|
||||
}
|
||||
|
||||
void SShell::MakeFromTransformationOf(SShell *a,
|
||||
Vector t, Quaternion q, bool mirror)
|
||||
Vector t, Quaternion q, double scale)
|
||||
{
|
||||
booleanFailed = false;
|
||||
|
||||
SSurface *s;
|
||||
for(s = a->surface.First(); s; s = a->surface.NextAfter(s)) {
|
||||
SSurface n;
|
||||
n = SSurface::FromTransformationOf(s, t, q, mirror, true);
|
||||
n = SSurface::FromTransformationOf(s, t, q, scale, true);
|
||||
surface.Add(&n); // keeping the old ID
|
||||
}
|
||||
|
||||
SCurve *c;
|
||||
for(c = a->curve.First(); c; c = a->curve.NextAfter(c)) {
|
||||
SCurve n;
|
||||
n = SCurve::FromTransformationOf(c, t, q, mirror);
|
||||
n = SCurve::FromTransformationOf(c, t, q, scale);
|
||||
curve.Add(&n); // keeping the old ID
|
||||
}
|
||||
}
|
||||
|
|
|
@ -91,7 +91,7 @@ public:
|
|||
bool IsCircle(Vector axis, Vector *center, double *r);
|
||||
bool IsRational(void);
|
||||
|
||||
SBezier TransformedBy(Vector t, Quaternion q, bool mirror);
|
||||
SBezier TransformedBy(Vector t, Quaternion q, double scale);
|
||||
SBezier InPerspective(Vector u, Vector v, Vector n,
|
||||
Vector origin, double cameraTan);
|
||||
void ScaleSelfBy(double s);
|
||||
|
@ -190,7 +190,7 @@ public:
|
|||
hSSurface surfB;
|
||||
|
||||
static SCurve FromTransformationOf(SCurve *a, Vector t, Quaternion q,
|
||||
bool mirror);
|
||||
double scale);
|
||||
SCurve MakeCopySplitAgainst(SShell *agnstA, SShell *agnstB,
|
||||
SSurface *srfA, SSurface *srfB);
|
||||
void RemoveShortSegments(SSurface *srfA, SSurface *srfB);
|
||||
|
@ -260,7 +260,7 @@ public:
|
|||
double thetas, double thetaf);
|
||||
static SSurface FromPlane(Vector pt, Vector u, Vector v);
|
||||
static SSurface FromTransformationOf(SSurface *a, Vector t, Quaternion q,
|
||||
bool mirror,
|
||||
double scale,
|
||||
bool includingTrims);
|
||||
void ScaleSelfBy(double s);
|
||||
|
||||
|
@ -384,8 +384,8 @@ public:
|
|||
Vector edge_n_in, Vector edge_n_out, Vector surf_n);
|
||||
|
||||
void MakeFromCopyOf(SShell *a);
|
||||
void MakeFromTransformationOf(SShell *a, Vector trans, Quaternion q,
|
||||
bool mirror);
|
||||
void MakeFromTransformationOf(SShell *a,
|
||||
Vector trans, Quaternion q, double scale);
|
||||
void MakeFromAssemblyOf(SShell *a, SShell *b);
|
||||
void MergeCoincidentSurfaces(void);
|
||||
|
||||
|
|
|
@ -235,10 +235,6 @@ void TextWindow::ScreenChangeGroupOption(int link, DWORD v) {
|
|||
g->suppress = !(g->suppress);
|
||||
break;
|
||||
|
||||
case 'm':
|
||||
g->mirror = !(g->mirror);
|
||||
break;
|
||||
|
||||
case 'r':
|
||||
g->relaxConstraints = !(g->relaxConstraints);
|
||||
break;
|
||||
|
@ -315,6 +311,15 @@ void TextWindow::ScreenChangeGroupName(int link, DWORD v) {
|
|||
SS.TW.edit.meaning = EDIT_GROUP_NAME;
|
||||
SS.TW.edit.group.v = v;
|
||||
}
|
||||
void TextWindow::ScreenChangeGroupScale(int link, DWORD v) {
|
||||
Group *g = SK.GetGroup(SS.TW.shown.group);
|
||||
|
||||
char str[1024];
|
||||
sprintf(str, "%.3f", g->scale);
|
||||
ShowTextEditControl(17, 9, str);
|
||||
SS.TW.edit.meaning = EDIT_GROUP_SCALE;
|
||||
SS.TW.edit.group.v = v;
|
||||
}
|
||||
void TextWindow::ScreenDeleteGroup(int link, DWORD v) {
|
||||
SS.UndoRemember();
|
||||
|
||||
|
@ -449,11 +454,9 @@ void TextWindow::ShowGroupInfo(void) {
|
|||
&TextWindow::ScreenChangeGroupOption,
|
||||
(!sup ? "" : "no"), (!sup ? "no" : ""));
|
||||
|
||||
Printf(false, "%FtMIRROR%E %Fh%f%Lm%s%E%Fs%s%E / %Fh%f%Lm%s%E%Fs%s%E",
|
||||
&TextWindow::ScreenChangeGroupOption,
|
||||
(g->mirror ? "" : "yes"), (g->mirror ? "yes" : ""),
|
||||
&TextWindow::ScreenChangeGroupOption,
|
||||
(!g->mirror ? "" : "no"), (!g->mirror ? "no" : ""));
|
||||
Printf(true, "%FtSCALE BY%E %# %Fl%Ll%f%D[change]%E",
|
||||
g->scale,
|
||||
&TextWindow::ScreenChangeGroupScale, g->h.v);
|
||||
}
|
||||
|
||||
bool relax = g->relaxConstraints;
|
||||
|
@ -734,6 +737,23 @@ void TextWindow::EditControlDone(char *s) {
|
|||
}
|
||||
break;
|
||||
}
|
||||
case EDIT_GROUP_SCALE: {
|
||||
Expr *e = Expr::From(s);
|
||||
if(e) {
|
||||
double ev = e->Eval();
|
||||
if(fabs(ev) < 1e-6) {
|
||||
Error("Scale cannot be zero.");
|
||||
} else {
|
||||
Group *g = SK.GetGroup(edit.group);
|
||||
g->scale = ev;
|
||||
SS.MarkGroupDirty(g->h);
|
||||
SS.later.generateAll = true;
|
||||
}
|
||||
} else {
|
||||
Error("Not a valid number or expression: '%s'", s);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case EDIT_HELIX_TURNS:
|
||||
case EDIT_HELIX_PITCH:
|
||||
case EDIT_HELIX_DRADIUS: {
|
||||
|
@ -783,9 +803,11 @@ void TextWindow::EditControlDone(char *s) {
|
|||
break;
|
||||
|
||||
default: {
|
||||
bool st = EditControlDoneForStyles(s),
|
||||
cf = EditControlDoneForConfiguration(s);
|
||||
if(st && cf) {
|
||||
int cnt = 0;
|
||||
if(EditControlDoneForStyles(s)) cnt++;
|
||||
if(EditControlDoneForConfiguration(s)) cnt++;
|
||||
if(EditControlDoneForPaste(s)) cnt++;
|
||||
if(cnt > 1) {
|
||||
// The identifiers were somehow assigned not uniquely?
|
||||
oops();
|
||||
}
|
||||
|
|
|
@ -216,7 +216,9 @@ void TextWindow::Show(void) {
|
|||
Printf(false, "%s", SS.GW.pending.description);
|
||||
Printf(true, "%Fl%f%Ll(cancel operation)%E",
|
||||
&TextWindow::ScreenUnselectAll);
|
||||
} else if(gs.n > 0 || gs.constraints > 0) {
|
||||
} else if((gs.n > 0 || gs.constraints > 0) &&
|
||||
shown.screen != SCREEN_PASTE_TRANSFORMED)
|
||||
{
|
||||
if(edit.meaning != EDIT_TTF_TEXT) HideTextEditControl();
|
||||
ShowHeader(false);
|
||||
DescribeSelection();
|
||||
|
@ -235,6 +237,7 @@ void TextWindow::Show(void) {
|
|||
case SCREEN_MESH_VOLUME: ShowMeshVolume(); break;
|
||||
case SCREEN_LIST_OF_STYLES: ShowListOfStyles(); break;
|
||||
case SCREEN_STYLE_INFO: ShowStyleInfo(); break;
|
||||
case SCREEN_PASTE_TRANSFORMED: ShowPasteTransformed(); break;
|
||||
}
|
||||
}
|
||||
Printf(false, "");
|
||||
|
|
22
ui.h
22
ui.h
|
@ -54,6 +54,7 @@ public:
|
|||
static const int SCREEN_MESH_VOLUME = 5;
|
||||
static const int SCREEN_LIST_OF_STYLES = 6;
|
||||
static const int SCREEN_STYLE_INFO = 7;
|
||||
static const int SCREEN_PASTE_TRANSFORMED = 8;
|
||||
typedef struct {
|
||||
int screen;
|
||||
|
||||
|
@ -64,6 +65,14 @@ public:
|
|||
bool dimIsDistance;
|
||||
double dimFinish;
|
||||
int dimSteps;
|
||||
|
||||
struct {
|
||||
int times;
|
||||
Vector trans;
|
||||
double theta;
|
||||
Vector origin;
|
||||
double scale;
|
||||
} paste;
|
||||
|
||||
double volume;
|
||||
} ShownState;
|
||||
|
@ -73,6 +82,7 @@ public:
|
|||
// For multiple groups
|
||||
static const int EDIT_TIMES_REPEATED = 1;
|
||||
static const int EDIT_GROUP_NAME = 2;
|
||||
static const int EDIT_GROUP_SCALE = 3;
|
||||
// For the configuraiton screen
|
||||
static const int EDIT_LIGHT_DIRECTION = 10;
|
||||
static const int EDIT_LIGHT_INTENSITY = 11;
|
||||
|
@ -102,6 +112,10 @@ public:
|
|||
static const int EDIT_STYLE_NAME = 55;
|
||||
static const int EDIT_BACKGROUND_COLOR = 56;
|
||||
static const int EDIT_BACKGROUND_IMG_SCALE = 57;
|
||||
// For paste transforming
|
||||
static const int EDIT_PASTE_TIMES_REPEATED = 60;
|
||||
static const int EDIT_PASTE_ANGLE = 61;
|
||||
static const int EDIT_PASTE_SCALE = 62;
|
||||
struct {
|
||||
int meaning;
|
||||
int i;
|
||||
|
@ -125,6 +139,7 @@ public:
|
|||
void ShowStyleInfo(void);
|
||||
void ShowStepDimension(void);
|
||||
void ShowMeshVolume(void);
|
||||
void ShowPasteTransformed(void);
|
||||
// Special screen, based on selection
|
||||
void DescribeSelection(void);
|
||||
|
||||
|
@ -178,11 +193,14 @@ public:
|
|||
static void ScreenStepDimFinish(int link, DWORD v);
|
||||
static void ScreenStepDimGo(int link, DWORD v);
|
||||
|
||||
static void ScreenPasteTransformed(int link, DWORD v);
|
||||
|
||||
static void ScreenHome(int link, DWORD v);
|
||||
|
||||
// These ones do stuff with the edit control
|
||||
static void ScreenChangeExprA(int link, DWORD v);
|
||||
static void ScreenChangeGroupName(int link, DWORD v);
|
||||
static void ScreenChangeGroupScale(int link, DWORD v);
|
||||
static void ScreenChangeLightDirection(int link, DWORD v);
|
||||
static void ScreenChangeLightIntensity(int link, DWORD v);
|
||||
static void ScreenChangeColor(int link, DWORD v);
|
||||
|
@ -198,9 +216,11 @@ public:
|
|||
static void ScreenChangeStyleColor(int link, DWORD v);
|
||||
static void ScreenChangeBackgroundColor(int link, DWORD v);
|
||||
static void ScreenChangeBackgroundImageScale(int link, DWORD v);
|
||||
static void ScreenChangePasteTransformed(int link, DWORD v);
|
||||
|
||||
bool EditControlDoneForStyles(char *s);
|
||||
bool EditControlDoneForConfiguration(char *s);
|
||||
bool EditControlDoneForPaste(char *s);
|
||||
void EditControlDone(char *s);
|
||||
};
|
||||
|
||||
|
@ -317,7 +337,7 @@ public:
|
|||
static void MenuRequest(int id);
|
||||
void DeleteSelection(void);
|
||||
void CopySelection(void);
|
||||
void PasteClipboard(Vector trans, double theta, bool mirror);
|
||||
void PasteClipboard(Vector trans, double theta, double scale);
|
||||
static void MenuClipboard(int id);
|
||||
|
||||
// The width and height (in pixels) of the window.
|
||||
|
|
6
util.cpp
6
util.cpp
|
@ -344,11 +344,11 @@ Quaternion Quaternion::Times(Quaternion b) {
|
|||
return r;
|
||||
}
|
||||
|
||||
Quaternion Quaternion::MirrorZ(void) {
|
||||
Quaternion Quaternion::Mirror(void) {
|
||||
Vector u = RotationU(),
|
||||
v = RotationV();
|
||||
u.z *= -1;
|
||||
v.z *= -1;
|
||||
u = u.ScaledBy(-1);
|
||||
v = v.ScaledBy(-1);
|
||||
return Quaternion::From(u, v);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
|
||||
paste transformed
|
||||
de-select after left-clicking nothing, keep sel on drag?
|
||||
bbox selection is select-only, not toggle?
|
||||
show and modify view parameters (translate, rotate, scale)
|
||||
context menu to hide / suppress groups by entity?
|
||||
|
||||
-----
|
||||
associative entities from solid model, as a special group
|
||||
|
|
Loading…
Reference in New Issue