Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mirror groups #72

Open
whitequark opened this issue Oct 14, 2016 · 36 comments · May be fixed by #1109
Open

Mirror groups #72

whitequark opened this issue Oct 14, 2016 · 36 comments · May be fixed by #1109

Comments

@whitequark
Copy link
Contributor

A mirror group would mirror entities and meshes against a specified workplane (exactly one). This makes it possible to achieve the following forms of symmetry, starting from the shape ∀∃:

  1. by using a mirror group once:

    ∀∃ E∀
    
  2. by using a mirror group twice, with the same initial group active during second "Mirror" command

    ∀∃ E∀
    A∃
    
  3. by using a mirror group twice, with the first mirror group active during second "Mirror" command

    ∀∃ E∀
    A∃ EA
    

Unresolved questions: whether there should also be a way to use a point and a normal, while locked in workplane, to specify the mirror plane. It adds some logic, but may be more convenient, especially later in a sketch when none of the reference planes are handy.

Feature originally requested by Fong Lin.

@whitequark
Copy link
Contributor Author

@jwesthues Any comments on this?

@whitequark whitequark modified the milestone: 3.0 Oct 14, 2016
@jwesthues
Copy link
Member

Mirror against an existing workplane (or point and normal vector) is reasonable, and easy to implement, no solver work required.

The more natural implementation might be to introduce mirrored point and normal types, analogous to the rotated points and normals, and let the solver determine the mirror plane. You could still trivially mirror about an existing workplane, with a point-symmetric constraint.

You can do arbitrary 2d reflections now, since they're equivalent to out-of-plane rotations; but that's not too elegant.

@whitequark
Copy link
Contributor Author

The more natural implementation might be to introduce mirrored point and normal types, analogous to the rotated points and normals, and let the solver determine the mirror plane.

This is a very nice idea; we should do this.

@whitequark
Copy link
Contributor Author

You can do arbitrary 2d reflections now, since they're equivalent to out-of-plane rotations; but that's not too elegant.

@jwesthues Actually, looks like you cannot in case of a text entity.

@whitequark
Copy link
Contributor Author

Actually, looks like you cannot in case of a text entity.

To be more specific, out-of-plane rotations work in all cases, but copying and pasting transformed with scale of -1 does not for text entities (nor image entities, I think).

@jwesthues
Copy link
Member

That makes sense. The "text by two points" representation lacks sufficient degrees of freedom to handle reflections, though the third point would make that possible...

@whitequark
Copy link
Contributor Author

We actually have four points now--this allows to e.g. outline a text with a bounding box easily. But the way it's implemented (ping @Evil-Spirit) means there is no way to represent mirrored text still.

@Evil-Spirit
Copy link
Collaborator

@whitequark I don't see any problems here, anyway we have to say text to be mirrored somehow. When we will geneate sketch mirror, we don't need to write equations because entities just a copies, so we can place any point where we want it to be.

@whitequark
Copy link
Contributor Author

@Evil-Spirit I'm talking about mirroring via Paste Transformed here. It's still useful if the mirrored half needs to be modified afterwards. See also #223.

@Evil-Spirit
Copy link
Collaborator

@whitequark, I think we anyway need to introduce some flags for chirality preserving of all constraints and entities which generates equations. So, for text and image it will be just mirrorX, mirrorY flags

@Evil-Spirit
Copy link
Collaborator

@whitequark, I propose to make universal group for arrays which can create step-and-transfrom by specifing transform formulas for translation, rotation and [uniform]scale. e. g. guitar frets can be adjusted using this approach.

@whitequark
Copy link
Contributor Author

So far I don't think anyone I've talked to discovered even that they can use scale = -1 on their own, I wouldn't expect this feature to be used much.

@phkahler
Copy link
Member

phkahler commented Sep 14, 2019

