Solvespace
1268 строк · 43.8 Кб
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#include "config.h"
11
12void SolveSpaceUI::ExportSectionTo(const Platform::Path &filename) {
13Vector gn = (SS.GW.projRight).Cross(SS.GW.projUp);
14gn = gn.WithMagnitude(1);
15
16Group *g = SK.GetGroup(SS.GW.activeGroup);
17g->GenerateDisplayItems();
18if(g->displayMesh.IsEmpty()) {
19Error(_("No solid model present; draw one with extrudes and revolves, "
20"or use Export 2d View to export bare lines and curves."));
21return;
22}
23
24// The plane in which the exported section lies; need this because we'll
25// reorient from that plane into the xy plane before exporting.
26Vector origin, u, v, n;
27double d;
28
29SS.GW.GroupSelection();
30auto const &gs = SS.GW.gs;
31if((gs.n == 0 && g->activeWorkplane != Entity::FREE_IN_3D)) {
32Entity *wrkpl = SK.GetEntity(g->activeWorkplane);
33origin = wrkpl->WorkplaneGetOffset();
34n = wrkpl->Normal()->NormalN();
35u = wrkpl->Normal()->NormalU();
36v = wrkpl->Normal()->NormalV();
37} else if(gs.n == 1 && gs.faces == 1) {
38Entity *face = SK.GetEntity(gs.entity[0]);
39origin = face->FaceGetPointNum();
40n = face->FaceGetNormalNum();
41if(n.Dot(gn) < 0) n = n.ScaledBy(-1);
42u = n.Normal(0);
43v = n.Normal(1);
44} else if(gs.n == 3 && gs.vectors == 2 && gs.points == 1) {
45Vector ut = SK.GetEntity(gs.entity[0])->VectorGetNum(),
46vt = SK.GetEntity(gs.entity[1])->VectorGetNum();
47ut = ut.WithMagnitude(1);
48vt = vt.WithMagnitude(1);
49
50if(fabs(SS.GW.projUp.Dot(vt)) < fabs(SS.GW.projUp.Dot(ut))) {
51swap(ut, vt);
52}
53if(SS.GW.projRight.Dot(ut) < 0) ut = ut.ScaledBy(-1);
54if(SS.GW.projUp. Dot(vt) < 0) vt = vt.ScaledBy(-1);
55
56origin = SK.GetEntity(gs.point[0])->PointGetNum();
57n = ut.Cross(vt);
58u = ut.WithMagnitude(1);
59v = (n.Cross(u)).WithMagnitude(1);
60} else {
61Error(_("Bad selection for export section. Please select:\n\n"
62" * nothing, with an active workplane "
63"(workplane is section plane)\n"
64" * a face (section plane through face)\n"
65" * a point and two line segments "
66"(plane through point and parallel to lines)\n"));
67return;
68}
69SS.GW.ClearSelection();
70
71n = n.WithMagnitude(1);
72d = origin.Dot(n);
73
74SEdgeList el = {};
75SBezierList bl = {};
76
77// If there's a mesh, then grab the edges from it.
78g->runningMesh.MakeEdgesInPlaneInto(&el, n, d);
79
80// If there's a shell, then grab the edges and possibly Beziers.
81bool export_as_pwl = SS.exportPwlCurves || fabs(SS.exportOffset) > LENGTH_EPS;
82g->runningShell.MakeSectionEdgesInto(n, d, &el, export_as_pwl ? NULL : &bl);
83
84// All of these are solid model edges, so use the appropriate style.
85SEdge *se;
86for(se = el.l.First(); se; se = el.l.NextAfter(se)) {
87se->auxA = Style::SOLID_EDGE;
88}
89SBezier *sb;
90for(sb = bl.l.First(); sb; sb = bl.l.NextAfter(sb)) {
91sb->auxA = Style::SOLID_EDGE;
92}
93
94// Remove all overlapping edges/beziers to merge the areas they describe.
95el.CullExtraneousEdges(/*both=*/true);
96bl.CullIdenticalBeziers(/*both=*/true);
97
98// Collect lines and beziers with custom style & export.
99for(auto &ent : SK.entity) {
100Entity *e = &ent;
101if (!e->IsVisible()) continue;
102if (e->style.v < Style::FIRST_CUSTOM) continue;
103if (!Style::Exportable(e->style.v)) continue;
104if (!e->IsInPlane(n,d)) continue;
105if (export_as_pwl) {
106e->GenerateEdges(&el);
107} else {
108e->GenerateBezierCurves(&bl);
109}
110}
111
112// Only remove half of the overlapping edges/beziers to support TTF Stick Fonts.
113el.CullExtraneousEdges(/*both=*/false);
114bl.CullIdenticalBeziers(/*both=*/false);
115
116// And write the edges.
117VectorFileWriter *out = VectorFileWriter::ForFile(filename);
118if(out) {
119// parallel projection (no perspective), and no mesh
120ExportLinesAndMesh(&el, &bl, NULL,
121u, v, n, origin, 0,
122out);
123}
124el.Clear();
125bl.Clear();
126}
127
128// This is an awful temporary hack to replace Constraint::GetEdges until we have proper
129// export through Canvas.
130class GetEdgesCanvas : public Canvas {
131public:
132Camera camera;
133SEdgeList *edges;
134
135const Camera &GetCamera() const override {
136return camera;
137}
138
139void DrawLine(const Vector &a, const Vector &b, hStroke hcs) override {
140edges->AddEdge(a, b, Style::CONSTRAINT);
141}
142void DrawEdges(const SEdgeList &el, hStroke hcs) override {
143for(const SEdge &e : el.l) {
144edges->AddEdge(e.a, e.b, Style::CONSTRAINT);
145}
146}
147void DrawVectorText(const std::string &text, double height,
148const Vector &o, const Vector &u, const Vector &v,
149hStroke hcs) override {
150auto traceEdge = [&](Vector a, Vector b) { edges->AddEdge(a, b, Style::CONSTRAINT); };
151VectorFont::Builtin()->Trace(height, o, u, v, text, traceEdge, camera);
152}
153
154void DrawQuad(const Vector &a, const Vector &b, const Vector &c, const Vector &d,
155hFill hcf) override {
156// Do nothing
157}
158
159bool DrawBeziers(const SBezierList &bl, hStroke hcs) override {
160ssassert(false, "Not implemented");
161}
162void DrawOutlines(const SOutlineList &ol, hStroke hcs, DrawOutlinesAs drawAs) override {
163ssassert(false, "Not implemented");
164}
165void DrawPoint(const Vector &o, hStroke hcs) override {
166ssassert(false, "Not implemented");
167}
168void DrawPolygon(const SPolygon &p, hFill hcf) override {
169ssassert(false, "Not implemented");
170}
171void DrawMesh(const SMesh &m, hFill hcfFront, hFill hcfBack = {}) override {
172ssassert(false, "Not implemented");
173}
174void DrawFaces(const SMesh &m, const std::vector<uint32_t> &faces, hFill hcf) override {
175ssassert(false, "Not implemented");
176}
177void DrawPixmap(std::shared_ptr<const Pixmap> pm,
178const Vector &o, const Vector &u, const Vector &v,
179const Point2d &ta, const Point2d &tb, hFill hcf) override {
180ssassert(false, "Not implemented");
181}
182void InvalidatePixmap(std::shared_ptr<const Pixmap> pm) override {
183ssassert(false, "Not implemented");
184}
185};
186
187void SolveSpaceUI::ExportViewOrWireframeTo(const Platform::Path &filename, bool exportWireframe) {
188SEdgeList edges = {};
189SBezierList beziers = {};
190
191VectorFileWriter *out = VectorFileWriter::ForFile(filename);
192if(!out) return;
193
194SS.exportMode = true;
195GenerateAll(Generate::ALL);
196
197SMesh *sm = NULL;
198if(SS.GW.showShaded || SS.GW.drawOccludedAs != GraphicsWindow::DrawOccludedAs::VISIBLE) {
199Group *g = SK.GetGroup(SS.GW.activeGroup);
200g->GenerateDisplayItems();
201sm = &(g->displayMesh);
202}
203if(sm && sm->IsEmpty()) {
204sm = NULL;
205}
206
207for(auto &entity : SK.entity) {
208Entity *e = &entity;
209if(!e->IsVisible()) continue;
210
211if(SS.exportPwlCurves || sm || fabs(SS.exportOffset) > LENGTH_EPS)
212{
213// We will be doing hidden line removal, which we can't do on
214// exact curves; so we need things broken down to pwls. Same
215// problem with cutter radius compensation.
216e->GenerateEdges(&edges);
217} else {
218e->GenerateBezierCurves(&beziers);
219}
220}
221
222if(SS.GW.showEdges || SS.GW.showOutlines) {
223Group *g = SK.GetGroup(SS.GW.activeGroup);
224g->GenerateDisplayItems();
225if(SS.GW.showEdges) {
226g->displayOutlines.ListTaggedInto(&edges, Style::SOLID_EDGE);
227}
228}
229
230if(SS.GW.showConstraints != GraphicsWindow::ShowConstraintMode::SCM_NOSHOW ) {
231if(!out->OutputConstraints(&SK.constraint)) {
232GetEdgesCanvas canvas = {};
233canvas.camera = SS.GW.GetCamera();
234canvas.edges = &edges;
235
236// The output format cannot represent constraints directly,
237// so convert them to edges.
238for(Constraint &c : SK.constraint) {
239c.Draw(Constraint::DrawAs::DEFAULT, &canvas);
240}
241
242canvas.Clear();
243}
244}
245
246if(exportWireframe) {
247Vector u = Vector::From(1.0, 0.0, 0.0),
248v = Vector::From(0.0, 1.0, 0.0),
249n = Vector::From(0.0, 0.0, 1.0),
250origin = Vector::From(0.0, 0.0, 0.0);
251double cameraTan = 0.0,
252scale = 1.0;
253
254out->SetModelviewProjection(u, v, n, origin, cameraTan, scale);
255
256ExportWireframeCurves(&edges, &beziers, out);
257} else {
258Vector u = SS.GW.projRight,
259v = SS.GW.projUp,
260n = u.Cross(v),
261origin = SS.GW.offset.ScaledBy(-1);
262
263out->SetModelviewProjection(u, v, n, origin,
264SS.CameraTangent()*SS.GW.scale, SS.exportScale);
265
266ExportLinesAndMesh(&edges, &beziers, sm,
267u, v, n, origin, SS.CameraTangent()*SS.GW.scale,
268out);
269
270if(!out->HasCanvasSize()) {
271// These file formats don't have a canvas size, so they just
272// get exported in the raw coordinate system. So indicate what
273// that was on-screen.
274SS.justExportedInfo.showOrigin = true;
275SS.justExportedInfo.pt = origin;
276SS.justExportedInfo.u = u;
277SS.justExportedInfo.v = v;
278} else {
279SS.justExportedInfo.showOrigin = false;
280}
281
282SS.justExportedInfo.draw = true;
283GW.Invalidate();
284}
285
286edges.Clear();
287beziers.Clear();
288}
289
290void SolveSpaceUI::ExportWireframeCurves(SEdgeList *sel, SBezierList *sbl,
291VectorFileWriter *out)
292{
293SBezierLoopSetSet sblss = {};
294SEdge *se;
295for(se = sel->l.First(); se; se = sel->l.NextAfter(se)) {
296SBezier sb = SBezier::From(
297(se->a).ScaledBy(1.0 / SS.exportScale),
298(se->b).ScaledBy(1.0 / SS.exportScale));
299sblss.AddOpenPath(&sb);
300}
301
302sbl->ScaleSelfBy(1.0/SS.exportScale);
303SBezier *sb;
304for(sb = sbl->l.First(); sb; sb = sbl->l.NextAfter(sb)) {
305sblss.AddOpenPath(sb);
306}
307
308out->OutputLinesAndMesh(&sblss, NULL);
309sblss.Clear();
310}
311
312void SolveSpaceUI::ExportLinesAndMesh(SEdgeList *sel, SBezierList *sbl, SMesh *sm,
313Vector u, Vector v, Vector n,
314Vector origin, double cameraTan,
315VectorFileWriter *out)
316{
317double s = 1.0 / SS.exportScale;
318
319// Project into the export plane; so when we're done, z doesn't matter,
320// and x and y are what goes in the DXF.
321for(SEdge *e = sel->l.First(); e; e = sel->l.NextAfter(e)) {
322// project into the specified csys, and apply export scale
323(e->a) = e->a.InPerspective(u, v, n, origin, cameraTan).ScaledBy(s);
324(e->b) = e->b.InPerspective(u, v, n, origin, cameraTan).ScaledBy(s);
325}
326
327if(sbl) {
328for(SBezier *b = sbl->l.First(); b; b = sbl->l.NextAfter(b)) {
329*b = b->InPerspective(u, v, n, origin, cameraTan);
330int i;
331for(i = 0; i <= b->deg; i++) {
332b->ctrl[i] = (b->ctrl[i]).ScaledBy(s);
333}
334}
335}
336
337// If cutter radius compensation is requested, then perform it now
338if(fabs(SS.exportOffset) > LENGTH_EPS) {
339// assemble those edges into a polygon, and clear the edge list
340SPolygon sp = {};
341sel->AssemblePolygon(&sp, NULL);
342sel->Clear();
343
344SPolygon compd = {};
345sp.normal = Vector::From(0, 0, -1);
346sp.FixContourDirections();
347sp.OffsetInto(&compd, SS.exportOffset*s);
348sp.Clear();
349
350compd.MakeEdgesInto(sel);
351compd.Clear();
352}
353
354// Now the triangle mesh; project, then build a BSP to perform
355// occlusion testing and generated the shaded surfaces.
356SMesh smp = {};
357if(sm) {
358Vector l0 = (SS.lightDir[0]).WithMagnitude(1),
359l1 = (SS.lightDir[1]).WithMagnitude(1);
360STriangle *tr;
361for(tr = sm->l.First(); tr; tr = sm->l.NextAfter(tr)) {
362STriangle tt = *tr;
363tt.a = (tt.a).InPerspective(u, v, n, origin, cameraTan).ScaledBy(s);
364tt.b = (tt.b).InPerspective(u, v, n, origin, cameraTan).ScaledBy(s);
365tt.c = (tt.c).InPerspective(u, v, n, origin, cameraTan).ScaledBy(s);
366
367// And calculate lighting for the triangle
368Vector n = tt.Normal().WithMagnitude(1);
369double lighting = min(1.0, SS.ambientIntensity +
370max(0.0, (SS.lightIntensity[0])*(n.Dot(l0))) +
371max(0.0, (SS.lightIntensity[1])*(n.Dot(l1))));
372double r = min(1.0, tt.meta.color.redF() * lighting),
373g = min(1.0, tt.meta.color.greenF() * lighting),
374b = min(1.0, tt.meta.color.blueF() * lighting);
375tt.meta.color = RGBf(r, g, b);
376smp.AddTriangle(&tt);
377}
378}
379
380SMesh sms = {};
381
382// We need the mesh for occlusion testing, but if we don't/can't export it,
383// don't generate it.
384if(SS.GW.showShaded && out->CanOutputMesh()) {
385// Use the BSP routines to generate the split triangles in paint order.
386SBsp3 *bsp = SBsp3::FromMesh(&smp);
387if(bsp) bsp->GenerateInPaintOrder(&sms);
388// And cull the back-facing triangles
389STriangle *tr;
390sms.l.ClearTags();
391for(tr = sms.l.First(); tr; tr = sms.l.NextAfter(tr)) {
392Vector n = tr->Normal();
393if(n.z < 0) {
394tr->tag = 1;
395}
396}
397sms.l.RemoveTagged();
398}
399
400// And now we perform hidden line removal if requested
401SEdgeList hlrd = {};
402if(sm) {
403SKdNode *root = SKdNode::From(&smp);
404
405// Generate the edges where a curved surface turns from front-facing
406// to back-facing.
407if(SS.GW.showEdges || SS.GW.showOutlines) {
408root->MakeCertainEdgesInto(sel, EdgeKind::TURNING,
409/*coplanarIsInter=*/false, NULL, NULL,
410GW.showOutlines ? Style::OUTLINE : Style::SOLID_EDGE);
411}
412
413root->ClearTags();
414int cnt = 1234;
415
416SEdge *se;
417for(se = sel->l.First(); se; se = sel->l.NextAfter(se)) {
418if(se->auxA == Style::CONSTRAINT) {
419// Constraints should not get hidden line removed; they're
420// always on top.
421hlrd.AddEdge(se->a, se->b, se->auxA);
422continue;
423}
424
425SEdgeList edges = {};
426// Split the original edge against the mesh
427edges.AddEdge(se->a, se->b, se->auxA);
428root->OcclusionTestLine(*se, &edges, cnt);
429if(SS.GW.drawOccludedAs == GraphicsWindow::DrawOccludedAs::STIPPLED) {
430for(SEdge &se : edges.l) {
431if(se.tag == 1) {
432se.auxA = Style::HIDDEN_EDGE;
433}
434}
435} else if(SS.GW.drawOccludedAs == GraphicsWindow::DrawOccludedAs::INVISIBLE) {
436edges.l.RemoveTagged();
437}
438
439// the occlusion test splits unnecessarily; so fix those
440edges.MergeCollinearSegments(se->a, se->b);
441cnt++;
442// And add the results to our output
443SEdge *sen;
444for(sen = edges.l.First(); sen; sen = edges.l.NextAfter(sen)) {
445hlrd.AddEdge(sen->a, sen->b, sen->auxA);
446}
447edges.Clear();
448}
449
450sel = &hlrd;
451}
452
453// Clean up: remove overlapping line segments and
454// segments with zero-length projections.
455sel->l.ClearTags();
456for(int i = 0; i < sel->l.n; ++i) {
457SEdge *sei = &sel->l[i];
458hStyle hsi = { (uint32_t)sei->auxA };
459Style *si = Style::Get(hsi);
460if(sei->tag != 0) continue;
461
462// Remove segments with zero length projections.
463Vector ai = sei->a;
464ai.z = 0.0;
465Vector bi = sei->b;
466bi.z = 0.0;
467Vector di = bi.Minus(ai);
468if(fabs(di.x) < LENGTH_EPS && fabs(di.y) < LENGTH_EPS) {
469sei->tag = 1;
470continue;
471}
472
473for(int j = i + 1; j < sel->l.n; ++j) {
474SEdge *sej = &sel->l[j];
475if(sej->tag != 0) continue;
476
477Vector *pAj = &sej->a;
478Vector *pBj = &sej->b;
479
480// Remove segments with zero length projections.
481Vector aj = sej->a;
482aj.z = 0.0;
483Vector bj = sej->b;
484bj.z = 0.0;
485Vector dj = bj.Minus(aj);
486if(fabs(dj.x) < LENGTH_EPS && fabs(dj.y) < LENGTH_EPS) {
487sej->tag = 1;
488continue;
489}
490
491// Skip non-collinear segments.
492const double eps = 1e-6;
493if(aj.DistanceToLine(ai, di) > eps) continue;
494if(bj.DistanceToLine(ai, di) > eps) continue;
495
496double ta = aj.Minus(ai).Dot(di) / di.Dot(di);
497double tb = bj.Minus(ai).Dot(di) / di.Dot(di);
498if(ta > tb) {
499std::swap(pAj, pBj);
500std::swap(ta, tb);
501}
502
503hStyle hsj = { (uint32_t)sej->auxA };
504Style *sj = Style::Get(hsj);
505
506bool canRemoveI = sej->auxA == sei->auxA || si->zIndex < sj->zIndex;
507bool canRemoveJ = sej->auxA == sei->auxA || sj->zIndex < si->zIndex;
508
509if(canRemoveJ) {
510// j-segment inside i-segment
511if(ta > 0.0 - eps && tb < 1.0 + eps) {
512sej->tag = 1;
513continue;
514}
515
516// cut segment
517bool aInside = ta > 0.0 - eps && ta < 1.0 + eps;
518if(tb > 1.0 - eps && aInside) {
519*pAj = sei->b;
520continue;
521}
522
523// cut segment
524bool bInside = tb > 0.0 - eps && tb < 1.0 + eps;
525if(ta < 0.0 - eps && bInside) {
526*pBj = sei->a;
527continue;
528}
529
530// split segment
531if(ta < 0.0 - eps && tb > 1.0 + eps) {
532sel->AddEdge(sei->b, *pBj, sej->auxA, sej->auxB);
533*pBj = sei->a;
534continue;
535}
536}
537
538if(canRemoveI) {
539// j-segment inside i-segment
540if(ta < 0.0 + eps && tb > 1.0 - eps) {
541sei->tag = 1;
542break;
543}
544
545// cut segment
546bool aInside = ta > 0.0 + eps && ta < 1.0 - eps;
547if(tb > 1.0 - eps && aInside) {
548sei->b = *pAj;
549i--;
550break;
551}
552
553// cut segment
554bool bInside = tb > 0.0 + eps && tb < 1.0 - eps;
555if(ta < 0.0 + eps && bInside) {
556sei->a = *pBj;
557i--;
558break;
559}
560
561// split segment
562if(ta > 0.0 + eps && tb < 1.0 - eps) {
563sel->AddEdge(*pBj, sei->b, sei->auxA, sei->auxB);
564sei->b = *pAj;
565i--;
566break;
567}
568}
569}
570}
571sel->l.RemoveTagged();
572
573// We kept the line segments and Beziers separate until now; but put them
574// all together, and also project everything into the xy plane, since not
575// all export targets ignore the z component of the points.
576ssassert(sbl != nullptr, "Adding line segments to beziers assumes bezier list is non-null.");
577for(SEdge *e = sel->l.First(); e; e = sel->l.NextAfter(e)) {
578SBezier sb = SBezier::From(e->a, e->b);
579sb.auxA = e->auxA;
580sbl->l.Add(&sb);
581}
582for(SBezier *b = sbl->l.First(); b; b = sbl->l.NextAfter(b)) {
583for(int i = 0; i <= b->deg; i++) {
584b->ctrl[i].z = 0;
585}
586}
587
588// If possible, then we will assemble these output curves into loops. They
589// will then get exported as closed paths.
590SBezierLoopSetSet sblss = {};
591SBezierLoopSet leftovers = {};
592SSurface srf = SSurface::FromPlane(Vector::From(0, 0, 0),
593Vector::From(1, 0, 0),
594Vector::From(0, 1, 0));
595SPolygon spxyz = {};
596bool allClosed;
597SEdge notClosedAt;
598sbl->l.ClearTags();
599sblss.FindOuterFacesFrom(sbl, &spxyz, &srf,
600SS.ExportChordTolMm(),
601&allClosed, ¬ClosedAt,
602NULL, NULL,
603&leftovers);
604sblss.l.Add(&leftovers);
605
606// Now write the lines and triangles to the output file
607out->OutputLinesAndMesh(&sblss, &sms);
608
609spxyz.Clear();
610sblss.Clear();
611smp.Clear();
612sms.Clear();
613hlrd.Clear();
614}
615
616double VectorFileWriter::MmToPts(double mm) {
617// 72 points in an inch
618return (mm/25.4)*72;
619}
620
621VectorFileWriter *VectorFileWriter::ForFile(const Platform::Path &filename) {
622VectorFileWriter *ret;
623bool needOpen = true;
624if(filename.HasExtension("dxf")) {
625static DxfFileWriter DxfWriter;
626ret = &DxfWriter;
627needOpen = false;
628} else if(filename.HasExtension("ps") || filename.HasExtension("eps")) {
629static EpsFileWriter EpsWriter;
630ret = &EpsWriter;
631} else if(filename.HasExtension("pdf")) {
632static PdfFileWriter PdfWriter;
633ret = &PdfWriter;
634} else if(filename.HasExtension("svg")) {
635static SvgFileWriter SvgWriter;
636ret = &SvgWriter;
637} else if(filename.HasExtension("plt") || filename.HasExtension("hpgl")) {
638static HpglFileWriter HpglWriter;
639ret = &HpglWriter;
640} else if(filename.HasExtension("step") || filename.HasExtension("stp")) {
641static Step2dFileWriter Step2dWriter;
642ret = &Step2dWriter;
643} else if(filename.HasExtension("txt") || filename.HasExtension("ngc")) {
644static GCodeFileWriter GCodeWriter;
645ret = &GCodeWriter;
646} else {
647Error("Can't identify output file type from file extension of "
648"filename '%s'; try "
649".step, .stp, .dxf, .svg, .plt, .hpgl, .pdf, .txt, .ngc, "
650".eps, or .ps.",
651filename.raw.c_str());
652return NULL;
653}
654ret->filename = filename;
655if(!needOpen) return ret;
656
657FILE *f = OpenFile(filename, "wb");
658if(!f) {
659Error("Couldn't write to '%s'", filename.raw.c_str());
660return NULL;
661}
662ret->f = f;
663return ret;
664}
665
666void VectorFileWriter::SetModelviewProjection(const Vector &u, const Vector &v, const Vector &n,
667const Vector &origin, double cameraTan,
668double scale) {
669this->u = u;
670this->v = v;
671this->n = n;
672this->origin = origin;
673this->cameraTan = cameraTan;
674this->scale = scale;
675}
676
677Vector VectorFileWriter::Transform(Vector &pos) const {
678return pos.InPerspective(u, v, n, origin, cameraTan).ScaledBy(1.0 / scale);
679}
680
681void VectorFileWriter::OutputLinesAndMesh(SBezierLoopSetSet *sblss, SMesh *sm) {
682STriangle *tr;
683SBezier *b;
684
685// First calculate the bounding box.
686ptMin = Vector::From(VERY_POSITIVE, VERY_POSITIVE, VERY_POSITIVE);
687ptMax = Vector::From(VERY_NEGATIVE, VERY_NEGATIVE, VERY_NEGATIVE);
688if(sm) {
689for(tr = sm->l.First(); tr; tr = sm->l.NextAfter(tr)) {
690(tr->a).MakeMaxMin(&ptMax, &ptMin);
691(tr->b).MakeMaxMin(&ptMax, &ptMin);
692(tr->c).MakeMaxMin(&ptMax, &ptMin);
693}
694}
695if(sblss) {
696SBezierLoopSet *sbls;
697for(sbls = sblss->l.First(); sbls; sbls = sblss->l.NextAfter(sbls)) {
698SBezierLoop *sbl;
699for(sbl = sbls->l.First(); sbl; sbl = sbls->l.NextAfter(sbl)) {
700for(b = sbl->l.First(); b; b = sbl->l.NextAfter(b)) {
701for(int i = 0; i <= b->deg; i++) {
702(b->ctrl[i]).MakeMaxMin(&ptMax, &ptMin);
703}
704}
705}
706}
707}
708
709// And now we compute the canvas size.
710double s = 1.0 / SS.exportScale;
711if(SS.exportCanvasSizeAuto) {
712// It's based on the calculated bounding box; we grow it along each
713// boundary by the specified amount.
714ptMin.x -= s*SS.exportMargin.left;
715ptMax.x += s*SS.exportMargin.right;
716ptMin.y -= s*SS.exportMargin.bottom;
717ptMax.y += s*SS.exportMargin.top;
718} else {
719ptMin.x = (s*SS.exportCanvas.dx);
720ptMin.y = (s*SS.exportCanvas.dy);
721ptMax.x = ptMin.x + (s*SS.exportCanvas.width);
722ptMax.y = ptMin.y + (s*SS.exportCanvas.height);
723}
724
725StartFile();
726if(SS.exportBackgroundColor) {
727Background(SS.backgroundColor);
728}
729if(sm && SS.exportShadedTriangles) {
730for(tr = sm->l.First(); tr; tr = sm->l.NextAfter(tr)) {
731Triangle(tr);
732}
733}
734if(sblss) {
735SBezierLoopSet *sbls;
736for(sbls = sblss->l.First(); sbls; sbls = sblss->l.NextAfter(sbls)) {
737for(SBezierLoop *sbl = sbls->l.First(); sbl; sbl = sbls->l.NextAfter(sbl)) {
738b = sbl->l.First();
739if(!b || !Style::Exportable(b->auxA)) continue;
740
741hStyle hs = { (uint32_t)b->auxA };
742Style *stl = Style::Get(hs);
743double lineWidth = Style::WidthMm(b->auxA)*s;
744RgbaColor strokeRgb = Style::Color(hs, /*forExport=*/true);
745RgbaColor fillRgb = Style::FillColor(hs, /*forExport=*/true);
746
747StartPath(strokeRgb, lineWidth, stl->filled, fillRgb, hs);
748for(b = sbl->l.First(); b; b = sbl->l.NextAfter(b)) {
749Bezier(b);
750}
751FinishPath(strokeRgb, lineWidth, stl->filled, fillRgb, hs);
752}
753}
754}
755FinishAndCloseFile();
756}
757
758void VectorFileWriter::BezierAsPwl(SBezier *sb) {
759List<Vector> lv = {};
760sb->MakePwlInto(&lv, SS.ExportChordTolMm());
761
762for(int i = 1; i < lv.n; i++) {
763SBezier sb = SBezier::From(lv[i-1], lv[i]);
764Bezier(&sb);
765}
766lv.Clear();
767}
768
769void VectorFileWriter::BezierAsNonrationalCubic(SBezier *sb, int depth) {
770Vector t0 = sb->TangentAt(0), t1 = sb->TangentAt(1);
771// The curve is correct, and the first derivatives are correct, at the
772// endpoints.
773SBezier bnr = SBezier::From(
774sb->Start(),
775sb->Start().Plus(t0.ScaledBy(1.0/3)),
776sb->Finish().Minus(t1.ScaledBy(1.0/3)),
777sb->Finish());
778
779double tol = SS.ExportChordTolMm();
780// Arbitrary choice, but make it a little finer than pwl tolerance since
781// it should be easier to achieve that with the smooth curves.
782tol /= 2;
783
784bool closeEnough = true;
785int i;
786for(i = 1; i <= 3; i++) {
787double t = i/4.0;
788Vector p0 = sb->PointAt(t),
789pn = bnr.PointAt(t);
790double d = (p0.Minus(pn)).Magnitude();
791if(d > tol) {
792closeEnough = false;
793}
794}
795
796if(closeEnough || depth > 3) {
797Bezier(&bnr);
798} else {
799SBezier bef, aft;
800sb->SplitAt(0.5, &bef, &aft);
801BezierAsNonrationalCubic(&bef, depth+1);
802BezierAsNonrationalCubic(&aft, depth+1);
803}
804}
805
806//-----------------------------------------------------------------------------
807// Export a triangle mesh, in the requested format.
808//-----------------------------------------------------------------------------
809void SolveSpaceUI::ExportMeshTo(const Platform::Path &filename) {
810SS.exportMode = true;
811GenerateAll(Generate::ALL);
812
813Group *g = SK.GetGroup(SS.GW.activeGroup);
814g->GenerateDisplayItems();
815
816SMesh *m = &(SK.GetGroup(SS.GW.activeGroup)->displayMesh);
817if(m->IsEmpty()) {
818Error(_("Active group mesh is empty; nothing to export."));
819return;
820}
821
822FILE *f = OpenFile(filename, "wb");
823if(!f) {
824Error("Couldn't write to '%s'", filename.raw.c_str());
825return;
826}
827ShowNakedEdges(/*reportOnlyWhenNotOkay=*/true);
828if(filename.HasExtension("stl")) {
829ExportMeshAsStlTo(f, m);
830} else if(filename.HasExtension("obj")) {
831Platform::Path mtlFilename = filename.WithExtension("mtl");
832FILE *fMtl = OpenFile(mtlFilename, "wb");
833if(!fMtl) {
834Error("Couldn't write to '%s'", filename.raw.c_str());
835return;
836}
837
838fprintf(f, "mtllib %s\n", mtlFilename.FileName().c_str());
839ExportMeshAsObjTo(f, fMtl, m);
840
841fclose(fMtl);
842} else if(filename.HasExtension("js") ||
843filename.HasExtension("html")) {
844SOutlineList *e = &(SK.GetGroup(SS.GW.activeGroup)->displayOutlines);
845ExportMeshAsThreeJsTo(f, filename, m, e);
846} else if(filename.HasExtension("wrl")) {
847ExportMeshAsVrmlTo(f, filename, m);
848} else {
849Error("Can't identify output file type from file extension of "
850"filename '%s'; try .stl, .obj, .js, .html.", filename.raw.c_str());
851}
852
853fclose(f);
854
855SS.justExportedInfo.showOrigin = false;
856SS.justExportedInfo.draw = true;
857GW.Invalidate();
858}
859
860//-----------------------------------------------------------------------------
861// Export the mesh as an STL file; it should always be vertex-to-vertex and
862// not self-intersecting, so not much to do.
863//-----------------------------------------------------------------------------
864void SolveSpaceUI::ExportMeshAsStlTo(FILE *f, SMesh *sm) {
865char str[80] = {};
866strcpy(str, "STL exported mesh");
867fwrite(str, 1, 80, f);
868
869uint32_t n = sm->l.n;
870fwrite(&n, 4, 1, f);
871
872double s = SS.exportScale;
873int i;
874for(i = 0; i < sm->l.n; i++) {
875STriangle *tr = &(sm->l[i]);
876Vector n = tr->Normal().WithMagnitude(1);
877float w;
878w = (float)n.x; fwrite(&w, 4, 1, f);
879w = (float)n.y; fwrite(&w, 4, 1, f);
880w = (float)n.z; fwrite(&w, 4, 1, f);
881w = (float)((tr->a.x)/s); fwrite(&w, 4, 1, f);
882w = (float)((tr->a.y)/s); fwrite(&w, 4, 1, f);
883w = (float)((tr->a.z)/s); fwrite(&w, 4, 1, f);
884w = (float)((tr->b.x)/s); fwrite(&w, 4, 1, f);
885w = (float)((tr->b.y)/s); fwrite(&w, 4, 1, f);
886w = (float)((tr->b.z)/s); fwrite(&w, 4, 1, f);
887w = (float)((tr->c.x)/s); fwrite(&w, 4, 1, f);
888w = (float)((tr->c.y)/s); fwrite(&w, 4, 1, f);
889w = (float)((tr->c.z)/s); fwrite(&w, 4, 1, f);
890fputc(0, f);
891fputc(0, f);
892}
893}
894
895//-----------------------------------------------------------------------------
896// Export the mesh as Wavefront OBJ format. This requires us to reduce all the
897// identical vertices to the same identifier, so do that first.
898//-----------------------------------------------------------------------------
899void SolveSpaceUI::ExportMeshAsObjTo(FILE *fObj, FILE *fMtl, SMesh *sm) {
900std::map<RgbaColor, std::string, RgbaColorCompare> colors;
901for(const STriangle &t : sm->l) {
902RgbaColor color = t.meta.color;
903if(colors.find(color) == colors.end()) {
904std::string id = ssprintf("h%02x%02x%02x",
905color.red,
906color.green,
907color.blue);
908colors.emplace(color, id);
909}
910for(int i = 0; i < 3; i++) {
911fprintf(fObj, "v %.10f %.10f %.10f\n",
912CO(t.vertices[i].ScaledBy(1 / SS.exportScale)));
913}
914}
915
916for(auto &it : colors) {
917fprintf(fMtl, "newmtl %s\n",
918it.second.c_str());
919fprintf(fMtl, "Kd %.3f %.3f %.3f\n",
920it.first.redF(), it.first.greenF(), it.first.blueF());
921}
922
923for(const STriangle &t : sm->l) {
924for(int i = 0; i < 3; i++) {
925Vector n = t.normals[i].WithMagnitude(1.0);
926fprintf(fObj, "vn %.10f %.10f %.10f\n",
927CO(n));
928}
929}
930
931RgbaColor currentColor = {};
932for(int i = 0; i < sm->l.n; i++) {
933const STriangle &t = sm->l[i];
934if(!currentColor.Equals(t.meta.color)) {
935currentColor = t.meta.color;
936fprintf(fObj, "usemtl %s\n", colors[currentColor].c_str());
937}
938
939fprintf(fObj, "f %d//%d %d//%d %d//%d\n",
940i * 3 + 1, i * 3 + 1,
941i * 3 + 2, i * 3 + 2,
942i * 3 + 3, i * 3 + 3);
943}
944}
945
946//-----------------------------------------------------------------------------
947// Export the mesh as a JavaScript script, which is compatible with Three.js.
948//-----------------------------------------------------------------------------
949void SolveSpaceUI::ExportMeshAsThreeJsTo(FILE *f, const Platform::Path &filename,
950SMesh *sm, SOutlineList *sol)
951{
952SPointList spl = {};
953STriangle *tr;
954Vector bndl, bndh;
955
956const std::string THREE_FN("three-r111.min.js");
957const std::string HAMMER_FN("hammer-2.0.8.js");
958const std::string CONTROLS_FN("SolveSpaceControls.js");
959
960const char htmlbegin[] = R"(
961<!DOCTYPE html>
962<html lang="en">
963<head>
964<meta charset="utf-8"></meta>
965<title>Three.js Solvespace Mesh</title>
966<script id="%s">%s</script>
967<script id="%s">%s</script>
968<script id="%s">%s</script>
969<style type="text/css">
970body { margin: 0; overflow: hidden; }
971</style>
972</head>
973<body>
974<script>
975)";
976const char htmlend[] = R"(
977document.body.appendChild(solvespace(solvespace_model_%s, {
978scale: %g,
979offset: new THREE.Vector3(%g, %g, %g),
980projUp: new THREE.Vector3(%g, %g, %g),
981projRight: new THREE.Vector3(%g, %g, %g)
982}));
983</script>
984</body>
985</html>
986)";
987
988// A default three.js viewer with OrthographicTrackballControls is
989// generated as a comment preceding the data.
990
991// x bounds should be the range of x or y, whichever
992// is larger, before aspect ratio correction is applied.
993// y bounds should be the range of x or y, whichever is
994// larger. No aspect ratio correction is applied.
995// Near plane should be 1.
996// Camera's z-position should be the range of z + 1 or the larger of
997// the x or y bounds, whichever is larger.
998// Far plane should be at least twice as much as the camera's
999// z-position.
1000// Edge projection bias should be about 1/500 of the far plane's distance.
1001// Further corrections will be applied to the z-position and far plane in
1002// the default viewer, but the defaults are fine for a model which
1003// only rotates about the world origin.
1004
1005sm->GetBounding(&bndh, &bndl);
1006double largerBoundXY = max((bndh.x - bndl.x), (bndh.y - bndl.y));
1007double largerBoundZ = max(largerBoundXY, (bndh.z - bndl.z + 1));
1008
1009std::string basename = filename.FileStem();
1010for(size_t i = 0; i < basename.length(); i++) {
1011if(!(isalnum(basename[i]) || ((unsigned)basename[i] >= 0x80))) {
1012basename[i] = '_';
1013}
1014}
1015
1016if(filename.HasExtension("html")) {
1017fprintf(f, htmlbegin,
1018THREE_FN.c_str(),
1019LoadStringFromGzip("threejs/" + THREE_FN + ".gz").c_str(),
1020HAMMER_FN.c_str(),
1021LoadStringFromGzip("threejs/" + HAMMER_FN + ".gz").c_str(),
1022CONTROLS_FN.c_str(),
1023LoadString("threejs/" + CONTROLS_FN).c_str());
1024}
1025
1026fprintf(f, "var solvespace_model_%s = {\n"
1027" bounds: {\n"
1028" x: %f, y: %f, near: %f, far: %f, z: %f, edgeBias: %f\n"
1029" },\n",
1030basename.c_str(),
1031largerBoundXY,
1032largerBoundXY,
10331.0,
1034largerBoundZ * 2,
1035largerBoundZ,
1036largerBoundZ / 250);
1037
1038// Output lighting information.
1039fputs(" lights: {\n"
1040" d: [\n", f);
1041
1042// Directional.
1043int lightCount;
1044for(lightCount = 0; lightCount < 2; lightCount++) {
1045fprintf(f, " {\n"
1046" intensity: %f, direction: [%f, %f, %f]\n"
1047" },\n",
1048SS.lightIntensity[lightCount],
1049CO(SS.lightDir[lightCount]));
1050}
1051
1052// Global Ambience.
1053fprintf(f, " ],\n"
1054" a: %f\n", SS.ambientIntensity);
1055
1056for(tr = sm->l.First(); tr; tr = sm->l.NextAfter(tr)) {
1057spl.IncrementTagFor(tr->a);
1058spl.IncrementTagFor(tr->b);
1059spl.IncrementTagFor(tr->c);
1060}
1061
1062// Output all the vertices.
1063SPoint *sp;
1064fputs(" },\n"
1065" points: [\n", f);
1066for(sp = spl.l.First(); sp; sp = spl.l.NextAfter(sp)) {
1067fprintf(f, " [%f, %f, %f],\n",
1068sp->p.x / SS.exportScale,
1069sp->p.y / SS.exportScale,
1070sp->p.z / SS.exportScale);
1071}
1072
1073fputs(" ],\n"
1074" faces: [\n", f);
1075// And now all the triangular faces, in terms of those vertices.
1076// This time we count from zero.
1077for(tr = sm->l.First(); tr; tr = sm->l.NextAfter(tr)) {
1078fprintf(f, " [%d, %d, %d],\n",
1079spl.IndexForPoint(tr->a),
1080spl.IndexForPoint(tr->b),
1081spl.IndexForPoint(tr->c));
1082}
1083
1084// Output face normals.
1085fputs(" ],\n"
1086" normals: [\n", f);
1087for(tr = sm->l.First(); tr; tr = sm->l.NextAfter(tr)) {
1088fprintf(f, " [[%f, %f, %f], [%f, %f, %f], [%f, %f, %f]],\n",
1089CO(tr->an), CO(tr->bn), CO(tr->cn));
1090}
1091
1092fputs(" ],\n"
1093" colors: [\n", f);
1094// Output triangle colors.
1095for(tr = sm->l.First(); tr; tr = sm->l.NextAfter(tr)) {
1096fprintf(f, " 0x%x,\n", tr->meta.color.ToARGB32());
1097}
1098
1099fputs(" ],\n"
1100" edges: [\n", f);
1101// Output edges. Assume user's model colors do not obscure white edges.
1102for(const SOutline &so : sol->l) {
1103if(so.tag == 0) continue;
1104fprintf(f, " [[%f, %f, %f], [%f, %f, %f]],\n",
1105so.a.x / SS.exportScale,
1106so.a.y / SS.exportScale,
1107so.a.z / SS.exportScale,
1108so.b.x / SS.exportScale,
1109so.b.y / SS.exportScale,
1110so.b.z / SS.exportScale);
1111}
1112
1113fputs(" ]\n};\n", f);
1114
1115if(filename.HasExtension("html")) {
1116fprintf(f, htmlend,
1117basename.c_str(),
1118SS.GW.scale,
1119CO(SS.GW.offset),
1120CO(SS.GW.projUp),
1121CO(SS.GW.projRight));
1122}
1123
1124spl.Clear();
1125}
1126
1127//-----------------------------------------------------------------------------
1128// Export the mesh as a VRML text file / WRL.
1129//-----------------------------------------------------------------------------
1130void SolveSpaceUI::ExportMeshAsVrmlTo(FILE *f, const Platform::Path &filename, SMesh *sm) {
1131struct STriangleSpan {
1132STriangle *first, *past_last;
1133
1134STriangle *begin() const { return first; }
1135STriangle *end() const { return past_last; }
1136};
1137
1138
1139std::string basename = filename.FileStem();
1140for(auto & c : basename) {
1141if(!(isalnum(c) || ((unsigned)c >= 0x80))) {
1142c = '_';
1143}
1144}
1145
1146fprintf(f, "#VRML V2.0 utf8\n"
1147"#Exported from SolveSpace %s\n"
1148"\n"
1149"DEF %s Transform {\n"
1150" children [",
1151PACKAGE_VERSION,
1152basename.c_str());
1153
1154
1155std::map<std::uint8_t, std::vector<STriangleSpan>> opacities;
1156STriangle *start = sm->l.begin();
1157std::uint8_t last_opacity = start->meta.color.alpha;
1158for(auto & tr : sm->l) {
1159if(tr.meta.color.alpha != last_opacity) {
1160opacities[last_opacity].push_back(STriangleSpan{start, &tr});
1161start = &tr;
1162last_opacity = start->meta.color.alpha;
1163}
1164}
1165opacities[last_opacity].push_back(STriangleSpan{start, sm->l.end()});
1166
1167for(auto && op : opacities) {
1168fprintf(f, "\n"
1169" Shape {\n"
1170" appearance Appearance {\n"
1171" material DEF %s_material_%u Material {\n"
1172" diffuseColor %f %f %f\n"
1173" ambientIntensity %f\n"
1174" transparency %f\n"
1175" }\n"
1176" }\n"
1177" geometry IndexedFaceSet {\n"
1178" colorPerVertex TRUE\n"
1179" coord Coordinate { point [\n",
1180basename.c_str(),
1181(unsigned)op.first,
1182SS.ambientIntensity,
1183SS.ambientIntensity,
1184SS.ambientIntensity,
1185SS.ambientIntensity,
11861.f - ((float)op.first / 255.0f));
1187
1188SPointList spl = {};
1189
1190for(const auto & sp : op.second) {
1191for(const auto & tr : sp) {
1192spl.IncrementTagFor(tr.a);
1193spl.IncrementTagFor(tr.b);
1194spl.IncrementTagFor(tr.c);
1195}
1196}
1197
1198// Output all the vertices.
1199for(auto sp : spl.l) {
1200fprintf(f, " %f %f %f,\n",
1201sp.p.x / SS.exportScale,
1202sp.p.y / SS.exportScale,
1203sp.p.z / SS.exportScale);
1204}
1205
1206fputs(" ] }\n"
1207" coordIndex [\n", f);
1208// And now all the triangular faces, in terms of those vertices.
1209for(const auto & sp : op.second) {
1210for(const auto & tr : sp) {
1211fprintf(f, " %d, %d, %d, -1,\n",
1212spl.IndexForPoint(tr.a),
1213spl.IndexForPoint(tr.b),
1214spl.IndexForPoint(tr.c));
1215}
1216}
1217
1218fputs(" ]\n"
1219" color Color { color [\n", f);
1220// Output triangle colors.
1221std::vector<int> triangle_colour_ids;
1222std::vector<RgbaColor> colours_present;
1223for(const auto & sp : op.second) {
1224for(const auto & tr : sp) {
1225const auto colour_itr = std::find_if(colours_present.begin(), colours_present.end(),
1226[&](const RgbaColor & c) {
1227return c.Equals(tr.meta.color);
1228});
1229if(colour_itr == colours_present.end()) {
1230fprintf(f, " %.10f %.10f %.10f,\n",
1231tr.meta.color.redF(),
1232tr.meta.color.greenF(),
1233tr.meta.color.blueF());
1234triangle_colour_ids.push_back(colours_present.size());
1235colours_present.insert(colours_present.end(), tr.meta.color);
1236} else {
1237triangle_colour_ids.push_back(colour_itr - colours_present.begin());
1238}
1239}
1240}
1241
1242fputs(" ] }\n"
1243" colorIndex [\n", f);
1244
1245for(auto colour_idx : triangle_colour_ids) {
1246fprintf(f, " %d, %d, %d, -1,\n", colour_idx, colour_idx, colour_idx);
1247}
1248
1249fputs(" ]\n"
1250" }\n"
1251" }\n", f);
1252
1253spl.Clear();
1254}
1255
1256fputs(" ]\n"
1257"}\n", f);
1258}
1259
1260//-----------------------------------------------------------------------------
1261// Export a view of the model as an image; we just take a screenshot, by
1262// rendering the view in the usual way and then copying the pixels.
1263//-----------------------------------------------------------------------------
1264void SolveSpaceUI::ExportAsPngTo(const Platform::Path &filename) {
1265screenshotFile = filename;
1266// The rest of the work is done in the next redraw.
1267GW.Invalidate();
1268}
1269