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, ¬ClosedAt,
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