I started a mirror branch in my fork. The goal is to reflect points across an invisible plane which is figured out by the solver - there should be 3 DoF for reflected points. There is no shell support at the moment, I'm just trying to copy sketch entities. It's crashing for reasons I don't understand yet (not if I comment out the second copy which is mirrored). Copying entities with transformations is one of the more difficult parts of the code for me. I searched for N_ROT_AXIS_TRANS which is the type I used for helix and made sure all the corresponding parts are there for MIRROR. Normals should just be copied for now - I can actually reflect them once the entities copy. If anyone can have a look or suggest something I'd appreciate that.

Edit: looks like the math in ForceTo is wrong, but that shouldn't cause a crash.

@phkahler
Copy link
Member

phkahler commented Sep 14, 2019

@jwesthues how are normals represented as quaternions? In other words, I have a unit normal vector n=(x,y,z) and I need to put it in quaternion form after transformation (reflection). This is for EntityBase::NormalGetNum();

BTW the crash was due to not handling normals right. Now they just don't change orientation when reflecting, so arcs don't work right. I can imagine there are other consequences that I'm not aware of too. Also, do I have to handle faces as well? There are different face types in entity.cpp and they get created on copy in group.cpp.

I will try to write down some kind of document or how-to for creating new solver-based entity transformations once I understand it better.

@jwesthues
Copy link
Member

It's not possible to go from just a unit vector to a SolveSpace normal--the unit vector is 2 DOF and the normal is 3 DOF, where the third DOF represents twist about that vector. A normal in SolveSpace is unlike a vector and like a 3x3 rotation matrix (or a unit quaternion, or any other representation of SO(3)). So e.g. a point plus a normal represent the 6 DOF of a rigid body in free space, and thus the transformation of a part in an assembly.

The faces also need to be transformed.

@phkahler
Copy link
Member

I think there's some confusion. So a normal in solvespace in really an orientation. Is it an actual quaternion or something similar? I seem to recall rotations being represented by a unit-vector and an angle to rotate about that axis, rather than the usual quaternion way. I'd like to understand this better, It I needed to get a 3-vector from a normal, what should I do? And how to go the other way? The bottom line is that I need to populate

Quaternion EntityBase::NormalGetNum()

with something for my new case Type::NORMAL_MIRROR

@phkahler
Copy link
Member

Oh, I think I got it. I'll rotate the normal 180 degrees around my mirror normal-vector.

@phkahler
Copy link
Member

The visible issue is that arcs stay parallel to the sketch plane even when the reflection requires something else. IIRC arcs have a "normal" of some sort, or a plane they exist in. I'm not sure if it's defined by a "normal" entity or a face or what, but they're not behaving correctly.

@Evil-Spirit can you have a look at my mirror branch. Just draw a rectangle with rounded corners and press CTRL-X to mirror it. I'd appreciate that.

@jwesthues
Copy link
Member

The plane of the arc is defined by a normal. The points would usually do it, but that breaks for a semicircle.

There's no continuous representation of a "normal" (as you say, an orientation, 3 rotational DOF) in terms of three scalars,

https://en.wikipedia.org/wiki/Hairy_ball_theorem

SolveSpace uses a unit quaternion for assemblies. For step and repeat rotating, it uses an axis-angle representation, though the axis is already fixed and can't be changed by the solver.

@phkahler
Copy link
Member

Got it. Made a stupid mistake in reflecting normals which made nothing happen to them. Rotating 180 around the mirror axis(normal) was the right thing to do. They might want to be flipped as well, but I'd want and see if anything doesn't work right.

@phkahler
Copy link
Member

@ghoss I noticed you have done some work on Mirror groups. How did that turn out? It looks like you created a new copy-type for it, how did that go?

@ghoss
Copy link
Contributor

ghoss commented May 19, 2020

Negative, unfortunately. I didn't get much further than the copy-type at the time as I couldn't gain enough understanding of the existing group mechanism in the codebase to integrate my additions.

@phkahler
Copy link
Member

