diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b194fd..c85bda3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ New sketch features: * A new sketch in workplane group can be created based on existing workplane. * TTF text request has two additional points on the right side, which allow constraining the width of text. + * Image requests can now be created, similar to TTF text requests. * Irrelevant points (e.g. arc center point) are not counted when estimating the bounding box used to compute chord tolerance. * When adding a constraint which has a label and is redundant with another diff --git a/res/CMakeLists.txt b/res/CMakeLists.txt index b91f216..200eb6f 100644 --- a/res/CMakeLists.txt +++ b/res/CMakeLists.txt @@ -158,6 +158,7 @@ add_resources( icons/graphics-window/equal.png icons/graphics-window/extrude.png icons/graphics-window/horiz.png + icons/graphics-window/image.png icons/graphics-window/in3d.png icons/graphics-window/lathe.png icons/graphics-window/length.png diff --git a/res/icons/graphics-window/image.png b/res/icons/graphics-window/image.png new file mode 100644 index 0000000..d652d8e Binary files /dev/null and b/res/icons/graphics-window/image.png differ diff --git a/res/shaders/imesh.frag b/res/shaders/imesh.frag index 872bb48..4fa0713 100644 --- a/res/shaders/imesh.frag +++ b/res/shaders/imesh.frag @@ -4,7 +4,9 @@ // Copyright 2016 Aleksey Egorov //----------------------------------------------------------------------------- uniform vec4 color; +uniform sampler2D texture; void main() { + if(texture2D(texture, gl_FragCoord.xy / 32.0).a < 0.5) discard; gl_FragColor = color; } diff --git a/src/describescreen.cpp b/src/describescreen.cpp index 003c31d..454cedf 100644 --- a/src/describescreen.cpp +++ b/src/describescreen.cpp @@ -195,6 +195,16 @@ void TextWindow::DescribeSelection() { } break; } + case Entity::Type::IMAGE: { + Printf(false, "%FtIMAGE%E"); + Platform::Path relativePath = e->file.RelativeTo(SS.saveFile.Parent()); + if(relativePath.IsEmpty()) { + Printf(true, " file = '%Fi%s%E'", e->file.raw.c_str()); + } else { + Printf(true, " file = '%Fi%s%E'", relativePath.raw.c_str()); + } + break; + } default: Printf(true, "%Ft?? ENTITY%E"); diff --git a/src/draw.cpp b/src/draw.cpp index e7e007c..1a638a7 100644 --- a/src/draw.cpp +++ b/src/draw.cpp @@ -394,6 +394,9 @@ void GraphicsWindow::HitTestMakeSelection(Point2d mp) { for(Entity &e : SK.entity) { if(!e.IsVisible()) continue; + // If faces aren't selectable, image entities aren't either. + if(e.type == Entity::Type::IMAGE && !showFaces) continue; + // Don't hover whatever's being dragged. if(IsFromPending(e.h.request())) { // The one exception is when we're creating a new cubic; we diff --git a/src/drawentity.cpp b/src/drawentity.cpp index 1e5a31e..ce455f1 100644 --- a/src/drawentity.cpp +++ b/src/drawentity.cpp @@ -104,6 +104,7 @@ void Entity::GetReferencePoints(std::vector *refs) { case Type::CUBIC: case Type::CUBIC_PERIODIC: case Type::TTF_TEXT: + case Type::IMAGE: refs->push_back(SK.GetEntity(point[0])->PointGetNum()); break; @@ -695,6 +696,54 @@ void Entity::Draw(DrawAs how, Canvas *canvas) { } return; } + case Type::IMAGE: { + Canvas::Fill fill = {}; + std::shared_ptr pixmap; + switch(how) { + case DrawAs::HIDDEN: return; + + case DrawAs::HOVERED: { + fill.color = Style::Color(Style::HOVERED).WithAlpha(180); + fill.pattern = Canvas::FillPattern::CHECKERED_A; + fill.zIndex = 2; + break; + } + + case DrawAs::SELECTED: { + fill.color = Style::Color(Style::SELECTED).WithAlpha(180); + fill.pattern = Canvas::FillPattern::CHECKERED_B; + fill.zIndex = 1; + break; + } + + default: + fill.color = RgbaColor::FromFloat(1.0f, 1.0f, 1.0f); + pixmap = SS.images[file]; + break; + } + + Canvas::hFill hf = canvas->GetFill(fill); + Vector v[4] = {}; + for(int i = 0; i < 4; i++) { + v[i] = SK.GetEntity(point[i])->PointGetNum(); + } + Vector iu = v[3].Minus(v[0]); + Vector iv = v[1].Minus(v[0]); + + if(how == DrawAs::DEFAULT && pixmap == NULL) { + Canvas::Stroke stroke = Style::Stroke(Style::DRAW_ERROR); + stroke.color = stroke.color.WithAlpha(50); + Canvas::hStroke hs = canvas->GetStroke(stroke); + canvas->DrawLine(v[0], v[2], hs); + canvas->DrawLine(v[1], v[3], hs); + for(int i = 0; i < 4; i++) { + canvas->DrawLine(v[i], v[(i + 1) % 4], hs); + } + } else { + canvas->DrawPixmap(pixmap, v[0], iu, iv, + Point2d::From(0.0, 0.0), Point2d::From(1.0, 1.0), hf); + } + } case Type::FACE_NORMAL_PT: case Type::FACE_XPROD: diff --git a/src/entity.cpp b/src/entity.cpp index 8d579d1..4f4eabe 100644 --- a/src/entity.cpp +++ b/src/entity.cpp @@ -785,7 +785,10 @@ Vector EntityBase::EndpointFinish() const { } else ssassert(false, "Unexpected entity type"); } -void EntityBase::TtfTextGetPointsExprs(ExprVector *eb, ExprVector *ec) const { +void EntityBase::RectGetPointsExprs(ExprVector *eb, ExprVector *ec) const { + ssassert(type == Type::TTF_TEXT || type == Type::IMAGE, + "Unexpected entity type"); + EntityBase *a = SK.GetEntity(point[0]); EntityBase *o = SK.GetEntity(point[1]); @@ -846,6 +849,7 @@ void EntityBase::GenerateEquations(IdList *l) const { break; } + case Type::IMAGE: case Type::TTF_TEXT: { if(SK.GetEntity(point[0])->type != Type::POINT_IN_2D) break; EntityBase *b = SK.GetEntity(point[2]); @@ -854,7 +858,7 @@ void EntityBase::GenerateEquations(IdList *l) const { ExprVector ec = c->PointGetExprsInWorkplane(workplane); ExprVector ebp, ecp; - TtfTextGetPointsExprs(&ebp, &ecp); + RectGetPointsExprs(&ebp, &ecp); ExprVector beq = eb.Minus(ebp); AddEq(l, beq.x, 0); diff --git a/src/file.cpp b/src/file.cpp index 99ac2c7..5b8447a 100644 --- a/src/file.cpp +++ b/src/file.cpp @@ -33,6 +33,7 @@ void SolveSpaceUI::ClearExisting() { SK.entity.Clear(); SK.param.Clear(); + images.clear(); } hGroup SolveSpaceUI::CreateDefaultDrawingGroup() { @@ -131,6 +132,7 @@ const SolveSpaceUI::SaveTable SolveSpaceUI::SAVED[] = { { 'r', "Request.style", 'x', &(SS.sv.r.style) }, { 'r', "Request.str", 'S', &(SS.sv.r.str) }, { 'r', "Request.font", 'S', &(SS.sv.r.font) }, + { 'r', "Request.file", 'P', &(SS.sv.r.file) }, { 'r', "Request.aspectRatio", 'f', &(SS.sv.r.aspectRatio) }, { 'e', "Entity.h.v", 'x', &(SS.sv.e.h.v) }, @@ -139,6 +141,7 @@ const SolveSpaceUI::SaveTable SolveSpaceUI::SAVED[] = { { 'e', "Entity.style", 'x', &(SS.sv.e.style) }, { 'e', "Entity.str", 'S', &(SS.sv.e.str) }, { 'e', "Entity.font", 'S', &(SS.sv.e.font) }, + { 'e', "Entity.file", 'P', &(SS.sv.e.file) }, { 'e', "Entity.point[0].v", 'x', &(SS.sv.e.point[0].v) }, { 'e', "Entity.point[1].v", 'x', &(SS.sv.e.point[1].v) }, { 'e', "Entity.point[2].v", 'x', &(SS.sv.e.point[2].v) }, @@ -408,7 +411,10 @@ void SolveSpaceUI::LoadUsingTable(const Platform::Path &filename, char *key, cha case 'x': sscanf(val, "%x", &u); p->x()= u; break; case 'P': { - p->P() = filename.Parent().Join(Platform::Path::FromPortable(val)); + Platform::Path path = Platform::Path::FromPortable(val); + if(!path.IsEmpty()) { + p->P() = filename.Parent().Join(path).Expand(); + } break; } @@ -537,7 +543,7 @@ bool SolveSpaceUI::LoadFromFile(const Platform::Path &filename, bool canCancel) NewFile(); } } - if(!ReloadAllImported(filename, canCancel)) { + if(!ReloadAllLinked(filename, canCancel)) { return false; } UpgradeLegacyData(); @@ -577,7 +583,7 @@ void SolveSpaceUI::UpgradeLegacyData() { Entity *b = entity.FindById(text->point[2]); Entity *c = entity.FindById(text->point[3]); ExprVector bex, cex; - text->TtfTextGetPointsExprs(&bex, &cex); + text->RectGetPointsExprs(&bex, &cex); b->PointForceParamTo(bex.Eval()); c->PointForceParamTo(cex.Eval()); } @@ -819,11 +825,11 @@ bool SolveSpaceUI::LoadEntitiesFromFile(const Platform::Path &filename, EntityLi return true; } -bool SolveSpaceUI::ReloadAllImported(const Platform::Path &filename, bool canCancel) -{ +bool SolveSpaceUI::ReloadAllLinked(const Platform::Path &saveFile, bool canCancel) { std::map linkMap; allConsistent = false; + for(Group &g : SK.group) { if(g.type != Group::Type::LINKED) continue; @@ -838,10 +844,16 @@ bool SolveSpaceUI::ReloadAllImported(const Platform::Path &filename, bool canCan try_again: if(LoadEntitiesFromFile(g.linkFile, &g.impEntity, &g.impMesh, &g.impShell)) { - // We loaded the data, good. + // We loaded the data, good. Now import its dependencies as well. + for(Entity &e : g.impEntity) { + if(e.type != Entity::Type::IMAGE) continue; + if(!ReloadLinkedImage(g.linkFile, &e.file, canCancel)) { + return false; + } + } } else if(linkMap.count(g.linkFile) == 0) { // The file was moved; prompt the user for its new location. - switch(LocateImportedFileYesNoCancel(g.linkFile.RelativeTo(filename), canCancel)) { + switch(LocateImportedFileYesNoCancel(g.linkFile.RelativeTo(saveFile), canCancel)) { case DIALOG_YES: { Platform::Path newLinkFile; if(GetOpenFile(&newLinkFile, "", SlvsFileFilter)) { @@ -867,6 +879,59 @@ try_again: } } + for(Request &r : SK.request) { + if(r.type != Request::Type::IMAGE) continue; + + if(!ReloadLinkedImage(saveFile, &r.file, canCancel)) { + return false; + } + } + return true; } +bool SolveSpaceUI::ReloadLinkedImage(const Platform::Path &saveFile, + Platform::Path *filename, bool canCancel) { + std::shared_ptr pixmap; + bool promptOpenFile = false; + if(filename->IsEmpty()) { + // We're prompting the user for a new image. + promptOpenFile = true; + } else { + auto image = SS.images.find(*filename); + if(image != SS.images.end()) return true; + + pixmap = Pixmap::ReadPng(*filename); + if(pixmap == NULL) { + // The file was moved; prompt the user for its new location. + switch(LocateImportedFileYesNoCancel(filename->RelativeTo(saveFile), canCancel)) { + case DIALOG_YES: + promptOpenFile = true; + break; + + case DIALOG_NO: + // We don't know where the file is, record it as absent. + break; + + case DIALOG_CANCEL: + return false; + } + } + } + + if(promptOpenFile) { + if(GetOpenFile(filename, "", RasterFileFilter)) { + pixmap = Pixmap::ReadPng(*filename); + if(pixmap == NULL) { + Error("The image '%s' is corrupted.", filename->raw.c_str()); + } + // We know where the file is now, good. + } else if(canCancel) { + return false; + } + } + + // We loaded the data, good. + SS.images[*filename] = pixmap; + return true; +} diff --git a/src/graphicswin.cpp b/src/graphicswin.cpp index e634e9e..83d5f93 100644 --- a/src/graphicswin.cpp +++ b/src/graphicswin.cpp @@ -112,6 +112,7 @@ const GraphicsWindow::MenuEntry GraphicsWindow::menu[] = { { 1, N_("&Bezier Cubic Spline"), Command::CUBIC, 'B', TN, mReq }, { 1, NULL, Command::NONE, 0, TN, NULL }, { 1, N_("&Text in TrueType Font"), Command::TTF_TEXT, 'T', TN, mReq }, +{ 1, N_("&Image"), Command::IMAGE, 0, TN, mReq }, { 1, NULL, Command::NONE, 0, TN, NULL }, { 1, N_("To&ggle Construction"), Command::CONSTRUCTION, 'G', TN, mReq }, { 1, N_("Tangent &Arc at Point"), Command::TANGENT_ARC, S|'A', TN, mReq }, @@ -943,7 +944,8 @@ void GraphicsWindow::MenuEdit(Command id) { break; case Command::REGEN_ALL: - SS.ReloadAllImported(SS.saveFile); + SS.images.clear(); + SS.ReloadAllLinked(SS.saveFile); SS.GenerateAll(SolveSpaceUI::Generate::UNTIL_ACTIVE); SS.ScheduleShowTW(); break; @@ -1015,6 +1017,12 @@ void GraphicsWindow::MenuRequest(Command id) { case Command::WORKPLANE: s = _("click origin of workplane"); goto c; case Command::RECTANGLE: s = _("click one corner of rectangle"); goto c; case Command::TTF_TEXT: s = _("click top left of text"); goto c; + case Command::IMAGE: + if(!SS.ReloadLinkedImage(SS.saveFile, &SS.GW.pending.filename, + /*canCancel=*/true)) { + return; + } + s = _("click top left of image"); goto c; c: SS.GW.pending.operation = GraphicsWindow::Pending::COMMAND; SS.GW.pending.command = id; diff --git a/src/group.cpp b/src/group.cpp index 4671c86..07d63a8 100644 --- a/src/group.cpp +++ b/src/group.cpp @@ -274,7 +274,7 @@ void Group::MenuGroup(Command id) { Group *gg = SK.GetGroup(g.h); if(gg->type == Type::LINKED) { - SS.ReloadAllImported(SS.saveFile); + SS.ReloadAllLinked(SS.saveFile); } gg->clean = false; SS.GW.activeGroup = gg->h; @@ -801,6 +801,7 @@ void Group::CopyEntity(IdList *el, en.style = ep->style; en.str = ep->str; en.font = ep->font; + en.file = ep->file; switch(ep->type) { case Entity::Type::WORKPLANE: diff --git a/src/mouse.cpp b/src/mouse.cpp index 3247cc5..d3dd7c9 100644 --- a/src/mouse.cpp +++ b/src/mouse.cpp @@ -51,7 +51,8 @@ void GraphicsWindow::StartDraggingByEntity(hEntity he) { e->type == Entity::Type::CUBIC || e->type == Entity::Type::CUBIC_PERIODIC || e->type == Entity::Type::CIRCLE || - e->type == Entity::Type::TTF_TEXT) + e->type == Entity::Type::TTF_TEXT || + e->type == Entity::Type::IMAGE) { int pts; EntReqTable::GetEntityInfo(e->type, e->extraPoints, @@ -1146,6 +1147,27 @@ void GraphicsWindow::MouseLeftDown(double mx, double my) { break; } + case Command::IMAGE: { + if(!SS.GW.LockedInWorkplane()) { + Error(_("Can't draw image in 3d; first, activate a workplane " + "with Sketch -> In Workplane.")); + ClearSuper(); + break; + } + hr = AddRequest(Request::Type::IMAGE); + AddToPending(hr); + Request *r = SK.GetRequest(hr); + r->file = pending.filename; + + SK.GetEntity(hr.entity(1))->PointForceTo(v); + SK.GetEntity(hr.entity(2))->PointForceTo(v); + + pending.operation = Pending::DRAGGING_NEW_POINT; + pending.point = hr.entity(2); + pending.description = "click to place bottom left of image"; + break; + } + case Command::COMMENT: { ClearSuper(); Constraint c = {}; diff --git a/src/platform/climain.cpp b/src/platform/climain.cpp index 52881ac..af8d7b0 100644 --- a/src/platform/climain.cpp +++ b/src/platform/climain.cpp @@ -76,7 +76,7 @@ File formats: export-wireframe:%s export-mesh:%s export-surfaces:%s -)", FormatListFromFileFilter(PngFileFilter).c_str(), +)", FormatListFromFileFilter(RasterFileFilter).c_str(), FormatListFromFileFilter(VectorFileFilter).c_str(), FormatListFromFileFilter(Vector3dFileFilter).c_str(), FormatListFromFileFilter(MeshFileFilter).c_str(), diff --git a/src/render/render.cpp b/src/render/render.cpp index a9e5e27..16a2cc8 100644 --- a/src/render/render.cpp +++ b/src/render/render.cpp @@ -441,7 +441,7 @@ void ObjectPicker::DrawFaces(const SMesh &m, const std::vector &faces, void ObjectPicker::DrawPixmap(std::shared_ptr pm, const Vector &o, const Vector &u, const Vector &v, const Point2d &ta, const Point2d &tb, Canvas::hFill hcf) { - ssassert(false, "Not implemented"); + DrawQuad(o, o.Plus(u), o.Plus(u).Plus(v), o.Plus(v), hcf); } bool ObjectPicker::Pick(std::function drawFn) { diff --git a/src/render/rendergl2.cpp b/src/render/rendergl2.cpp index 5aa7110..5830199 100644 --- a/src/render/rendergl2.cpp +++ b/src/render/rendergl2.cpp @@ -769,11 +769,11 @@ public: virtual Canvas::Layer GetLayer() const override { return stroke.layer; }; virtual int GetZIndex() const override { return stroke.zIndex; }; - static std::shared_ptr Create(OpenGl2Renderer *renderer, const SIndexedMesh &im, + static std::shared_ptr Create(OpenGl2Renderer *renderer, const SIndexedMesh &mesh, Canvas::Stroke *stroke) { PointDrawCall *dc = new PointDrawCall(); dc->stroke = *stroke; - dc->handle = renderer->imeshRenderer.Add(im); + dc->handle = renderer->imeshRenderer.Add(mesh); return std::shared_ptr(dc); } @@ -788,6 +788,42 @@ public: } }; +class PixmapDrawCall : public DrawCall { +public: + // Key + Canvas::Fill fill; + // Data + IndexedMeshRenderer::Handle handle; + + virtual Canvas::Layer GetLayer() const override { return fill.layer; }; + virtual int GetZIndex() const override { return fill.zIndex; }; + + static std::shared_ptr Create(OpenGl2Renderer *renderer, const SIndexedMesh &mesh, + Canvas::Fill *fill) { + PixmapDrawCall *dc = new PixmapDrawCall(); + dc->fill = *fill; + dc->handle = renderer->imeshRenderer.Add(mesh); + return std::shared_ptr(dc); + } + + void Draw(OpenGl2Renderer *renderer) override { + ssglDepthRange(fill.layer, fill.zIndex); + if(fill.pattern != Canvas::FillPattern::SOLID) { + renderer->SelectMask(fill.pattern); + } else if(fill.texture) { + renderer->SelectTexture(fill.texture); + } else { + renderer->SelectMask(Canvas::FillPattern::SOLID); + } + renderer->imeshRenderer.UseFilled(fill); + renderer->imeshRenderer.Draw(handle); + } + + void Remove(OpenGl2Renderer *renderer) override { + renderer->imeshRenderer.Remove(handle); + } +}; + class MeshDrawCall : public DrawCall { public: // Key @@ -813,22 +849,22 @@ public: return std::shared_ptr(dc); } - void DrawFace(OpenGl2Renderer *renderer, GLenum cullFace, Canvas::Fill *fill) { + void DrawFace(OpenGl2Renderer *renderer, GLenum cullFace, const Canvas::Fill &fill) { glCullFace(cullFace); - ssglDepthRange(fill->layer, fill->zIndex); - if(fill->pattern != Canvas::FillPattern::SOLID) { - renderer->SelectMask(fill->pattern); - } else if(fill->texture) { - renderer->SelectTexture(fill->texture); + ssglDepthRange(fill.layer, fill.zIndex); + if(fill.pattern != Canvas::FillPattern::SOLID) { + renderer->SelectMask(fill.pattern); + } else if(fill.texture) { + renderer->SelectTexture(fill.texture); } else { renderer->SelectMask(Canvas::FillPattern::SOLID); } if(isShaded) { renderer->meshRenderer.UseShaded(renderer->lighting); } else { - renderer->meshRenderer.UseFilled(*fill); + renderer->meshRenderer.UseFilled(fill); } - renderer->meshRenderer.Draw(handle, /*useColors=*/fill->color.IsEmpty(), fill->color); + renderer->meshRenderer.Draw(handle, /*useColors=*/fill.color.IsEmpty(), fill.color); } void Draw(OpenGl2Renderer *renderer) override { @@ -836,8 +872,8 @@ public: glEnable(GL_CULL_FACE); if(hasFillBack) - DrawFace(renderer, GL_FRONT, &fillBack); - DrawFace(renderer, GL_BACK, &fillFront); + DrawFace(renderer, GL_FRONT, fillBack); + DrawFace(renderer, GL_BACK, fillFront); glDisable(GL_POLYGON_OFFSET_FILL); glDisable(GL_CULL_FACE); @@ -977,7 +1013,14 @@ public: void DrawPixmap(std::shared_ptr pm, const Vector &o, const Vector &u, const Vector &v, const Point2d &ta, const Point2d &tb, hFill hcf) override { - ssassert(false, "Not implemented"); + Fill fill = *fills.FindById(hcf); + fill.texture = pm; + hcf = GetFill(fill); + + SIndexedMesh mesh = {}; + mesh.AddPixmap(o, u, v, ta, tb); + drawCalls.emplace(PixmapDrawCall::Create(renderer, mesh, fills.FindByIdNoOops(hcf))); + mesh.Clear(); } void InvalidatePixmap(std::shared_ptr pm) override { diff --git a/src/request.cpp b/src/request.cpp index 0fdd096..5046c01 100644 --- a/src/request.cpp +++ b/src/request.cpp @@ -30,6 +30,7 @@ static const EntReqMapping EntReqMap[] = { { Request::Type::CIRCLE, Entity::Type::CIRCLE, 1, false, true, true }, { Request::Type::ARC_OF_CIRCLE, Entity::Type::ARC_OF_CIRCLE, 3, false, true, false }, { Request::Type::TTF_TEXT, Entity::Type::TTF_TEXT, 4, false, true, false }, +{ Request::Type::IMAGE, Entity::Type::IMAGE, 4, false, true, false }, }; static void CopyEntityInfo(const EntReqMapping *te, int extraPoints, @@ -102,6 +103,20 @@ void Request::Generate(IdList *entity, break; } + case Type::IMAGE: { + auto image = SS.images.find(file); + if(image != SS.images.end()) { + std::shared_ptr pixmap = (*image).second; + if(pixmap != NULL) { + aspectRatio = (double)pixmap->width / (double)pixmap->height; + } + } + if(EXACT(aspectRatio == 0.0)) { + aspectRatio = 1.0; + } + break; + } + default: // most requests don't do anything else break; } @@ -118,6 +133,7 @@ void Request::Generate(IdList *entity, e.construction = construction; e.str = str; e.font = font; + e.file = file; e.aspectRatio = aspectRatio; e.h = h.entity(0); @@ -205,8 +221,9 @@ std::string Request::DescriptionString() const { case Type::CUBIC: s = "cubic-bezier"; break; case Type::CUBIC_PERIODIC: s = "periodic-cubic"; break; case Type::CIRCLE: s = "circle"; break; - case Type::ARC_OF_CIRCLE: s = "arc-of-circle;"; break; + case Type::ARC_OF_CIRCLE: s = "arc-of-circle"; break; case Type::TTF_TEXT: s = "ttf-text"; break; + case Type::IMAGE: s = "image"; break; } } ssassert(s != NULL, "Unexpected request type"); diff --git a/src/sketch.h b/src/sketch.h index 35c2602..c7950c3 100644 --- a/src/sketch.h +++ b/src/sketch.h @@ -312,7 +312,8 @@ public: CUBIC_PERIODIC = 301, CIRCLE = 400, ARC_OF_CIRCLE = 500, - TTF_TEXT = 600 + TTF_TEXT = 600, + IMAGE = 700 }; Request::Type type; @@ -326,6 +327,7 @@ public: std::string str; std::string font; + Platform::Path file; double aspectRatio; static hParam AddParam(ParamList *param, hParam hp); @@ -375,7 +377,8 @@ public: CUBIC_PERIODIC = 12001, CIRCLE = 13000, ARC_OF_CIRCLE = 14000, - TTF_TEXT = 15000 + TTF_TEXT = 15000, + IMAGE = 16000 }; Type type; @@ -400,6 +403,7 @@ public: std::string str; std::string font; + Platform::Path file; double aspectRatio; // For entities that are derived by a transformation, the number of @@ -476,7 +480,7 @@ public: Vector EndpointStart() const; Vector EndpointFinish() const; - void TtfTextGetPointsExprs(ExprVector *eap, ExprVector *ebp) const; + void RectGetPointsExprs(ExprVector *eap, ExprVector *ebp) const; void AddEq(IdList *l, Expr *expr, int index) const; void GenerateEquations(IdList *l) const; diff --git a/src/solvespace.cpp b/src/solvespace.cpp index 67f7634..9021a1e 100644 --- a/src/solvespace.cpp +++ b/src/solvespace.cpp @@ -457,7 +457,7 @@ void SolveSpaceUI::MenuFile(Command id) { case Command::EXPORT_PNG: { Platform::Path exportFile = SS.saveFile; - if(!GetSaveFile(&exportFile, "", PngFileFilter)) break; + if(!GetSaveFile(&exportFile, "", RasterFileFilter)) break; SS.ExportAsPngTo(exportFile); break; } diff --git a/src/solvespace.h b/src/solvespace.h index 7718191..82c37d4 100644 --- a/src/solvespace.h +++ b/src/solvespace.h @@ -620,6 +620,11 @@ public: } UndoStack; UndoStack undo; UndoStack redo; + + std::map, Platform::PathLess> images; + bool ReloadLinkedImage(const Platform::Path &saveFile, Platform::Path *filename, + bool canCancel); + void UndoEnableMenus(); void UndoRemember(); void UndoUndo(); @@ -743,7 +748,7 @@ public: void UpgradeLegacyData(); bool LoadEntitiesFromFile(const Platform::Path &filename, EntityList *le, SMesh *m, SShell *sh); - bool ReloadAllImported(const Platform::Path &filename, bool canCancel = false); + bool ReloadAllLinked(const Platform::Path &filename, bool canCancel = false); // And the various export options void ExportAsPngTo(const Platform::Path &filename); void ExportMeshTo(const Platform::Path &filename); diff --git a/src/style.cpp b/src/style.cpp index eb30516..c08f7ed 100644 --- a/src/style.cpp +++ b/src/style.cpp @@ -394,7 +394,7 @@ void TextWindow::ScreenBackgroundImage(int link, uint32_t v) { if(link == 'l') { Platform::Path bgImageFile; - if(GetOpenFile(&bgImageFile, "", PngFileFilter)) { + if(GetOpenFile(&bgImageFile, "", RasterFileFilter)) { FILE *f = OpenFile(bgImageFile, "rb"); if(f) { SS.bgImage.pixmap = Pixmap::ReadPng(f); diff --git a/src/textscreens.cpp b/src/textscreens.cpp index 49ba0ac..79fc46c 100644 --- a/src/textscreens.cpp +++ b/src/textscreens.cpp @@ -327,7 +327,7 @@ void TextWindow::ShowGroupInfo() { } } else if(g->type == Group::Type::LINKED) { Printf(true, " %Ftlink geometry from file%E"); - Platform::Path relativePath =g->linkFile.RelativeTo(SS.saveFile.Parent()); + Platform::Path relativePath = g->linkFile.RelativeTo(SS.saveFile.Parent()); if(relativePath.IsEmpty()) { Printf(false, "%Ba '%s'", g->linkFile.raw.c_str()); } else { diff --git a/src/toolbar.cpp b/src/toolbar.cpp index 6000be7..b467d44 100644 --- a/src/toolbar.cpp +++ b/src/toolbar.cpp @@ -24,6 +24,8 @@ static ToolIcon Toolbar[] = { N_("Sketch arc of a circle"), {} }, { "text", Command::TTF_TEXT, N_("Sketch curves from text in a TrueType font"), {} }, + { "image", Command::IMAGE, + N_("Sketch image from a file"), {} }, { "tangent-arc", Command::TANGENT_ARC, N_("Create tangent arc at selected point"), {} }, { "bezier", Command::CUBIC, diff --git a/src/ui.h b/src/ui.h index bcf1d23..bdef6ce 100644 --- a/src/ui.h +++ b/src/ui.h @@ -70,7 +70,7 @@ const FileFilter SlvsFileFilter[] = { { NULL, {} } }; // PNG format bitmap -const FileFilter PngFileFilter[] = { +const FileFilter RasterFileFilter[] = { { N_("PNG file"), { "png" } }, { NULL, {} } }; @@ -173,6 +173,7 @@ enum class Command : uint32_t { RECTANGLE, CUBIC, TTF_TEXT, + IMAGE, SPLIT_CURVES, TANGENT_ARC, CONSTRUCTION, @@ -712,6 +713,7 @@ public: hConstraint constraint; const char *description; + Platform::Path filename; bool hasSuggestion; Constraint::Type suggestion; diff --git a/src/undoredo.cpp b/src/undoredo.cpp index be1fef2..1739caf 100644 --- a/src/undoredo.cpp +++ b/src/undoredo.cpp @@ -138,7 +138,7 @@ void SolveSpaceUI::PopOntoCurrentFrom(UndoStack *uk) { // sketch just changed a lot. SS.GW.ClearSuper(); SS.TW.ClearSuper(); - SS.ReloadAllImported(SS.saveFile); + SS.ReloadAllLinked(SS.saveFile); SS.GenerateAll(SolveSpaceUI::Generate::ALL); SS.ScheduleShowTW(); diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index bac0fa7..bb70d97 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -54,6 +54,7 @@ set(testsuite_SOURCES request/cubic/test.cpp request/cubic_periodic/test.cpp request/datum_point/test.cpp + request/image/test.cpp request/line_segment/test.cpp request/ttf_text/test.cpp group/link/test.cpp diff --git a/test/request/image/drawing.png b/test/request/image/drawing.png new file mode 100644 index 0000000..56bedb7 Binary files /dev/null and b/test/request/image/drawing.png differ diff --git a/test/request/image/linked.slvs b/test/request/image/linked.slvs new file mode 100644 index 0000000..4819058 Binary files /dev/null and b/test/request/image/linked.slvs differ diff --git a/test/request/image/normal.slvs b/test/request/image/normal.slvs new file mode 100644 index 0000000..dab1385 Binary files /dev/null and b/test/request/image/normal.slvs differ diff --git a/test/request/image/test.cpp b/test/request/image/test.cpp new file mode 100644 index 0000000..da0954e --- /dev/null +++ b/test/request/image/test.cpp @@ -0,0 +1,14 @@ +#include "harness.h" + +TEST_CASE(normal_roundtrip) { + CHECK_LOAD("normal.slvs"); + // Can't render images through cairo for now. + // CHECK_RENDER("normal.png"); + CHECK_SAVE("normal.slvs"); +} + +TEST_CASE(linked_roundtrip) { + CHECK_LOAD("linked.slvs"); + // CHECK_RENDER("linked.png"); + CHECK_SAVE("linked.slvs"); +}