1 //-----------------------------------------------------------------------------
2 // The 2d vector output stuff that isn't specific to any particular file
3 // format: getting the appropriate lines and curves, performing hidden line
4 // removal, calculating bounding boxes, and so on. Also raster and triangle
5 // mesh output.
6 //
7 // Copyright 2008-2013 Jonathan Westhues.
8 //-----------------------------------------------------------------------------
9 #include "solvespace.h"
10 #ifndef WIN32
11 #include <unix/gloffscreen.h>
12 #endif
13 #include <png.h>
14 
ExportSectionTo(const std::string & filename)15 void SolveSpaceUI::ExportSectionTo(const std::string &filename) {
16     Vector gn = (SS.GW.projRight).Cross(SS.GW.projUp);
17     gn = gn.WithMagnitude(1);
18 
19     Group *g = SK.GetGroup(SS.GW.activeGroup);
20     g->GenerateDisplayItems();
21     if(g->displayMesh.IsEmpty()) {
22         Error("No solid model present; draw one with extrudes and revolves, "
23               "or use Export 2d View to export bare lines and curves.");
24         return;
25     }
26 
27     // The plane in which the exported section lies; need this because we'll
28     // reorient from that plane into the xy plane before exporting.
29     Vector origin, u, v, n;
30     double d;
31 
32     SS.GW.GroupSelection();
33 #define gs (SS.GW.gs)
34     if((gs.n == 0 && g->activeWorkplane.v != Entity::FREE_IN_3D.v)) {
35         Entity *wrkpl = SK.GetEntity(g->activeWorkplane);
36         origin = wrkpl->WorkplaneGetOffset();
37         n = wrkpl->Normal()->NormalN();
38         u = wrkpl->Normal()->NormalU();
39         v = wrkpl->Normal()->NormalV();
40     } else if(gs.n == 1 && gs.faces == 1) {
41         Entity *face = SK.GetEntity(gs.entity[0]);
42         origin = face->FaceGetPointNum();
43         n = face->FaceGetNormalNum();
44         if(n.Dot(gn) < 0) n = n.ScaledBy(-1);
45         u = n.Normal(0);
46         v = n.Normal(1);
47     } else if(gs.n == 3 && gs.vectors == 2 && gs.points == 1) {
48         Vector ut = SK.GetEntity(gs.entity[0])->VectorGetNum(),
49                vt = SK.GetEntity(gs.entity[1])->VectorGetNum();
50         ut = ut.WithMagnitude(1);
51         vt = vt.WithMagnitude(1);
52 
53         if(fabs(SS.GW.projUp.Dot(vt)) < fabs(SS.GW.projUp.Dot(ut))) {
54             swap(ut, vt);
55         }
56         if(SS.GW.projRight.Dot(ut) < 0) ut = ut.ScaledBy(-1);
57         if(SS.GW.projUp.   Dot(vt) < 0) vt = vt.ScaledBy(-1);
58 
59         origin = SK.GetEntity(gs.point[0])->PointGetNum();
60         n = ut.Cross(vt);
61         u = ut.WithMagnitude(1);
62         v = (n.Cross(u)).WithMagnitude(1);
63     } else {
64         Error("Bad selection for export section. Please select:\n\n"
65               "    * nothing, with an active workplane "
66                         "(workplane is section plane)\n"
67               "    * a face (section plane through face)\n"
68               "    * a point and two line segments "
69                         "(plane through point and parallel to lines)\n");
70         return;
71     }
72     SS.GW.ClearSelection();
73 
74     n = n.WithMagnitude(1);
75     d = origin.Dot(n);
76 
77     SEdgeList el = {};
78     SBezierList bl = {};
79 
80     // If there's a mesh, then grab the edges from it.
81     g->runningMesh.MakeEdgesInPlaneInto(&el, n, d);
82 
83     // If there's a shell, then grab the edges and possibly Beziers.
84     g->runningShell.MakeSectionEdgesInto(n, d,
85        &el,
86        (SS.exportPwlCurves || fabs(SS.exportOffset) > LENGTH_EPS) ? NULL : &bl);
87 
88     // All of these are solid model edges, so use the appropriate style.
89     SEdge *se;
90     for(se = el.l.First(); se; se = el.l.NextAfter(se)) {
91         se->auxA = Style::SOLID_EDGE;
92     }
93     SBezier *sb;
94     for(sb = bl.l.First(); sb; sb = bl.l.NextAfter(sb)) {
95         sb->auxA = Style::SOLID_EDGE;
96     }
97 
98     el.CullExtraneousEdges();
99     bl.CullIdenticalBeziers();
100 
101     // And write the edges.
102     VectorFileWriter *out = VectorFileWriter::ForFile(filename);
103     if(out) {
104         // parallel projection (no perspective), and no mesh
105         ExportLinesAndMesh(&el, &bl, NULL,
106                            u, v, n, origin, 0,
107                            out);
108     }
109     el.Clear();
110     bl.Clear();
111 }
112 
ExportViewOrWireframeTo(const std::string & filename,bool wireframe)113 void SolveSpaceUI::ExportViewOrWireframeTo(const std::string &filename, bool wireframe) {
114     int i;
115     SEdgeList edges = {};
116     SBezierList beziers = {};
117 
118     VectorFileWriter *out = VectorFileWriter::ForFile(filename);
119     if(!out) return;
120 
121     SS.exportMode = true;
122     GenerateAll(GENERATE_ALL);
123 
124     SMesh *sm = NULL;
125     if(SS.GW.showShaded || SS.GW.showHdnLines) {
126         Group *g = SK.GetGroup(SS.GW.activeGroup);
127         g->GenerateDisplayItems();
128         sm = &(g->displayMesh);
129     }
130     if(sm && sm->IsEmpty()) {
131         sm = NULL;
132     }
133 
134     for(i = 0; i < SK.entity.n; i++) {
135         Entity *e = &(SK.entity.elem[i]);
136         if(!e->IsVisible()) continue;
137         if(e->construction) continue;
138 
139         if(SS.exportPwlCurves || sm || fabs(SS.exportOffset) > LENGTH_EPS)
140         {
141             // We will be doing hidden line removal, which we can't do on
142             // exact curves; so we need things broken down to pwls. Same
143             // problem with cutter radius compensation.
144             e->GenerateEdges(&edges);
145         } else {
146             e->GenerateBezierCurves(&beziers);
147         }
148     }
149 
150     if(SS.GW.showEdges) {
151         Group *g = SK.GetGroup(SS.GW.activeGroup);
152         g->GenerateDisplayItems();
153         SEdgeList *selr = &(g->displayEdges);
154         SEdge *se;
155         for(se = selr->l.First(); se; se = selr->l.NextAfter(se)) {
156             edges.AddEdge(se->a, se->b, Style::SOLID_EDGE);
157         }
158     }
159 
160     if(SS.GW.showConstraints) {
161         if(!out->OutputConstraints(&SK.constraint)) {
162             // The output format cannot represent constraints directly,
163             // so convert them to edges.
164             Constraint *c;
165             for(c = SK.constraint.First(); c; c = SK.constraint.NextAfter(c)) {
166                 c->GetEdges(&edges);
167             }
168         }
169     }
170 
171     if(wireframe) {
172         Vector u = Vector::From(1.0, 0.0, 0.0),
173                v = Vector::From(0.0, 1.0, 0.0),
174                n = Vector::From(0.0, 0.0, 1.0),
175                origin = Vector::From(0.0, 0.0, 0.0);
176         double cameraTan = 0.0,
177                scale = 1.0;
178 
179         out->SetModelviewProjection(u, v, n, origin, cameraTan, scale);
180 
181         ExportWireframeCurves(&edges, &beziers, out);
182     } else {
183         Vector u = SS.GW.projRight,
184                v = SS.GW.projUp,
185                n = u.Cross(v),
186                origin = SS.GW.offset.ScaledBy(-1);
187 
188         out->SetModelviewProjection(u, v, n, origin,
189                                     SS.CameraTangent()*SS.GW.scale, SS.exportScale);
190 
191         ExportLinesAndMesh(&edges, &beziers, sm,
192                            u, v, n, origin, SS.CameraTangent()*SS.GW.scale,
193                            out);
194 
195         if(!out->HasCanvasSize()) {
196             // These file formats don't have a canvas size, so they just
197             // get exported in the raw coordinate system. So indicate what
198             // that was on-screen.
199             SS.justExportedInfo.showOrigin = true;
200             SS.justExportedInfo.pt = origin;
201             SS.justExportedInfo.u = u;
202             SS.justExportedInfo.v = v;
203         } else {
204             SS.justExportedInfo.showOrigin = false;
205         }
206 
207         SS.justExportedInfo.draw = true;
208         InvalidateGraphics();
209     }
210 
211     edges.Clear();
212     beziers.Clear();
213 }
214 
ExportWireframeCurves(SEdgeList * sel,SBezierList * sbl,VectorFileWriter * out)215 void SolveSpaceUI::ExportWireframeCurves(SEdgeList *sel, SBezierList *sbl,
216                            VectorFileWriter *out)
217 {
218     SBezierLoopSetSet sblss = {};
219     SEdge *se;
220     for(se = sel->l.First(); se; se = sel->l.NextAfter(se)) {
221         SBezier sb = SBezier::From(
222                                 (se->a).ScaledBy(1.0 / SS.exportScale),
223                                 (se->b).ScaledBy(1.0 / SS.exportScale));
224         sblss.AddOpenPath(&sb);
225     }
226 
227     sbl->ScaleSelfBy(1.0/SS.exportScale);
228     SBezier *sb;
229     for(sb = sbl->l.First(); sb; sb = sbl->l.NextAfter(sb)) {
230         sblss.AddOpenPath(sb);
231     }
232 
233     out->OutputLinesAndMesh(&sblss, NULL);
234     sblss.Clear();
235 }
236 
ExportLinesAndMesh(SEdgeList * sel,SBezierList * sbl,SMesh * sm,Vector u,Vector v,Vector n,Vector origin,double cameraTan,VectorFileWriter * out)237 void SolveSpaceUI::ExportLinesAndMesh(SEdgeList *sel, SBezierList *sbl, SMesh *sm,
238                                     Vector u, Vector v, Vector n,
239                                         Vector origin, double cameraTan,
240                                     VectorFileWriter *out)
241 {
242     double s = 1.0 / SS.exportScale;
243 
244     // Project into the export plane; so when we're done, z doesn't matter,
245     // and x and y are what goes in the DXF.
246     SEdge *e;
247     for(e = sel->l.First(); e; e = sel->l.NextAfter(e)) {
248         // project into the specified csys, and apply export scale
249         (e->a) = e->a.InPerspective(u, v, n, origin, cameraTan).ScaledBy(s);
250         (e->b) = e->b.InPerspective(u, v, n, origin, cameraTan).ScaledBy(s);
251     }
252 
253     SBezier *b;
254     if(sbl) {
255         for(b = sbl->l.First(); b; b = sbl->l.NextAfter(b)) {
256             *b = b->InPerspective(u, v, n, origin, cameraTan);
257             int i;
258             for(i = 0; i <= b->deg; i++) {
259                 b->ctrl[i] = (b->ctrl[i]).ScaledBy(s);
260             }
261         }
262     }
263 
264     // If cutter radius compensation is requested, then perform it now
265     if(fabs(SS.exportOffset) > LENGTH_EPS) {
266         // assemble those edges into a polygon, and clear the edge list
267         SPolygon sp = {};
268         sel->AssemblePolygon(&sp, NULL);
269         sel->Clear();
270 
271         SPolygon compd = {};
272         sp.normal = Vector::From(0, 0, -1);
273         sp.FixContourDirections();
274         sp.OffsetInto(&compd, SS.exportOffset*s);
275         sp.Clear();
276 
277         compd.MakeEdgesInto(sel);
278         compd.Clear();
279     }
280 
281     // Now the triangle mesh; project, then build a BSP to perform
282     // occlusion testing and generated the shaded surfaces.
283     SMesh smp = {};
284     if(sm) {
285         Vector l0 = (SS.lightDir[0]).WithMagnitude(1),
286                l1 = (SS.lightDir[1]).WithMagnitude(1);
287         STriangle *tr;
288         for(tr = sm->l.First(); tr; tr = sm->l.NextAfter(tr)) {
289             STriangle tt = *tr;
290             tt.a = (tt.a).InPerspective(u, v, n, origin, cameraTan).ScaledBy(s);
291             tt.b = (tt.b).InPerspective(u, v, n, origin, cameraTan).ScaledBy(s);
292             tt.c = (tt.c).InPerspective(u, v, n, origin, cameraTan).ScaledBy(s);
293 
294             // And calculate lighting for the triangle
295             Vector n = tt.Normal().WithMagnitude(1);
296             double lighting = SS.ambientIntensity +
297                                   max(0.0, (SS.lightIntensity[0])*(n.Dot(l0))) +
298                                   max(0.0, (SS.lightIntensity[1])*(n.Dot(l1)));
299             double r = min(1.0, tt.meta.color.redF()   * lighting),
300                    g = min(1.0, tt.meta.color.greenF() * lighting),
301                    b = min(1.0, tt.meta.color.blueF()  * lighting);
302             tt.meta.color = RGBf(r, g, b);
303             smp.AddTriangle(&tt);
304         }
305     }
306 
307     SMesh sms = {};
308 
309     // We need the mesh for occlusion testing, but if we don't/can't export it,
310     // don't generate it.
311     if(SS.GW.showShaded && out->CanOutputMesh()) {
312         // Use the BSP routines to generate the split triangles in paint order.
313         SBsp3 *bsp = SBsp3::FromMesh(&smp);
314         if(bsp) bsp->GenerateInPaintOrder(&sms);
315         // And cull the back-facing triangles
316         STriangle *tr;
317         sms.l.ClearTags();
318         for(tr = sms.l.First(); tr; tr = sms.l.NextAfter(tr)) {
319             Vector n = tr->Normal();
320             if(n.z < 0) {
321                 tr->tag = 1;
322             }
323         }
324         sms.l.RemoveTagged();
325     }
326 
327     // And now we perform hidden line removal if requested
328     SEdgeList hlrd = {};
329     if(sm) {
330         SKdNode *root = SKdNode::From(&smp);
331 
332         // Generate the edges where a curved surface turns from front-facing
333         // to back-facing.
334         if(SS.GW.showEdges) {
335             root->MakeCertainEdgesInto(sel, SKdNode::TURNING_EDGES,
336                                        /*coplanarIsInter=*/false, NULL, NULL,
337                                        GW.showOutlines ? Style::OUTLINE : Style::SOLID_EDGE);
338         }
339 
340         root->ClearTags();
341         int cnt = 1234;
342 
343         SEdge *se;
344         for(se = sel->l.First(); se; se = sel->l.NextAfter(se)) {
345             if(se->auxA == Style::CONSTRAINT) {
346                 // Constraints should not get hidden line removed; they're
347                 // always on top.
348                 hlrd.AddEdge(se->a, se->b, se->auxA);
349                 continue;
350             }
351 
352             SEdgeList edges = {};
353             // Split the original edge against the mesh
354             edges.AddEdge(se->a, se->b, se->auxA);
355             root->OcclusionTestLine(*se, &edges, cnt, /*removeHidden=*/!SS.GW.showHdnLines);
356             // the occlusion test splits unnecessarily; so fix those
357             edges.MergeCollinearSegments(se->a, se->b);
358             cnt++;
359             // And add the results to our output
360             SEdge *sen;
361             for(sen = edges.l.First(); sen; sen = edges.l.NextAfter(sen)) {
362                 hlrd.AddEdge(sen->a, sen->b, sen->auxA);
363             }
364             edges.Clear();
365         }
366 
367         sel = &hlrd;
368     }
369 
370     // Clean up: remove overlapping line segments and
371     // segments with zero-length projections.
372     sel->l.ClearTags();
373     for(int i = 0; i < sel->l.n; ++i) {
374         SEdge *sei = &sel->l.elem[i];
375         hStyle hsi = { (uint32_t)sei->auxA };
376         Style *si = Style::Get(hsi);
377         if(sei->tag != 0) continue;
378 
379         // Remove segments with zero length projections.
380         Vector ai = sei->a;
381         ai.z = 0.0;
382         Vector bi = sei->b;
383         bi.z = 0.0;
384         Vector di = bi.Minus(ai);
385         if(fabs(di.x) < LENGTH_EPS && fabs(di.y) < LENGTH_EPS) {
386             sei->tag = 1;
387             continue;
388         }
389 
390         for(int j = i + 1; j < sel->l.n; ++j) {
391             SEdge *sej = &sel->l.elem[j];
392             if(sej->tag != 0) continue;
393 
394             Vector *pAj = &sej->a;
395             Vector *pBj = &sej->b;
396 
397             // Remove segments with zero length projections.
398             Vector aj = sej->a;
399             aj.z = 0.0;
400             Vector bj = sej->b;
401             bj.z = 0.0;
402             Vector dj = bj.Minus(aj);
403             if(fabs(dj.x) < LENGTH_EPS && fabs(dj.y) < LENGTH_EPS) {
404                 sej->tag = 1;
405                 continue;
406             }
407 
408             // Skip non-collinear segments.
409             const double eps = 1e-6;
410             if(aj.DistanceToLine(ai, di) > eps) continue;
411             if(bj.DistanceToLine(ai, di) > eps) continue;
412 
413             double ta = aj.Minus(ai).Dot(di) / di.Dot(di);
414             double tb = bj.Minus(ai).Dot(di) / di.Dot(di);
415             if(ta > tb) {
416                 std::swap(pAj, pBj);
417                 std::swap(ta, tb);
418             }
419 
420             hStyle hsj = { (uint32_t)sej->auxA };
421             Style *sj = Style::Get(hsj);
422 
423             bool canRemoveI = sej->auxA == sei->auxA || si->zIndex < sj->zIndex;
424             bool canRemoveJ = sej->auxA == sei->auxA || sj->zIndex < si->zIndex;
425 
426             if(canRemoveJ) {
427                 // j-segment inside i-segment
428                 if(ta > 0.0 - eps && tb < 1.0 + eps) {
429                     sej->tag = 1;
430                     continue;
431                 }
432 
433                 // cut segment
434                 bool aInside = ta > 0.0 - eps && ta < 1.0 + eps;
435                 if(tb > 1.0 - eps && aInside) {
436                     *pAj = sei->b;
437                     continue;
438                 }
439 
440                 // cut segment
441                 bool bInside = tb > 0.0 - eps && tb < 1.0 + eps;
442                 if(ta < 0.0 - eps && bInside) {
443                     *pBj = sei->a;
444                     continue;
445                 }
446 
447                 // split segment
448                 if(ta < 0.0 - eps && tb > 1.0 + eps) {
449                     sel->AddEdge(sei->b, *pBj, sej->auxA, sej->auxB);
450                     *pBj = sei->a;
451                     continue;
452                 }
453             }
454 
455             if(canRemoveI) {
456                 // j-segment inside i-segment
457                 if(ta < 0.0 + eps && tb > 1.0 - eps) {
458                     sei->tag = 1;
459                     break;
460                 }
461 
462                 // cut segment
463                 bool aInside = ta > 0.0 + eps && ta < 1.0 - eps;
464                 if(tb > 1.0 - eps && aInside) {
465                     sei->b = *pAj;
466                     i--;
467                     break;
468                 }
469 
470                 // cut segment
471                 bool bInside = tb > 0.0 + eps && tb < 1.0 - eps;
472                 if(ta < 0.0 + eps && bInside) {
473                     sei->a = *pBj;
474                     i--;
475                     break;
476                 }
477 
478                 // split segment
479                 if(ta > 0.0 + eps && tb < 1.0 - eps) {
480                     sel->AddEdge(*pBj, sei->b, sei->auxA, sei->auxB);
481                     sei->b = *pAj;
482                     i--;
483                     break;
484                 }
485             }
486         }
487     }
488     sel->l.RemoveTagged();
489 
490     // We kept the line segments and Beziers separate until now; but put them
491     // all together, and also project everything into the xy plane, since not
492     // all export targets ignore the z component of the points.
493     for(e = sel->l.First(); e; e = sel->l.NextAfter(e)) {
494         SBezier sb = SBezier::From(e->a, e->b);
495         sb.auxA = e->auxA;
496         sbl->l.Add(&sb);
497     }
498     for(b = sbl->l.First(); b; b = sbl->l.NextAfter(b)) {
499         for(int i = 0; i <= b->deg; i++) {
500             b->ctrl[i].z = 0;
501         }
502     }
503 
504     // If possible, then we will assemble these output curves into loops. They
505     // will then get exported as closed paths.
506     SBezierLoopSetSet sblss = {};
507     SBezierList leftovers = {};
508     SSurface srf = SSurface::FromPlane(Vector::From(0, 0, 0),
509                                        Vector::From(1, 0, 0),
510                                        Vector::From(0, 1, 0));
511     SPolygon spxyz = {};
512     bool allClosed;
513     SEdge notClosedAt;
514     sbl->l.ClearTags();
515     sblss.FindOuterFacesFrom(sbl, &spxyz, &srf,
516                              SS.ExportChordTolMm(),
517                              &allClosed, &notClosedAt,
518                              NULL, NULL,
519                              &leftovers);
520     for(b = leftovers.l.First(); b; b = leftovers.l.NextAfter(b)) {
521         sblss.AddOpenPath(b);
522     }
523 
524     // Now write the lines and triangles to the output file
525     out->OutputLinesAndMesh(&sblss, &sms);
526 
527     leftovers.Clear();
528     spxyz.Clear();
529     sblss.Clear();
530     smp.Clear();
531     sms.Clear();
532     hlrd.Clear();
533 }
534 
MmToPts(double mm)535 double VectorFileWriter::MmToPts(double mm) {
536     // 72 points in an inch
537     return (mm/25.4)*72;
538 }
539 
ForFile(const std::string & filename)540 VectorFileWriter *VectorFileWriter::ForFile(const std::string &filename) {
541     VectorFileWriter *ret;
542     bool needOpen = true;
543     if(FilenameHasExtension(filename, ".dxf")) {
544         static DxfFileWriter DxfWriter;
545         ret = &DxfWriter;
546         needOpen = false;
547     } else if(FilenameHasExtension(filename, ".ps") || FilenameHasExtension(filename, ".eps")) {
548         static EpsFileWriter EpsWriter;
549         ret = &EpsWriter;
550     } else if(FilenameHasExtension(filename, ".pdf")) {
551         static PdfFileWriter PdfWriter;
552         ret = &PdfWriter;
553     } else if(FilenameHasExtension(filename, ".svg")) {
554         static SvgFileWriter SvgWriter;
555         ret = &SvgWriter;
556     } else if(FilenameHasExtension(filename, ".plt")||FilenameHasExtension(filename, ".hpgl")) {
557         static HpglFileWriter HpglWriter;
558         ret = &HpglWriter;
559     } else if(FilenameHasExtension(filename, ".step")||FilenameHasExtension(filename, ".stp")) {
560         static Step2dFileWriter Step2dWriter;
561         ret = &Step2dWriter;
562     } else if(FilenameHasExtension(filename, ".txt")) {
563         static GCodeFileWriter GCodeWriter;
564         ret = &GCodeWriter;
565     } else {
566         Error("Can't identify output file type from file extension of "
567         "filename '%s'; try "
568         ".step, .stp, .dxf, .svg, .plt, .hpgl, .pdf, .txt, "
569         ".eps, or .ps.",
570             filename.c_str());
571         return NULL;
572     }
573     ret->filename = filename;
574     if(!needOpen) return ret;
575 
576     FILE *f = ssfopen(filename, "wb");
577     if(!f) {
578         Error("Couldn't write to '%s'", filename.c_str());
579         return NULL;
580     }
581     ret->f = f;
582     return ret;
583 }
584 
SetModelviewProjection(const Vector & u,const Vector & v,const Vector & n,const Vector & origin,double cameraTan,double scale)585 void VectorFileWriter::SetModelviewProjection(const Vector &u, const Vector &v, const Vector &n,
586                                               const Vector &origin, double cameraTan,
587                                               double scale) {
588     this->u = u;
589     this->v = v;
590     this->n = n;
591     this->origin = origin;
592     this->cameraTan = cameraTan;
593     this->scale = scale;
594 }
595 
Transform(Vector & pos) const596 Vector VectorFileWriter::Transform(Vector &pos) const {
597     return pos.InPerspective(u, v, n, origin, cameraTan).ScaledBy(1.0 / scale);
598 }
599 
OutputLinesAndMesh(SBezierLoopSetSet * sblss,SMesh * sm)600 void VectorFileWriter::OutputLinesAndMesh(SBezierLoopSetSet *sblss, SMesh *sm) {
601     STriangle *tr;
602     SBezier *b;
603 
604     // First calculate the bounding box.
605     ptMin = Vector::From(VERY_POSITIVE, VERY_POSITIVE, VERY_POSITIVE);
606     ptMax = Vector::From(VERY_NEGATIVE, VERY_NEGATIVE, VERY_NEGATIVE);
607     if(sm) {
608         for(tr = sm->l.First(); tr; tr = sm->l.NextAfter(tr)) {
609             (tr->a).MakeMaxMin(&ptMax, &ptMin);
610             (tr->b).MakeMaxMin(&ptMax, &ptMin);
611             (tr->c).MakeMaxMin(&ptMax, &ptMin);
612         }
613     }
614     if(sblss) {
615         SBezierLoopSet *sbls;
616         for(sbls = sblss->l.First(); sbls; sbls = sblss->l.NextAfter(sbls)) {
617             SBezierLoop *sbl;
618             for(sbl = sbls->l.First(); sbl; sbl = sbls->l.NextAfter(sbl)) {
619                 for(b = sbl->l.First(); b; b = sbl->l.NextAfter(b)) {
620                     for(int i = 0; i <= b->deg; i++) {
621                         (b->ctrl[i]).MakeMaxMin(&ptMax, &ptMin);
622                     }
623                 }
624             }
625         }
626     }
627 
628     // And now we compute the canvas size.
629     double s = 1.0 / SS.exportScale;
630     if(SS.exportCanvasSizeAuto) {
631         // It's based on the calculated bounding box; we grow it along each
632         // boundary by the specified amount.
633         ptMin.x -= s*SS.exportMargin.left;
634         ptMax.x += s*SS.exportMargin.right;
635         ptMin.y -= s*SS.exportMargin.bottom;
636         ptMax.y += s*SS.exportMargin.top;
637     } else {
638         ptMin.x = -(s*SS.exportCanvas.dx);
639         ptMin.y = -(s*SS.exportCanvas.dy);
640         ptMax.x = ptMin.x + (s*SS.exportCanvas.width);
641         ptMax.y = ptMin.y + (s*SS.exportCanvas.height);
642     }
643 
644     StartFile();
645     if(sm && SS.exportShadedTriangles) {
646         for(tr = sm->l.First(); tr; tr = sm->l.NextAfter(tr)) {
647             Triangle(tr);
648         }
649     }
650     if(sblss) {
651         SBezierLoopSet *sbls;
652         for(sbls = sblss->l.First(); sbls; sbls = sblss->l.NextAfter(sbls)) {
653             SBezierLoop *sbl;
654             sbl = sbls->l.First();
655             if(!sbl) continue;
656             b = sbl->l.First();
657             if(!b || !Style::Exportable(b->auxA)) continue;
658 
659             hStyle hs = { (uint32_t)b->auxA };
660             Style *stl = Style::Get(hs);
661             double lineWidth   = Style::WidthMm(b->auxA)*s;
662             RgbaColor strokeRgb = Style::Color(hs, true);
663             RgbaColor fillRgb   = Style::FillColor(hs, true);
664 
665             StartPath(strokeRgb, lineWidth, stl->filled, fillRgb, hs);
666             for(sbl = sbls->l.First(); sbl; sbl = sbls->l.NextAfter(sbl)) {
667                 for(b = sbl->l.First(); b; b = sbl->l.NextAfter(b)) {
668                     Bezier(b);
669                 }
670             }
671             FinishPath(strokeRgb, lineWidth, stl->filled, fillRgb, hs);
672         }
673     }
674     FinishAndCloseFile();
675 }
676 
BezierAsPwl(SBezier * sb)677 void VectorFileWriter::BezierAsPwl(SBezier *sb) {
678     List<Vector> lv = {};
679     sb->MakePwlInto(&lv, SS.ExportChordTolMm());
680     int i;
681     for(i = 1; i < lv.n; i++) {
682         SBezier sb = SBezier::From(lv.elem[i-1], lv.elem[i]);
683         Bezier(&sb);
684     }
685     lv.Clear();
686 }
687 
BezierAsNonrationalCubic(SBezier * sb,int depth)688 void VectorFileWriter::BezierAsNonrationalCubic(SBezier *sb, int depth) {
689     Vector t0 = sb->TangentAt(0), t1 = sb->TangentAt(1);
690     // The curve is correct, and the first derivatives are correct, at the
691     // endpoints.
692     SBezier bnr = SBezier::From(
693                         sb->Start(),
694                         sb->Start().Plus(t0.ScaledBy(1.0/3)),
695                         sb->Finish().Minus(t1.ScaledBy(1.0/3)),
696                         sb->Finish());
697 
698     double tol = SS.ExportChordTolMm();
699     // Arbitrary choice, but make it a little finer than pwl tolerance since
700     // it should be easier to achieve that with the smooth curves.
701     tol /= 2;
702 
703     bool closeEnough = true;
704     int i;
705     for(i = 1; i <= 3; i++) {
706         double t = i/4.0;
707         Vector p0 = sb->PointAt(t),
708                pn = bnr.PointAt(t);
709         double d = (p0.Minus(pn)).Magnitude();
710         if(d > tol) {
711             closeEnough = false;
712         }
713     }
714 
715     if(closeEnough || depth > 3) {
716         Bezier(&bnr);
717     } else {
718         SBezier bef, aft;
719         sb->SplitAt(0.5, &bef, &aft);
720         BezierAsNonrationalCubic(&bef, depth+1);
721         BezierAsNonrationalCubic(&aft, depth+1);
722     }
723 }
724 
725 //-----------------------------------------------------------------------------
726 // Export a triangle mesh, in the requested format.
727 //-----------------------------------------------------------------------------
ExportMeshTo(const std::string & filename)728 void SolveSpaceUI::ExportMeshTo(const std::string &filename) {
729     SS.exportMode = true;
730     GenerateAll(GENERATE_ALL);
731 
732     Group *g = SK.GetGroup(SS.GW.activeGroup);
733     g->GenerateDisplayItems();
734 
735     SMesh *m = &(SK.GetGroup(SS.GW.activeGroup)->displayMesh);
736     if(m->IsEmpty()) {
737         Error("Active group mesh is empty; nothing to export.");
738         return;
739     }
740 
741     FILE *f = ssfopen(filename, "wb");
742     if(!f) {
743         Error("Couldn't write to '%s'", filename.c_str());
744         return;
745     }
746 
747     if(FilenameHasExtension(filename, ".stl")) {
748         ExportMeshAsStlTo(f, m);
749     } else if(FilenameHasExtension(filename, ".obj")) {
750         ExportMeshAsObjTo(f, m);
751     } else if(FilenameHasExtension(filename, ".js") ||
752               FilenameHasExtension(filename, ".html")) {
753         SEdgeList *e = &(SK.GetGroup(SS.GW.activeGroup)->displayEdges);
754         ExportMeshAsThreeJsTo(f, filename, m, e);
755     } else {
756         Error("Can't identify output file type from file extension of "
757               "filename '%s'; try .stl, .obj, .js.", filename.c_str());
758     }
759 
760     fclose(f);
761 
762     SS.justExportedInfo.showOrigin = false;
763     SS.justExportedInfo.draw = true;
764     InvalidateGraphics();
765 }
766 
767 //-----------------------------------------------------------------------------
768 // Export the mesh as an STL file; it should always be vertex-to-vertex and
769 // not self-intersecting, so not much to do.
770 //-----------------------------------------------------------------------------
ExportMeshAsStlTo(FILE * f,SMesh * sm)771 void SolveSpaceUI::ExportMeshAsStlTo(FILE *f, SMesh *sm) {
772     char str[80] = {};
773     strcpy(str, "STL exported mesh");
774     fwrite(str, 1, 80, f);
775 
776     uint32_t n = sm->l.n;
777     fwrite(&n, 4, 1, f);
778 
779     double s = SS.exportScale;
780     int i;
781     for(i = 0; i < sm->l.n; i++) {
782         STriangle *tr = &(sm->l.elem[i]);
783         Vector n = tr->Normal().WithMagnitude(1);
784         float w;
785         w = (float)n.x;           fwrite(&w, 4, 1, f);
786         w = (float)n.y;           fwrite(&w, 4, 1, f);
787         w = (float)n.z;           fwrite(&w, 4, 1, f);
788         w = (float)((tr->a.x)/s); fwrite(&w, 4, 1, f);
789         w = (float)((tr->a.y)/s); fwrite(&w, 4, 1, f);
790         w = (float)((tr->a.z)/s); fwrite(&w, 4, 1, f);
791         w = (float)((tr->b.x)/s); fwrite(&w, 4, 1, f);
792         w = (float)((tr->b.y)/s); fwrite(&w, 4, 1, f);
793         w = (float)((tr->b.z)/s); fwrite(&w, 4, 1, f);
794         w = (float)((tr->c.x)/s); fwrite(&w, 4, 1, f);
795         w = (float)((tr->c.y)/s); fwrite(&w, 4, 1, f);
796         w = (float)((tr->c.z)/s); fwrite(&w, 4, 1, f);
797         fputc(0, f);
798         fputc(0, f);
799     }
800 }
801 
802 //-----------------------------------------------------------------------------
803 // Export the mesh as Wavefront OBJ format. This requires us to reduce all the
804 // identical vertices to the same identifier, so do that first.
805 //-----------------------------------------------------------------------------
ExportMeshAsObjTo(FILE * f,SMesh * sm)806 void SolveSpaceUI::ExportMeshAsObjTo(FILE *f, SMesh *sm) {
807     SPointList spl = {};
808     STriangle *tr;
809     for(tr = sm->l.First(); tr; tr = sm->l.NextAfter(tr)) {
810         spl.IncrementTagFor(tr->a);
811         spl.IncrementTagFor(tr->b);
812         spl.IncrementTagFor(tr->c);
813     }
814 
815     // Output all the vertices.
816     SPoint *sp;
817     for(sp = spl.l.First(); sp; sp = spl.l.NextAfter(sp)) {
818         fprintf(f, "v %.10f %.10f %.10f\r\n",
819                         sp->p.x / SS.exportScale,
820                         sp->p.y / SS.exportScale,
821                         sp->p.z / SS.exportScale);
822     }
823 
824     // And now all the triangular faces, in terms of those vertices. The
825     // file format counts from 1, not 0.
826     for(tr = sm->l.First(); tr; tr = sm->l.NextAfter(tr)) {
827         fprintf(f, "f %d %d %d\r\n",
828                         spl.IndexForPoint(tr->a) + 1,
829                         spl.IndexForPoint(tr->b) + 1,
830                         spl.IndexForPoint(tr->c) + 1);
831     }
832 
833     spl.Clear();
834 }
835 
836 //-----------------------------------------------------------------------------
837 // Export the mesh as a JavaScript script, which is compatible with Three.js.
838 //-----------------------------------------------------------------------------
ExportMeshAsThreeJsTo(FILE * f,const std::string & filename,SMesh * sm,SEdgeList * sel)839 void SolveSpaceUI::ExportMeshAsThreeJsTo(FILE *f, const std::string &filename,
840                                          SMesh *sm, SEdgeList *sel)
841 {
842     SPointList spl = {};
843     STriangle *tr;
844     SEdge *e;
845     Vector bndl, bndh;
846     const char htmlbegin0[] = R"(
847 <!DOCTYPE html>
848 <html lang="en">
849   <head>
850     <meta charset="utf-8"></meta>
851     <title>Three.js Solvespace Mesh</title>
852     <script src="http://threejs.org/build/three.js"></script>
853     <script src="http://hammerjs.github.io/dist/hammer.js"></script>
854     <style type="text/css">
855     body { margin: 0; overflow: hidden; }
856     </style>
857   </head>
858   <body>
859     <script>
860     </script>
861     <script>
862 window.devicePixelRatio = window.devicePixelRatio || 1;
863 
864 SolvespaceCamera = function(renderWidth, renderHeight, scale, up, right, offset) {
865         THREE.Camera.call(this);
866 
867         this.type = 'SolvespaceCamera';
868 
869         this.renderWidth = renderWidth;
870         this.renderHeight = renderHeight;
871         this.zoomScale = scale; /* Avoid namespace collision w/ THREE.Object.scale */
872         this.up = up;
873         this.right = right;
874         this.offset = offset;
875         this.depthBias = 0;
876 
877         this.updateProjectionMatrix();
878     };
879 
880     SolvespaceCamera.prototype = Object.create(THREE.Camera.prototype);
881     SolvespaceCamera.prototype.constructor = SolvespaceCamera;
882     SolvespaceCamera.prototype.updateProjectionMatrix = function() {
883         var temp = new THREE.Matrix4();
884         var offset = new THREE.Matrix4().makeTranslation(this.offset.x, this.offset.y, this.offset.z);
885         // Convert to right handed- do up cross right instead.
886         var n = new THREE.Vector3().crossVectors(this.up, this.right);
887         var rotate = new THREE.Matrix4().makeBasis(this.right, this.up, n);
888         rotate.transpose();
889         /* FIXME: At some point we ended up using row-major.
890            THREE.js wants column major. Scale/depth correct unaffected b/c diagonal
891            matrices remain the same when transposed. makeTranslation also makes
892            a column-major matrix. */
893 
894         /* TODO: If we want perspective, we need an additional matrix
895            here which will modify w for perspective divide. */
896         var scale = new THREE.Matrix4().makeScale(2 * this.zoomScale / this.renderWidth,
897             2 * this.zoomScale / this.renderHeight, this.zoomScale / 30000.0);
898 
899         temp.multiply(scale);
900         temp.multiply(rotate);
901         temp.multiply(offset);
902 
903         this.projectionMatrix.copy(temp);
904     };
905 
906     SolvespaceCamera.prototype.NormalizeProjectionVectors = function() {
907         /* After rotating, up and right may no longer be orthogonal.
908         However, their cross product will produce the correct
909         rotated plane, and we can recover an orthogonal basis. */
910         var n = new THREE.Vector3().crossVectors(this.right, this.up);
911         this.up = new THREE.Vector3().crossVectors(n, this.right);
912         this.right.normalize();
913         this.up.normalize();
914     };
915 
916     SolvespaceCamera.prototype.rotate = function(right, up) {
917         var oldRight = new THREE.Vector3().copy(this.right).normalize();
918         var oldUp = new THREE.Vector3().copy(this.up).normalize();
919         this.up.applyAxisAngle(oldRight, up);
920         this.right.applyAxisAngle(oldUp, right);
921         this.NormalizeProjectionVectors();
922     }
923 
924     SolvespaceCamera.prototype.offsetProj = function(right, up) {
925         var shift = new THREE.Vector3(right * this.right.x + up * this.up.x,
926             right * this.right.y + up * this.up.y,
927             right * this.right.z + up * this.up.z);
928         this.offset.add(shift);
929     }
930 
931     /* Calculate the offset in terms of up and right projection vectors
932     that will preserve the world coordinates of the current mouse position after
933     the zoom. */
934     SolvespaceCamera.prototype.zoomTo = function(x, y, delta) {
935         // Get offset components in world coordinates, in terms of up/right.
936         var projOffsetX = this.offset.dot(this.right);
937         var projOffsetY = this.offset.dot(this.up);
938 
939         /* Remove offset before scaling so, that mouse position changes
940         proportionally to the model and independent of current offset. */
941         var centerRightI = x/this.zoomScale - projOffsetX;
942         var centerUpI = y/this.zoomScale - projOffsetY;
943         var zoomFactor;
944 
945         /* Zoom 20% every 100 delta. */
946         if(delta < 0) {
947             zoomFactor = (-delta * 0.002 + 1);
948         }
949         else if(delta > 0) {
950             zoomFactor = (delta * (-1.0/600.0) + 1)
951         }
952         else {
953             return;
954         }
955 
956         this.zoomScale = this.zoomScale * zoomFactor;
957         var centerRightF = x/this.zoomScale - projOffsetX;
958         var centerUpF = y/this.zoomScale - projOffsetY;
959 
960         this.offset.addScaledVector(this.right, centerRightF - centerRightI);
961         this.offset.addScaledVector(this.up, centerUpF - centerUpI);
962     }
963 
964 
965     SolvespaceControls = function(object, domElement) {
966         var _this = this;
967         this.object = object;
968         this.domElement = ( domElement !== undefined ) ? domElement : document;
969 
970         var threePan = new Hammer.Pan({event : 'threepan', pointers : 3, enable : false});
971         var panAfterTap = new Hammer.Pan({event : 'panaftertap', enable : false});
972 
973         this.touchControls = new Hammer.Manager(domElement, {
974             recognizers: [
975                 [Hammer.Pinch, { enable: true }],
976                 [Hammer.Pan],
977                 [Hammer.Tap],
978             ]
979         });
980 
981         this.touchControls.add(threePan);
982         this.touchControls.add(panAfterTap);
983 
984         var changeEvent = {
985             type: 'change'
986         };
987         var startEvent = {
988             type: 'start'
989         };
990         var endEvent = {
991             type: 'end'
992         };
993 
994         var _changed = false;
995         var _mouseMoved = false;
996         //var _touchPoints = new Array();
997         var _offsetPrev = new THREE.Vector2(0, 0);
998         var _offsetCur = new THREE.Vector2(0, 0);
999         var _rotatePrev = new THREE.Vector2(0, 0);
1000         var _rotateCur = new THREE.Vector2(0, 0);
1001 
1002         // Used during touch events.
1003         var _rotateOrig = new THREE.Vector2(0, 0);
1004         var _offsetOrig = new THREE.Vector2(0, 0);
1005         var _prevScale = 1.0;
1006 
1007         this.handleEvent = function(event) {
1008             if (typeof this[event.type] == 'function') {
1009                 this[event.type](event);
1010             }
1011         }
1012 
1013         function mousedown(event) {
1014             event.preventDefault();
1015             event.stopPropagation();
1016 
1017             switch (event.button) {
1018                 case 0:
1019                     _rotateCur.set(event.screenX/window.devicePixelRatio, event.screenY/window.devicePixelRatio);
1020                     _rotatePrev.copy(_rotateCur);
1021                     document.addEventListener('mousemove', mousemove, false);
1022                     document.addEventListener('mouseup', mouseup, false);
1023                     break;
1024                 case 2:
1025                     _offsetCur.set(event.screenX/window.devicePixelRatio, event.screenY/window.devicePixelRatio);
1026                     _offsetPrev.copy(_offsetCur);
1027                     document.addEventListener('mousemove', mousemove, false);
1028                     document.addEventListener('mouseup', mouseup, false);
1029                     break;
1030                 default:
1031                     break;
1032             }
1033         }
1034 
1035         function wheel( event ) {
1036             event.preventDefault();
1037             /* FIXME: Width and height might not be supported universally, but
1038             can be calculated? */
1039             var box = _this.domElement.getBoundingClientRect();
1040             object.zoomTo(event.clientX - box.width/2 - box.left,
1041                  -(event.clientY - box.height/2 - box.top), event.deltaY);
1042             _changed = true;
1043         }
1044 
1045         function mousemove(event) {
1046             switch (event.button) {
1047                 case 0:
1048                     _rotateCur.set(event.screenX/window.devicePixelRatio, event.screenY/window.devicePixelRatio);
1049                     var diff = new THREE.Vector2().subVectors(_rotateCur, _rotatePrev)
1050                         .multiplyScalar(1 / object.zoomScale);
1051                     object.rotate(-0.3 * Math.PI / 180 * diff.x * object.zoomScale,
1052                          -0.3 * Math.PI / 180 * diff.y * object.zoomScale);
1053                     _changed = true;
1054                     _rotatePrev.copy(_rotateCur);
1055                     break;
1056                 case 2:
1057                     _mouseMoved = true;
1058                     _offsetCur.set(event.screenX/window.devicePixelRatio, event.screenY/window.devicePixelRatio);
1059                     var diff = new THREE.Vector2().subVectors(_offsetCur, _offsetPrev)
1060                         .multiplyScalar(1 / object.zoomScale);
1061                     object.offsetProj(diff.x, -diff.y);
1062                     _changed = true;
1063                     _offsetPrev.copy(_offsetCur)
1064                     break;
1065             }
1066         }
1067 
1068 
1069         function mouseup(event) {
1070             /* TODO: Opera mouse gestures will intercept this event, making it
1071             possible to have multiple mousedown events consecutively without
1072             a corresponding mouseup (so multiple viewports can be rotated/panned
1073             simultaneously). Disable mouse gestures for now. */
1074             event.preventDefault();
1075             event.stopPropagation();
1076 
1077             document.removeEventListener('mousemove', mousemove);
1078             document.removeEventListener('mouseup', mouseup);
1079 
1080             _this.dispatchEvent(endEvent);
1081         }
1082 
1083         function pan(event) {
1084             /* neWcur - prev does not necessarily equal (cur + diff) - prev.
1085             Floating point is not associative. */
1086             touchDiff = new THREE.Vector2(event.deltaX, event.deltaY);
1087             _rotateCur.addVectors(_rotateOrig, touchDiff);
1088             incDiff = new THREE.Vector2().subVectors(_rotateCur, _rotatePrev)
1089                 .multiplyScalar(1 / object.zoomScale);
1090             object.rotate(-0.3 * Math.PI / 180 * incDiff.x * object.zoomScale,
1091                  -0.3 * Math.PI / 180 * incDiff.y * object.zoomScale);
1092             _changed = true;
1093             _rotatePrev.copy(_rotateCur);
1094         }
1095 
1096         function panstart(event) {
1097             /* TODO: Dynamically enable pan function? */
1098             _rotateOrig.copy(_rotateCur);
1099         }
1100 
1101         function pinchstart(event) {
1102             _prevScale = event.scale;
1103         }
1104 
1105         function pinch(event) {
1106             /* FIXME: Width and height might not be supported universally, but
1107             can be calculated? */
1108             var box = _this.domElement.getBoundingClientRect();
1109 
1110             /* 16.6... pixels chosen heuristically... matches my touchpad. */
1111             if (event.scale < _prevScale) {
1112                 object.zoomTo(event.center.x - box.width/2 - box.left,
1113                      -(event.center.y - box.height/2 - box.top), 100/6.0);
1114                 _changed = true;
1115             } else if (event.scale > _prevScale) {
1116                 object.zoomTo(event.center.x - box.width/2 - box.left,
1117                      -(event.center.y - box.height/2 - box.top), -100/6.0);
1118                 _changed = true;
1119             }
1120 
1121             _prevScale = event.scale;
1122         }
1123 
1124         /* A tap will enable panning/disable rotate. */
1125         function tap(event) {
1126             panAfterTap.set({enable : true});
1127             _this.touchControls.get('pan').set({enable : false});
1128         }
1129 
1130         function panaftertap(event) {
1131             touchDiff = new THREE.Vector2(event.deltaX, event.deltaY);
1132             _offsetCur.addVectors(_offsetOrig, touchDiff);
1133             incDiff = new THREE.Vector2().subVectors(_offsetCur, _offsetPrev)
1134                 .multiplyScalar(1 / object.zoomScale);
1135             object.offsetProj(incDiff.x, -incDiff.y);
1136             _changed = true;
1137             _offsetPrev.copy(_offsetCur);
1138         }
1139 
1140         function panaftertapstart(event) {
1141             _offsetOrig.copy(_offsetCur);
1142         }
1143 
1144         function panaftertapend(event) {
1145             panAfterTap.set({enable : false});
1146             _this.touchControls.get('pan').set({enable : true});
1147         }
1148 
1149         function contextmenu(event) {
1150             event.preventDefault();
1151         }
1152 
1153         this.update = function() {
1154             if (_changed) {
1155                 _this.dispatchEvent(changeEvent);
1156                 _changed = false;
1157             }
1158         }
1159 
1160         this.domElement.addEventListener('mousedown', mousedown, false);
1161         this.domElement.addEventListener('wheel', wheel, false);
1162         this.domElement.addEventListener('contextmenu', contextmenu, false);
1163 
1164         /* Hammer.on wraps addEventListener */
1165         // Rotate
1166         this.touchControls.on('pan', pan);
1167         this.touchControls.on('panstart', panstart);
1168 
1169         // Zoom
1170         this.touchControls.on('pinch', pinch);
1171         this.touchControls.on('pinchstart', pinchstart);
1172 
1173         //Pan
1174         this.touchControls.on('tap', tap);
1175         this.touchControls.on('panaftertapstart', panaftertapstart);
1176         this.touchControls.on('panaftertap', panaftertap);
1177         this.touchControls.on('panaftertapend', panaftertapend);
1178     }
1179 
1180     SolvespaceControls.prototype = Object.create(THREE.EventDispatcher.prototype);
1181     SolvespaceControls.prototype.constructor = SolvespaceControls;
1182 
1183 
1184     solvespace = function(obj, params) {
1185         var scene, edgeScene, camera, edgeCamera, renderer;
1186         var geometry, controls, material, mesh, edges;
1187         var width, height;
1188         var directionalLightArray = [];
1189 
1190         if (typeof params === "undefined" || !("width" in params)) {
1191             width = window.innerWidth;
1192         } else {
1193             width = params.width;
1194         }
1195 
1196         if (typeof params === "undefined" || !("height" in params)) {
1197             height = window.innerHeight;
1198         } else {
1199             height = params.height;
1200         }
1201 
1202         width *= window.devicePixelRatio;
1203         height *= window.devicePixelRatio;
1204 
1205         domElement = init();
1206         render();
1207         return domElement;
1208 
1209 
1210         function init() {
1211             scene = new THREE.Scene();
1212             edgeScene = new THREE.Scene();
1213 
1214             camera = new SolvespaceCamera(width/window.devicePixelRatio,
1215                 height/window.devicePixelRatio, 5, new THREE.Vector3(0, 1, 0),
1216                 new THREE.Vector3(1, 0, 0), new THREE.Vector3(0, 0, 0));
1217 
1218             mesh = createMesh(obj);
1219             scene.add(mesh);
1220             edges = createEdges(obj);
1221             edgeScene.add(edges);
1222 
1223             for (var i = 0; i < obj.lights.d.length; i++) {
1224                 var lightColor = new THREE.Color(obj.lights.d[i].intensity,
1225                     obj.lights.d[i].intensity, obj.lights.d[i].intensity);
1226                 var directionalLight = new THREE.DirectionalLight(lightColor, 1);
1227                 directionalLight.position.set(obj.lights.d[i].direction[0],
1228                     obj.lights.d[i].direction[1], obj.lights.d[i].direction[2]);
1229                 directionalLightArray.push(directionalLight);
1230                 scene.add(directionalLight);
1231             }
1232 
1233             var lightColor = new THREE.Color(obj.lights.a, obj.lights.a, obj.lights.a);
1234             var ambientLight = new THREE.AmbientLight(lightColor.getHex());
1235             scene.add(ambientLight);
1236 
1237             renderer = new THREE.WebGLRenderer({ antialias: true});
1238             renderer.setSize(width, height);
1239             renderer.autoClear = false;
1240             renderer.domElement.style = "width:"+width/window.devicePixelRatio+"px;height:"+height/window.devicePixelRatio+"px;";
1241 
1242             controls = new SolvespaceControls(camera, renderer.domElement);
1243             controls.addEventListener("change", render);
1244             controls.addEventListener("change", lightUpdate);
1245 
1246             animate();
1247             return renderer.domElement;
1248         })";
1249     const char htmlbegin1[] = R"(
1250         function animate() {
1251             requestAnimationFrame(animate);
1252             controls.update();
1253         }
1254 
1255         function render() {
1256             var context = renderer.getContext();
1257             camera.updateProjectionMatrix();
1258             renderer.clear();
1259 
1260             context.depthRange(0.1, 1);
1261             renderer.render(scene, camera);
1262 
1263             context.depthRange(0.1-(2/60000.0), 1-(2/60000.0));
1264             renderer.render(edgeScene, camera);
1265         }
1266 
1267         function lightUpdate() {
1268             var changeBasis = new THREE.Matrix4();
1269 
1270             // The original light positions were in camera space.
1271             // Project them into standard space using camera's basis
1272             // vectors (up, target, and their cross product).
1273             n = new THREE.Vector3().crossVectors(camera.up, camera.right);
1274             changeBasis.makeBasis(camera.right, camera.up, n);
1275 
1276             for (var i = 0; i < 2; i++) {
1277                 var newLightPos = changeBasis.applyToVector3Array(
1278                     [obj.lights.d[i].direction[0], obj.lights.d[i].direction[1],
1279                         obj.lights.d[i].direction[2]]);
1280                 directionalLightArray[i].position.set(newLightPos[0],
1281                     newLightPos[1], newLightPos[2]);
1282             }
1283         }
1284 
1285         function createMesh(meshObj) {
1286             var geometry = new THREE.Geometry();
1287             var materialIndex = 0;
1288             var materialList = [];
1289             var opacitiesSeen = {};
1290 
1291             for (var i = 0; i < meshObj.points.length; i++) {
1292                 geometry.vertices.push(new THREE.Vector3(meshObj.points[i][0],
1293                     meshObj.points[i][1], meshObj.points[i][2]));
1294             }
1295 
1296             for (var i = 0; i < meshObj.faces.length; i++) {
1297                 var currOpacity = ((meshObj.colors[i] & 0xFF000000) >>> 24) / 255.0;
1298                 if (opacitiesSeen[currOpacity] === undefined) {
1299                     opacitiesSeen[currOpacity] = materialIndex;
1300                     materialIndex++;
1301                     materialList.push(new THREE.MeshLambertMaterial({
1302                         vertexColors: THREE.FaceColors,
1303                         opacity: currOpacity,
1304                         transparent: true,
1305                         side: THREE.DoubleSide
1306                     }));
1307                 }
1308 
1309                 geometry.faces.push(new THREE.Face3(meshObj.faces[i][0],
1310                     meshObj.faces[i][1], meshObj.faces[i][2],
1311                     [new THREE.Vector3(meshObj.normals[i][0][0],
1312                         meshObj.normals[i][0][1], meshObj.normals[i][0][2]),
1313                      new THREE.Vector3(meshObj.normals[i][1][0],
1314                         meshObj.normals[i][1][1], meshObj.normals[i][1][2]),
1315                      new THREE.Vector3(meshObj.normals[i][2][0],
1316                         meshObj.normals[i][2][1], meshObj.normals[i][2][2])],
1317                     new THREE.Color(meshObj.colors[i] & 0x00FFFFFF),
1318                     opacitiesSeen[currOpacity]));
1319             }
1320 
1321             geometry.computeBoundingSphere();
1322             return new THREE.Mesh(geometry, new THREE.MultiMaterial(materialList));
1323         }
1324 
1325         function createEdges(meshObj) {
1326             var geometry = new THREE.Geometry();
1327             var material = new THREE.LineBasicMaterial();
1328 
1329             for (var i = 0; i < meshObj.edges.length; i++) {
1330                 geometry.vertices.push(new THREE.Vector3(meshObj.edges[i][0][0],
1331                         meshObj.edges[i][0][1], meshObj.edges[i][0][2]),
1332                     new THREE.Vector3(meshObj.edges[i][1][0],
1333                         meshObj.edges[i][1][1], meshObj.edges[i][1][2]));
1334             }
1335 
1336             geometry.computeBoundingSphere();
1337             return new THREE.LineSegments(geometry, material);
1338         }
1339     };
1340 )";
1341     const char htmlend[] = R"(
1342     document.body.appendChild(solvespace(solvespace_model_%s));
1343     </script>
1344   </body>
1345 </html>
1346 )";
1347 
1348     // A default three.js viewer with OrthographicTrackballControls is
1349     // generated as a comment preceding the data.
1350 
1351     // x bounds should be the range of x or y, whichever
1352     // is larger, before aspect ratio correction is applied.
1353     // y bounds should be the range of x or y, whichever is
1354     // larger. No aspect ratio correction is applied.
1355     // Near plane should be 1.
1356     // Camera's z-position should be the range of z + 1 or the larger of
1357     // the x or y bounds, whichever is larger.
1358     // Far plane should be at least twice as much as the camera's
1359     // z-position.
1360     // Edge projection bias should be about 1/500 of the far plane's distance.
1361     // Further corrections will be applied to the z-position and far plane in
1362     // the default viewer, but the defaults are fine for a model which
1363     // only rotates about the world origin.
1364 
1365     sm->GetBounding(&bndh, &bndl);
1366     double largerBoundXY = max((bndh.x - bndl.x), (bndh.y - bndl.y));
1367     double largerBoundZ = max(largerBoundXY, (bndh.z - bndl.z + 1));
1368 
1369     std::string extension = filename,
1370                 noExtFilename = filename;
1371     size_t dot = noExtFilename.rfind('.');
1372     extension.erase(0, dot + 1);
1373     noExtFilename.erase(dot);
1374 
1375     std::string baseFilename = noExtFilename;
1376     size_t lastSlash = baseFilename.rfind(PATH_SEP);
1377     if(lastSlash == std::string::npos) oops();
1378     baseFilename.erase(0, lastSlash + 1);
1379 
1380     for(size_t i = 0; i < baseFilename.length(); i++) {
1381         if(!isalpha(baseFilename[i]) &&
1382            /* also permit UTF-8 */ !((unsigned char)baseFilename[i] >= 0x80))
1383             baseFilename[i] = '_';
1384     }
1385 
1386     if(extension == "html") {
1387         fputs(htmlbegin0, f);
1388         fputs(htmlbegin1, f);
1389     }
1390 
1391     fprintf(f, "var solvespace_model_%s = {\n"
1392                "  bounds: {\n"
1393                "    x: %f, y: %f, near: %f, far: %f, z: %f, edgeBias: %f\n"
1394                "  },\n",
1395             baseFilename.c_str(),
1396             largerBoundXY,
1397             largerBoundXY,
1398             1.0,
1399             largerBoundZ * 2,
1400             largerBoundZ,
1401             largerBoundZ / 250);
1402 
1403     // Output lighting information.
1404     fputs("  lights: {\n"
1405           "    d: [\n", f);
1406 
1407     // Directional.
1408     int lightCount;
1409     for(lightCount = 0; lightCount < 2; lightCount++)
1410     {
1411         fprintf(f, "      {\n"
1412                    "        intensity: %f, direction: [%f, %f, %f]\n"
1413                    "      },\n",
1414                 SS.lightIntensity[lightCount],
1415                 CO(SS.lightDir[lightCount]));
1416     }
1417 
1418     // Global Ambience.
1419     fprintf(f, "    ],\n"
1420                "    a: %f\n", SS.ambientIntensity);
1421 
1422     for(tr = sm->l.First(); tr; tr = sm->l.NextAfter(tr)) {
1423         spl.IncrementTagFor(tr->a);
1424         spl.IncrementTagFor(tr->b);
1425         spl.IncrementTagFor(tr->c);
1426     }
1427 
1428     // Output all the vertices.
1429     SPoint *sp;
1430     fputs("  },\n"
1431           "  points: [\n", f);
1432     for(sp = spl.l.First(); sp; sp = spl.l.NextAfter(sp)) {
1433         fprintf(f, "    [%f, %f, %f],\n",
1434                 sp->p.x / SS.exportScale,
1435                 sp->p.y / SS.exportScale,
1436                 sp->p.z / SS.exportScale);
1437     }
1438 
1439     fputs("  ],\n"
1440           "  faces: [\n", f);
1441     // And now all the triangular faces, in terms of those vertices.
1442     // This time we count from zero.
1443     for(tr = sm->l.First(); tr; tr = sm->l.NextAfter(tr)) {
1444         fprintf(f, "    [%d, %d, %d],\n",
1445                 spl.IndexForPoint(tr->a),
1446                 spl.IndexForPoint(tr->b),
1447                 spl.IndexForPoint(tr->c));
1448     }
1449 
1450     // Output face normals.
1451     fputs("  ],\n"
1452           "  normals: [\n", f);
1453     for(tr = sm->l.First(); tr; tr = sm->l.NextAfter(tr)) {
1454         fprintf(f, "    [[%f, %f, %f], [%f, %f, %f], [%f, %f, %f]],\n",
1455                 CO(tr->an), CO(tr->bn), CO(tr->cn));
1456     }
1457 
1458     fputs("  ],\n"
1459           "  colors: [\n", f);
1460     // Output triangle colors.
1461     for(tr = sm->l.First(); tr; tr = sm->l.NextAfter(tr)) {
1462         fprintf(f, "    0x%x,\n", tr->meta.color.ToARGB32());
1463     }
1464 
1465     fputs("  ],\n"
1466           "  edges: [\n", f);
1467     // Output edges. Assume user's model colors do not obscure white edges.
1468     for(e = sel->l.First(); e; e = sel->l.NextAfter(e)) {
1469         fprintf(f, "    [[%f, %f, %f], [%f, %f, %f]],\n",
1470                 e->a.x / SS.exportScale,
1471                 e->a.y / SS.exportScale,
1472                 e->a.z / SS.exportScale,
1473                 e->b.x / SS.exportScale,
1474                 e->b.y / SS.exportScale,
1475                 e->b.z / SS.exportScale);
1476     }
1477 
1478     fputs("  ]\n};\n", f);
1479 
1480     if(extension == "html")
1481         fprintf(f, htmlend, baseFilename.c_str());
1482 
1483     spl.Clear();
1484 }
1485 
1486 //-----------------------------------------------------------------------------
1487 // Export a view of the model as an image; we just take a screenshot, by
1488 // rendering the view in the usual way and then copying the pixels.
1489 //-----------------------------------------------------------------------------
1490 void SolveSpaceUI::ExportAsPngTo(const std::string &filename) {
1491     int w = (int)SS.GW.width, h = (int)SS.GW.height;
1492     // No guarantee that the back buffer contains anything valid right now,
1493     // so repaint the scene. And hide the toolbar too.
1494     bool prevShowToolbar = SS.showToolbar;
1495     SS.showToolbar = false;
1496 #ifndef WIN32
1497     std::unique_ptr<GLOffscreen> gloffscreen(new GLOffscreen);
1498     gloffscreen->begin(w, h);
1499 #endif
1500     SS.GW.Paint();
1501     SS.showToolbar = prevShowToolbar;
1502 
1503     FILE *f = ssfopen(filename, "wb");
1504     if(!f) goto err;
1505 
1506     png_struct *png_ptr; png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING,
1507         NULL, NULL, NULL);
1508     if(!png_ptr) goto err;
1509 
1510     png_info *info_ptr; info_ptr = png_create_info_struct(png_ptr);
1511     if(!png_ptr) goto err;
1512 
1513     if(setjmp(png_jmpbuf(png_ptr))) goto err;
1514 
1515     png_init_io(png_ptr, f);
1516 
1517     // glReadPixels wants to align things on 4-boundaries, and there's 3
1518     // bytes per pixel. As long as the row width is divisible by 4, all
1519     // works out.
1520     w &= ~3; h &= ~3;
1521 
1522     png_set_IHDR(png_ptr, info_ptr, w, h,
1523         8, PNG_COLOR_TYPE_RGB, PNG_INTERLACE_NONE,
1524         PNG_COMPRESSION_TYPE_DEFAULT,PNG_FILTER_TYPE_DEFAULT);
1525 
1526     png_write_info(png_ptr, info_ptr);
1527 
1528     // Get the pixel data from the framebuffer
1529     uint8_t *pixels; pixels = (uint8_t *)AllocTemporary(3*w*h);
1530     uint8_t **rowptrs; rowptrs = (uint8_t **)AllocTemporary(h*sizeof(uint8_t *));
1531     glReadPixels(0, 0, w, h, GL_RGB, GL_UNSIGNED_BYTE, pixels);
1532 
1533     int y;
1534     for(y = 0; y < h; y++) {
1535         // gl puts the origin at lower left, but png puts it top left
1536         rowptrs[y] = pixels + ((h - 1) - y)*(3*w);
1537     }
1538     png_write_image(png_ptr, rowptrs);
1539 
1540     png_write_end(png_ptr, info_ptr);
1541     png_destroy_write_struct(&png_ptr, &info_ptr);
1542     fclose(f);
1543     return;
1544 
1545 err:
1546     Error("Error writing PNG file '%s'", filename.c_str());
1547     if(f) fclose(f);
1548     return;
1549 }
1550 
1551