Status update: I've tried two different ways to implement this and will likely start again a 3rd time. There are two branches in my repo you can try and I'll comment on.

mirror_groups

This branch I implemented a new "copy type" for entities using a mirror plane between the original entities and the new ones. The mirror plane is defined by equation ax+by+cz-d = 0 (or is it +d?) with magnitude of (a,b,c)=1 that leave d as the distance from the origin to the mirror plane along the mirror normal (a,b,c). Standard plane stuff. The 4 mirror parameters are free variables in the solver with one constraint Magnitude(a,b,c)=1. which leaves 3 DoF. For this branch I also added a constraint for sketches so that (a,b,c) is in the sketch plane which makes the copy stay in-plane. The way these groups move is very interesting. When you drag a point, the ForceTo function sets the mirror plane between the dragged point and its parent, which make everything move. There are some broken and incomplete things in this branch, and it was a lot of work to make a new copy-type for entities. I also created a new function to mirror a Shell.

new-mirror

The next try grew out of an experiment to make a "copy" group (shift-C). It just makes a 6DoF copy of the current group which can be moved around. This uses the existing copy-type for linked sketches which has 3 parameters for displacement and 4 for a quaternion (rotation to get arbitrary orientation). The existing functions support a scale for each group, including the shell copy function. This generic copy was turned into a mirror-copy by doing the following:

For the quaternion, constrain w=0 and then treat the remaining (x,y,z) as a vector and constrain its length to 1. Then constrain the displacement vector to be a multiple of (x,y,z). I created an 8th free parameter for the multiplier. Then set the group scale = -1. A mirror transformation like the first branch can be done by:

  1. Scale everything by -1
  2. Rotate 180 degrees around the mirror normal vector
  3. translate along the mirror normal by twice the plane distance to the origin.

By forcing the quaternion rotation axis to be the same as the translation, we get (3). By keeping it unit length and forcing w=0 we ensure (2) and the group scale does (1)

No new copy type, no new shell copy function. Yay! Aside from some broken stuff (hovering over the new copy crashes) this constrains the motion the same way as my first attempt, but it does not feel right at all. I think the reason is that the force function for that copy-type just moves the location and not the quaternion, so the point is moved and then the solver applies the constraints and it goes somewhere else. Sometimes it drags in the right-ish direction and sometimes it actually moves the opposite way as your motion.

Another problem with the second approach is that I can not create a numeric copy of the original sketch - in place. That's because the original and the mirror copy share the scale parameter for the entire group, so an in-place copy will actually flip and be somewhere else (entities only, the shell can be kept still).

a third option

So approach 1 is super complex and approach 2 has issues but was much simpler to do. I'm thinking approach 3 will create a new copy-type very much like the existing linked one, but will use a solver parameter for the scale. SCALE_ROT_TRANS. I think it's also a good idea to copy all points to be of type "free in 3d" or whatever where the x,y,z are all parameters in the solver and use mirror constraints (in the solver, not in the copy type) to enforce the mirror condition on each point. I think that will allow proper dragging, while having values of "1" and "-1" as constrained parameters will allow both a static and mirror copy to exist in the same group.

Another reason I want this new copy type is to support "frustum" extrusions. I did an experimental frustum group a couple years ago that used a new copy-type and this one could be used for that as well (the rotation is not needed for frustum, so we could just add constraints to the quaternion to lock it in place). Most of the code for this copy-type would be identical to that of the existing one.

Just sharing some experiments, all of these branches are on my fork (including frustum and copy) if anyone wants to try them. None of them are destined for SolveSpace in their current forms.

@Evil-Spirit
Copy link
Collaborator

A good idea is create new kind of copy - with arbitrary transform expression. Just create a base class "CopyTransform" and some virtual functions to override. You can play with it e.g. use matrices instead of quaternions witch is better in some of my experiments - look at https://github.com/NoteCAD/NoteCAD/blob/master/Assets/Code/Solver/ExpBasis.cs

@Evil-Spirit
Copy link
Collaborator

Evil-Spirit commented May 25, 2020

	public void GenerateEquations(EquationSystem sys) {
		sys.AddParameters(parameters);

		sys.AddEquation(u.Magnitude() - 1.0);
		sys.AddEquation(v.Magnitude() - 1.0);

		var cross = ExpVector.Cross(u, v);
		var dot = ExpVector.Dot(u, v);
		sys.AddEquation(Exp.Atan2(cross.Magnitude(), dot) - Math.PI / 2);
		sys.AddEquation(n - ExpVector.Cross(u, v));
	}

How do you like that, @jwesthues?

@whitequark
Copy link
Contributor Author

I like @Evil-Spirit's idea, but I'm not really qualified to see whether it has some non-obvious downsides.

@jwesthues
Copy link
Member

I'd probably have started with something like @phkahler's new-mirror? Any mirroring (e.g., scale all the coordinates by -1) followed by an arbitrary translation/rotation makes an arbitrary mirroring, as he says. The only remaining job is to lock down the excess degrees of freedom, if even desired; perhaps it would be better just to introduce his "copy" group with a checkbox for "mirrored"?

@phkahler
Copy link
Member

@Evil-Spirit I like generality too. SolveSpace already has a nice quaternion implementation that doesn't require as many (1 vs 6) constraints. There are also functions to get the (u,v,n) vectors from them if needed. When there is a case that wants full generality I wouldn't hesitate to do a 3x3 transform.

@jwesthues I tried copy group in #614, I think the branch is still available. It's not easy to postion things with 6DoF although it is more general. You idea to make a virtual mirror plane works really well but is only 3DoF and is actually a bit weird. I think the most likely uses for mirror would be to select a line (or 2 points) to mirror a sketch, and selecting a face in 3D. In either case there would be zero DoF for the copy.

When doing copy group, I also really wanted all the copies to end up in the same group so the solver could work on all at once. I built a dodecahedron by making 11 copies of an extruded pentagon. It was fun, but by the last one I got a boolean issue - perhaps the errors accumulated? If they were all in the same group it would solve everything to within the same tolerance. I'm not sure the original should be part of the copy group though, and that would be strange too. The same can be said for assemblies - it would be nice to link parts into a single group for solving.

Thank you all for the feedback, these are things I hope to try more after 3.0. The ideas keep changing and I'm starting to see more ways to combine things that seemed unrelated before.

@phkahler
Copy link
Member

@ghoss FYI I dropped a text file in here: https://github.com/solvespace/solvespace/tree/master/developer_docs That covers some of the copy-type stuff. Not sure how clear it is. Also not sure if you've been following but Ruevs finally got the "right" way to do Intersection as a single boolean - I doubt that would have happened if not for your initial attempt and A-(A-B) implementation, so Thank You for the initial attempt!

@Evil-Spirit
Copy link
Collaborator

Evil-Spirit commented May 26, 2020

@phkahler I know all about this quaternions and also I made comparsion between the other variants of quaternion equations (like sin cos version with angles etc) and matrix equations (euler versions etc). There is not only equation number should be considered while you writing equations, but also convergence is meangingful. Solvespace solves 3d assemblies step by step, so convergence is not so bad as you write all the equations simultaniously (which is good in some cases).

@phkahler phkahler added this to the 4.0 milestone Oct 12, 2020
@phkahler
Copy link
Member

phkahler commented Jan 26, 2021

@jwesthues I tried mirror groups again here:
https://github.com/phkahler/solvespace/tree/srt-mirror
First commit creates a new SCALE_ROT_TRANS copy type that can do all 3 operations using 8 parameters. I'd like to use that copy type for a lot more things. It could have been used for most of the existing operations with appropriate constraints on the DoF.

The second commit uses SRT to create mirror groups as described earlier. It adds 5 constraints:
Scale = -1
Quaternion x,y,z have to be unit length (they point in the direction of the mirror normal)
Quaternion w = 0 because we always want a 180 degree rotation around that axis
Translation = D * the quaternion x,y,z

D is introduced as a 9th free parameter for the group but not the copy type. Normally it would be twice the distance from origin to mirror plane, but there is no explicit mirror plane here.

The big problem I have with all this is dragging. The function Entity::PointForceTo() confuses me. Firstly there is no correct way to drag a point when there are 8 parameters in the transformation. I could make it work for the mirror group, but that's a special case and the math won't apply correctly to other variations. If I leave it blank, points are not draggable.

What is the purpose of EntityBase::PointGetExprs() if not to allow the solver to handle the kinematics of dragging?

Worst case, as I suggested last year is to make all these new points generic point-in-3d each with 3DoF and create a set of constraints for each one as would be defined in PointGetExprs. That seems excessive.

Would it be reasonably easy to modify the dragging code to add the expressions of a dragged point to the solver and treat it like a generic point in 3d? And drop those constraints after the drag. Would this be better?

Thanks for any suggestions.

BTW the new mirror group will crash if you hover over a face. Drag points carefully ;-)

@jwesthues
Copy link
Member

What behavior do you want when those points are dragged? For example, should dragging the mirrored point drag only the distance to the mirror plane? Move the mirror plane in a way that permits arbitrary translation of the mirrored geometry, while maintaining the geometry's orientation? Something else?

Subjective decisions like that are the reason why I didn't leave inverting the map from specialty point parameters to (x, y, z) to the solver. For example, those hand-coded inversions are where we decide that dragging a point on a part in an assembly shows up (as much as possible given the constraints) as a translation of the part, and not as a rotation. PointGetExprs() is still needed to write the constraint equations.

@phkahler
Copy link
Member

For example, those hand-coded inversions are where we decide that dragging a point on a part in an assembly shows up (as much as possible given the constraints) as a translation of the part, and not as a rotation.

That is the one that stood out to me. It seems to make sense. But if you draw a rectangle with 4 lines and 3 perpendicular constraints it will rotate as you drag a corner. Another use was with Helix. Dragging a point on the axis changes the length of extrusion, while dragging an off-axis point changes the angle. But I did that mostly because the real math was hard and even the operation of the code was new to me. I've come to like it though.

For a mirror, I would take the midpoint of the numeric point and the dragged-to position and put the mirror plane there. The mirror normal vector is thus the normalized vector from numeric point to the point it is forced to. The D distance to translate is twice the distance from origin to plane (dot product of the normal vector and the midpoint). The rotation axis is the same as the mirror normal vector. One of my previous tries used this and I liked it even if it's a bit weird.

If you think about it, a mirrored sketch behaves the same way as the ends of a revolved extrusion. Do a double-sided Revolve to see the mirror behaviour clearly. But if one end is held still we could still construct a curved extrusion from the sketch to its mirror.

I want to use a sequence of these to extrude along a path. Each "station" will be a mirror copy of the previous one. However I'd like to use SCALE_ROT_TRANS to go directly from the original sketch to the Nth station, and use constraint equations to tie the transforms together (as opposed to the points from one to the other). This way we can use the direct transform from base-sketch to Nth station rather than stacking the errors from N mirror transformations when building the shell.

I would also like to include different "segment" types in such an extrusion. We could have straight sections like standard extrude, curved one, some that can change size/scale along the way, and my favorite - some that are straight extrusion with scaling and twist - used to make wind turbine blades. A pump volute would be changing size along a curve (like a snail shell).

Maybe I'm off my rocker, but this would all be fairly easy for me to try now if the inverse kinematics were actually handled by the solver ;-)

Maybe in mouse.cpp (which I never really looked at before) instead of "AddPointToDraggedList" we could create a new point as POINT_IN_3D along with new constraints to tie it to the point that is now being dragged. When the drag is done, we'd delete these new points, their parameters, and constraints. If that worked well we could make the old way optional depending on... point type? Would that work? Is it somehow a bad idea?

Take my new mirror branch. Make a sketch, extrude it and SHIFT-M to make a mirror copy. Now draw a short line segment connecting to one of the points. Constrain the line length to some short value. Now you can drag the mirrored extrusion by this little handle and the solver will maintain the virtual mirror just fine. So imagine it's not a line segment, but a temporary point constrained to be coincident with the one dragged. That's what I meant in the previous paragraph in case it wasn't clear.

@jwesthues
Copy link
Member

jwesthues commented Jan 26, 2021

I think most people would probably rather that rectangle keep the same orientation as it was dragged? But the solver now just minimizes the sum of the squares of the distances that the points move, so it has no concept of orientation. It would of course be possible to add something about the slopes of lines to the cost function, but (a) those are trickier equations to write and solve, and (b) that would probably lead to new surprising behavior. For a part in an assembly, the orientation is separate solver parameters and it's a much simpler problem.

In any case, your proposals above deserve a proper response, which I'll write once I have time to play with your branch. Just for experimentation, you can of course get the behavior that you describe by sketching an extra datum point, and constraining that coincident with (or a fixed distance in all three axes from) your otherwise-undraggable point. (ETA: Which is exactly what you propose above with the line handle, of course.) If I wanted to implement the equivalent of that for real, then I'd probably just write three extra solver equations for the point that I wanted to place, weakly weighted so nothing blows up if they can't be exactly satisfied (if the point doesn't have all three DOF).

@ruevs
Copy link
Member

ruevs commented Jan 27, 2021

Without understanding the math or the logic behind it too deeply I played with the branch and if one selects the "correct" point to drag (which is the "origin" of the mirrored copy) then dragging behaves perfectly reasonably. If one selects any other point the dragging is... "crazy".

Here is a video to demonstrate https://www.youtube.com/watch?v=X9U80Nc4PYc

@phkahler
Copy link
Member

@ruevs and @jwesthues Here is roughly the correct code to put in EntityBase::PointForceTo() for that new mirror branch. The problem is that this is mirror specific, and also I don't set group parameter 9 = 2*d because this is in the entity code so the group parameter handle is not readily available. It seem to work fairly well, so long as you don't drag a point near its parent for the previous group.

        case Type::POINT_SRT: {
            // The inverse kinematics here are for mirror groups
            // the mirror plane contains the mid point
            Vector mid = (numPoint.Plus(p)).ScaledBy(0.5);
            Vector axis = p.Minus(numPoint).WithMagnitude(1.0);
            double d = mid.Dot(axis);
            Vector trans = axis.ScaledBy(2*d);
            SK.GetParam(param[0])->val = trans.x;
            SK.GetParam(param[1])->val = trans.y;
            SK.GetParam(param[2])->val = trans.z;
            SK.GetParam(param[4])->val = axis.x;
            SK.GetParam(param[5])->val = axis.y;
            SK.GetParam(param[6])->val = axis.z;
            break;
        }

One problem I had in a previous attempt was that I used the group "scale" set to -1, but that applied to all entites so I couldn't copy the original group into the mirror group (it needs scale=1). By putting the scale in param[7] I can have both mirrored and non-mirrored entites in one group.

Long ago I also created a frustum group which was extrusion without the perpendicular constraints and will a resizable "top". That suffered because the resize would only happen with constraints - I had used the same "move only" inverse kinematics which are also not compatible with this code snippet.

I think the best solution is to have an option (per copy type) to let the solver handle dragging.

Try mirroring a 2d sketch and then move the copy directly in front of the original sketch. Once there, try to imagine a flexible extrusion between them. That's how I want path extrude to work. It's also 3DoF so a pt-pt coincident constraint will fully constrain it. Then imagine multiple segments draggable in the same group!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

6 participants