diff --git a/.clang-format b/.clang-format index 451d630b3..dbd236ed2 100644 --- a/.clang-format +++ b/.clang-format @@ -1,148 +1,157 @@ --- -Language: Cpp -# BasedOnStyle: LLVM +Language: Cpp AccessModifierOffset: -2 AlignAfterOpenBracket: Align +#AlignArrayOfStructures: None AlignConsecutiveMacros: false AlignConsecutiveAssignments: false -# AlignConsecutiveBitFields: false # not available in clang-format 10 +#AlignConsecutiveBitFields: None AlignConsecutiveDeclarations: false AlignEscapedNewlines: DontAlign -AlignOperands: true +AlignOperands: Align AlignTrailingComments: false -AllowAllArgumentsOnNextLine: false -AllowAllConstructorInitializersOnNextLine: true -AllowAllParametersOfDeclarationOnNextLine: false -# AllowShortEnumsOnASingleLine: true # not available in clang-format 10 on ubuntu. +AllowAllArgumentsOnNextLine: true +AllowAllParametersOfDeclarationOnNextLine: true +AllowShortEnumsOnASingleLine: true +AllowShortBlocksOnASingleLine: false AllowShortCaseLabelsOnASingleLine: false AllowShortFunctionsOnASingleLine: None AllowShortLambdasOnASingleLine: All AllowShortIfStatementsOnASingleLine: Never AllowShortLoopsOnASingleLine: false -# AlwaysBreakAfterDefinitionReturnType: None +AlwaysBreakAfterDefinitionReturnType: None AlwaysBreakAfterReturnType: None AlwaysBreakBeforeMultilineStrings: false -AlwaysBreakTemplateDeclarations: MultiLine -BinPackArguments: false -BinPackParameters: false +AlwaysBreakTemplateDeclarations: No +#AttributeMacros: +# - __capability +BinPackArguments: true +BinPackParameters: true +#BreakBeforeBinaryOperators: NonAssignment +BreakBeforeBraces: Custom BraceWrapping: - AfterCaseLabel: false - AfterClass: true + AfterCaseLabel: false + AfterClass: true AfterControlStatement: MultiLine - AfterEnum: false - AfterFunction: true - AfterNamespace: true - # AfterObjCDeclaration: false - AfterStruct: true - AfterUnion: true + AfterEnum: false + AfterFunction: true + AfterNamespace: true + AfterStruct: true + AfterUnion: true AfterExternBlock: true - BeforeCatch: false - BeforeElse: false - # BeforeLambdaBody: false # not available in clang-format 10 - # BeforeWhile: false # not available in clang-format 10 - IndentBraces: false + BeforeCatch: false + BeforeElse: false + BeforeLambdaBody: false + BeforeWhile: false + IndentBraces: false SplitEmptyFunction: true SplitEmptyRecord: true SplitEmptyNamespace: true -BreakBeforeBinaryOperators: All -BreakBeforeBraces: Linux -# BreakBeforeInheritanceComma: false +BreakBeforeBinaryOperators: NonAssignment +#BreakBeforeConceptDeclarations: true +BreakBeforeBraces: Custom +BreakBeforeInheritanceComma: false BreakInheritanceList: BeforeComma BreakBeforeTernaryOperators: true -# BreakConstructorInitializersBeforeComma: false -BreakConstructorInitializers: BeforeColon -# BreakAfterJavaFieldAnnotations: false +BreakConstructorInitializersBeforeComma: false +BreakConstructorInitializers: BeforeComma +BreakAfterJavaFieldAnnotations: false BreakStringLiterals: true -ColumnLimit: 100 -CommentPragmas: '^ IWYU pragma:' +ColumnLimit: 120 +CommentPragmas: '^ IWYU pragma:' +#QualifierAlignment: Leave CompactNamespaces: false -ConstructorInitializerAllOnOneLineOrOnePerLine: false -ConstructorInitializerIndentWidth: 4 +ConstructorInitializerIndentWidth: 2 ContinuationIndentWidth: 4 Cpp11BracedListStyle: true DeriveLineEnding: true DerivePointerAlignment: false -DisableFormat: false -# ExperimentalAutoDetectBinPacking: false +DisableFormat: false +#EmptyLineAfterAccessModifier: Never +#EmptyLineBeforeAccessModifier: LogicalBlock +ExperimentalAutoDetectBinPacking: false +#PackConstructorInitializers: NextLine +BasedOnStyle: '' +ConstructorInitializerAllOnOneLineOrOnePerLine: true +AllowAllConstructorInitializersOnNextLine: true FixNamespaceComments: true -ForEachMacros: - - foreach - - Q_FOREACH - - BOOST_FOREACH -IncludeBlocks: Preserve -IncludeCategories: - - Regex: '^"(llvm|llvm-c|clang|clang-c)/' - Priority: 2 - SortPriority: 0 - - Regex: '^(<|"(gtest|gmock|isl|json)/)' - Priority: 3 - SortPriority: 0 - - Regex: '.*' - Priority: 1 - SortPriority: 0 +ForEachMacros: [] +#IfMacros: [] +IncludeBlocks: Preserve +IncludeCategories: [] IncludeIsMainRegex: '(Test)?$' IncludeIsMainSourceRegex: '' +#IndentAccessModifiers: false IndentCaseLabels: false -# IndentCaseBlocks: false # not available in clang-format 10 -IndentGotoLabels: false +IndentCaseBlocks: false +IndentGotoLabels: true IndentPPDirectives: AfterHash -# IndentExternBlock: AfterExternBlock # not available in clang-format 10 -IndentWidth: 2 +IndentExternBlock: AfterExternBlock +#IndentRequires: false +IndentWidth: 2 IndentWrappedFunctionNames: false -# InsertTrailingCommas: Wrapped # not available in clang-format 10 -# JavaScriptQuotes: Leave -# JavaScriptWrapImports: true -KeepEmptyLinesAtTheStartOfBlocks: false +InsertTrailingCommas: None +KeepEmptyLinesAtTheStartOfBlocks: true +#LambdaBodyIndentation: Signature MacroBlockBegin: '' -MacroBlockEnd: '' +MacroBlockEnd: '' MaxEmptyLinesToKeep: 1 NamespaceIndentation: None -# ObjCBinPackProtocolList: Auto -# ObjCBlockIndentWidth: 2 -# ObjCBreakBeforeNestedBlockParam: true -# ObjCSpaceAfterProperty: false -# ObjCSpaceBeforeProtocolList: true -PenaltyBreakAssignment: 2 +#PenaltyBreakAssignment: 2 PenaltyBreakBeforeFirstCallParameter: 19 PenaltyBreakComment: 300 PenaltyBreakFirstLessLess: 120 +#PenaltyBreakOpenParenthesis: 0 PenaltyBreakString: 1000 PenaltyBreakTemplateDeclaration: 10 PenaltyExcessCharacter: 1000000 PenaltyReturnTypeOnItsOwnLine: 60 +#PenaltyIndentedWhitespace: 0 PointerAlignment: Left -ReflowComments: true -SortIncludes: true +#PPIndentWidth: -1 +#ReferenceAlignment: Left +ReflowComments: false +#RemoveBracesLLVM: false +#SeparateDefinitionBlocks: Leave +#ShortNamespaceLines: 1 +#SortIncludes: CaseSensitive SortUsingDeclarations: true -SpaceAfterCStyleCast: false +SpaceAfterCStyleCast: true SpaceAfterLogicalNot: false SpaceAfterTemplateKeyword: false SpaceBeforeAssignmentOperators: true +#SpaceBeforeCaseColon: false SpaceBeforeCpp11BracedList: false SpaceBeforeCtorInitializerColon: true SpaceBeforeInheritanceColon: true SpaceBeforeParens: ControlStatements +#SpaceBeforeParensOptions: + #AfterControlStatements: true + #AfterForeachMacros: true + #AfterFunctionDefinitionName: false + #AfterFunctionDeclarationName: false + #AfterIfMacros: true + #AfterOverloadedOperator: false + #BeforeNonEmptyParentheses: false +#SpaceAroundPointerQualifiers: Default SpaceBeforeRangeBasedForLoopColon: true SpaceInEmptyBlock: false SpaceInEmptyParentheses: false SpacesBeforeTrailingComments: 2 -SpacesInAngles: false +#SpacesInAngles: Never SpacesInConditionalStatement: false SpacesInContainerLiterals: false SpacesInCStyleCastParentheses: false +#SpacesInLineCommentPrefix: +# Minimum: 1 +# Maximum: -1 SpacesInParentheses: false SpacesInSquareBrackets: false SpaceBeforeSquareBrackets: false -Standard: Latest -StatementMacros: - - Q_UNUSED - - QT_REQUIRE_VERSION -TabWidth: 2 -UseCRLF: false -UseTab: Never -# WhitespaceSensitiveMacros: # not available in clang-format 10 on ubuntu -# - STRINGIZE # not available in clang-format 10 on ubuntu -# - PP_STRINGIZE # not available in clang-format 10 on ubuntu -# - BOOST_PP_STRINGIZE # not available in clang-format 10 on ubuntu +#BitFieldColonSpacing: Both +Standard: Latest +TabWidth: 8 +UseCRLF: false +UseTab: Never +WhitespaceSensitiveMacros: [] ... - diff --git a/CMakeLists.txt b/CMakeLists.txt index 02ef64e06..20f81b463 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -16,6 +16,8 @@ option(USE_QT_5_12 "Allow to use Qt 5.12. Set this option to true for static ana Builds with this configuration are not supposed to be run." OFF ) +option(DRAW_POINT_IDS "Draw the id of path points next to the point." OFF) +option(PATH_DRAW_DIRECTION "Draws little arrows to indicate begin, end and direction of path segments." OFF) option(WERROR "Error on compiler warnings. Not available for MSVC." ON) if (USE_QT_5_12) @@ -28,7 +30,6 @@ find_package(PkgConfig) find_package(Python3 3.6 REQUIRED COMPONENTS Interpreter Development) find_package(2Geom REQUIRED) find_package(fmt 8.0.0 REQUIRED) -find_package(Boost 1.65 REQUIRED COMPONENTS graph) set(python_major_minor "python${Python3_VERSION_MAJOR}.${Python3_VERSION_MINOR}") set(python_major "python${Python3_VERSION_MAJOR}") @@ -98,7 +99,6 @@ target_link_libraries(libommpfritt poppler-qt5) target_link_libraries(libommpfritt -lpthread -lm) target_link_libraries(libommpfritt 2Geom::2geom) target_link_libraries(libommpfritt fmt::fmt) -target_link_libraries(libommpfritt Boost::graph) target_link_libraries(ommpfritt libommpfritt) if (WIN32) diff --git a/icons/icons.omm b/icons/icons.omm index d3a91c94a..c3b1ac036 100644 --- a/icons/icons.omm +++ b/icons/icons.omm @@ -137,6 +137,15 @@ "animated": false, "key": "edit-style", "type": "TriggerProperty" + }, + { + "animatable": true, + "animated": false, + "key": "facelist", + "type": "FaceListProperty", + "value": { + "path": 0 + } } ], "type": "StyleTag" @@ -171,6 +180,15 @@ "animated": false, "key": "edit-style", "type": "TriggerProperty" + }, + { + "animatable": true, + "animated": false, + "key": "facelist", + "type": "FaceListProperty", + "value": { + "path": 0 + } } ], "type": "StyleTag" @@ -300,6 +318,15 @@ "animated": false, "key": "edit-style", "type": "TriggerProperty" + }, + { + "animatable": true, + "animated": false, + "key": "facelist", + "type": "FaceListProperty", + "value": { + "path": 0 + } } ], "type": "StyleTag" @@ -334,6 +361,15 @@ "animated": false, "key": "edit-style", "type": "TriggerProperty" + }, + { + "animatable": true, + "animated": false, + "key": "facelist", + "type": "FaceListProperty", + "value": { + "path": 0 + } } ], "type": "StyleTag" @@ -471,6 +507,15 @@ "animated": false, "key": "edit-style", "type": "TriggerProperty" + }, + { + "animatable": true, + "animated": false, + "key": "facelist", + "type": "FaceListProperty", + "value": { + "path": 0 + } } ], "type": "StyleTag" @@ -605,6 +650,15 @@ "animated": false, "key": "edit-style", "type": "TriggerProperty" + }, + { + "animatable": true, + "animated": false, + "key": "facelist", + "type": "FaceListProperty", + "value": { + "path": 0 + } } ], "type": "StyleTag" @@ -740,6 +794,15 @@ "animated": false, "key": "edit-style", "type": "TriggerProperty" + }, + { + "animatable": true, + "animated": false, + "key": "facelist", + "type": "FaceListProperty", + "value": { + "path": 0 + } } ], "type": "StyleTag" @@ -774,6 +837,15 @@ "animated": false, "key": "edit-style", "type": "TriggerProperty" + }, + { + "animatable": true, + "animated": false, + "key": "facelist", + "type": "FaceListProperty", + "value": { + "path": 0 + } } ], "type": "StyleTag" @@ -900,6 +972,15 @@ "animated": false, "key": "edit-style", "type": "TriggerProperty" + }, + { + "animatable": true, + "animated": false, + "key": "facelist", + "type": "FaceListProperty", + "value": { + "path": 0 + } } ], "type": "StyleTag" @@ -1219,6 +1300,15 @@ "animated": false, "key": "edit-style", "type": "TriggerProperty" + }, + { + "animatable": true, + "animated": false, + "key": "facelist", + "type": "FaceListProperty", + "value": { + "path": 0 + } } ], "type": "StyleTag" @@ -1396,6 +1486,15 @@ "animated": false, "key": "edit-style", "type": "TriggerProperty" + }, + { + "animatable": true, + "animated": false, + "key": "facelist", + "type": "FaceListProperty", + "value": { + "path": 0 + } } ], "type": "StyleTag" @@ -1629,6 +1728,15 @@ "animated": false, "key": "edit-style", "type": "TriggerProperty" + }, + { + "animatable": true, + "animated": false, + "key": "facelist", + "type": "FaceListProperty", + "value": { + "path": 0 + } } ], "type": "StyleTag" @@ -1762,6 +1870,15 @@ "animated": false, "key": "edit-style", "type": "TriggerProperty" + }, + { + "animatable": true, + "animated": false, + "key": "facelist", + "type": "FaceListProperty", + "value": { + "path": 0 + } } ], "type": "StyleTag" @@ -1977,6 +2094,15 @@ "animated": false, "key": "edit-style", "type": "TriggerProperty" + }, + { + "animatable": true, + "animated": false, + "key": "facelist", + "type": "FaceListProperty", + "value": { + "path": 0 + } } ], "type": "StyleTag" @@ -2222,6 +2348,15 @@ "animated": false, "key": "edit-style", "type": "TriggerProperty" + }, + { + "animatable": true, + "animated": false, + "key": "facelist", + "type": "FaceListProperty", + "value": { + "path": 0 + } } ], "type": "StyleTag" @@ -2373,6 +2508,15 @@ "animated": false, "key": "edit-style", "type": "TriggerProperty" + }, + { + "animatable": true, + "animated": false, + "key": "facelist", + "type": "FaceListProperty", + "value": { + "path": 0 + } } ], "type": "StyleTag" @@ -2522,6 +2666,15 @@ "animated": false, "key": "edit-style", "type": "TriggerProperty" + }, + { + "animatable": true, + "animated": false, + "key": "facelist", + "type": "FaceListProperty", + "value": { + "path": 0 + } } ], "type": "StyleTag" @@ -2733,6 +2886,15 @@ "animated": false, "key": "edit-style", "type": "TriggerProperty" + }, + { + "animatable": true, + "animated": false, + "key": "facelist", + "type": "FaceListProperty", + "value": { + "path": 0 + } } ], "type": "StyleTag" @@ -2776,6 +2938,15 @@ "animated": false, "key": "edit-style", "type": "TriggerProperty" + }, + { + "animatable": true, + "animated": false, + "key": "facelist", + "type": "FaceListProperty", + "value": { + "path": 0 + } } ], "type": "StyleTag" @@ -3054,6 +3225,15 @@ "animated": false, "key": "edit-style", "type": "TriggerProperty" + }, + { + "animatable": true, + "animated": false, + "key": "facelist", + "type": "FaceListProperty", + "value": { + "path": 0 + } } ], "type": "StyleTag" @@ -3192,6 +3372,15 @@ "animated": false, "key": "edit-style", "type": "TriggerProperty" + }, + { + "animatable": true, + "animated": false, + "key": "facelist", + "type": "FaceListProperty", + "value": { + "path": 0 + } } ], "type": "StyleTag" @@ -3578,6 +3767,15 @@ "animated": false, "key": "edit-style", "type": "TriggerProperty" + }, + { + "animatable": true, + "animated": false, + "key": "facelist", + "type": "FaceListProperty", + "value": { + "path": 0 + } } ], "type": "StyleTag" @@ -3821,6 +4019,15 @@ "animated": false, "key": "edit-style", "type": "TriggerProperty" + }, + { + "animatable": true, + "animated": false, + "key": "facelist", + "type": "FaceListProperty", + "value": { + "path": 0 + } } ], "type": "StyleTag" @@ -3876,7 +4083,7 @@ "key": "scale", "type": "FloatVectorProperty", "value": [ - 1.0000000000003677, + 1.0000000000003681, 0.9999999999999004 ] }, @@ -4010,6 +4217,15 @@ "animated": false, "key": "edit-style", "type": "TriggerProperty" + }, + { + "animatable": true, + "animated": false, + "key": "facelist", + "type": "FaceListProperty", + "value": { + "path": 0 + } } ], "type": "StyleTag" @@ -4227,6 +4443,15 @@ "animated": false, "key": "edit-style", "type": "TriggerProperty" + }, + { + "animatable": true, + "animated": false, + "key": "facelist", + "type": "FaceListProperty", + "value": { + "path": 0 + } } ], "type": "StyleTag" @@ -4639,6 +4864,15 @@ "animated": false, "key": "edit-style", "type": "TriggerProperty" + }, + { + "animatable": true, + "animated": false, + "key": "facelist", + "type": "FaceListProperty", + "value": { + "path": 0 + } } ], "type": "StyleTag" @@ -4878,6 +5112,15 @@ "animated": false, "key": "edit-style", "type": "TriggerProperty" + }, + { + "animatable": true, + "animated": false, + "key": "facelist", + "type": "FaceListProperty", + "value": { + "path": 0 + } } ], "type": "StyleTag" @@ -5394,6 +5637,15 @@ "animated": false, "key": "edit-style", "type": "TriggerProperty" + }, + { + "animatable": true, + "animated": false, + "key": "facelist", + "type": "FaceListProperty", + "value": { + "path": 0 + } } ], "type": "StyleTag" @@ -5594,6 +5846,15 @@ "animated": false, "key": "edit-style", "type": "TriggerProperty" + }, + { + "animatable": true, + "animated": false, + "key": "facelist", + "type": "FaceListProperty", + "value": { + "path": 0 + } } ], "type": "StyleTag" @@ -5637,6 +5898,15 @@ "animated": false, "key": "edit-style", "type": "TriggerProperty" + }, + { + "animatable": true, + "animated": false, + "key": "facelist", + "type": "FaceListProperty", + "value": { + "path": 0 + } } ], "type": "StyleTag" @@ -5848,6 +6118,15 @@ "animated": false, "key": "edit-style", "type": "TriggerProperty" + }, + { + "animatable": true, + "animated": false, + "key": "facelist", + "type": "FaceListProperty", + "value": { + "path": 0 + } } ], "type": "StyleTag" @@ -6074,6 +6353,15 @@ "animated": false, "key": "edit-style", "type": "TriggerProperty" + }, + { + "animatable": true, + "animated": false, + "key": "facelist", + "type": "FaceListProperty", + "value": { + "path": 0 + } } ], "type": "StyleTag" @@ -6211,6 +6499,15 @@ "animated": false, "key": "edit-style", "type": "TriggerProperty" + }, + { + "animatable": true, + "animated": false, + "key": "facelist", + "type": "FaceListProperty", + "value": { + "path": 0 + } } ], "type": "StyleTag" @@ -6546,6 +6843,15 @@ "animated": false, "key": "edit-style", "type": "TriggerProperty" + }, + { + "animatable": true, + "animated": false, + "key": "facelist", + "type": "FaceListProperty", + "value": { + "path": 0 + } } ], "type": "StyleTag" @@ -6683,6 +6989,15 @@ "animated": false, "key": "edit-style", "type": "TriggerProperty" + }, + { + "animatable": true, + "animated": false, + "key": "facelist", + "type": "FaceListProperty", + "value": { + "path": 0 + } } ], "type": "StyleTag" @@ -7034,6 +7349,15 @@ "animated": false, "key": "edit-style", "type": "TriggerProperty" + }, + { + "animatable": true, + "animated": false, + "key": "facelist", + "type": "FaceListProperty", + "value": { + "path": 0 + } } ], "type": "StyleTag" @@ -7305,6 +7629,15 @@ "animated": false, "key": "edit-style", "type": "TriggerProperty" + }, + { + "animatable": true, + "animated": false, + "key": "facelist", + "type": "FaceListProperty", + "value": { + "path": 0 + } } ], "type": "StyleTag" @@ -7482,6 +7815,15 @@ "animated": false, "key": "edit-style", "type": "TriggerProperty" + }, + { + "animatable": true, + "animated": false, + "key": "facelist", + "type": "FaceListProperty", + "value": { + "path": 0 + } } ], "type": "StyleTag" @@ -7824,6 +8166,15 @@ "animated": false, "key": "edit-style", "type": "TriggerProperty" + }, + { + "animatable": true, + "animated": false, + "key": "facelist", + "type": "FaceListProperty", + "value": { + "path": 0 + } } ], "type": "StyleTag" @@ -8227,6 +8578,15 @@ "animated": false, "key": "edit-style", "type": "TriggerProperty" + }, + { + "animatable": true, + "animated": false, + "key": "facelist", + "type": "FaceListProperty", + "value": { + "path": 0 + } } ], "type": "StyleTag" @@ -8510,6 +8870,15 @@ "animated": false, "key": "edit-style", "type": "TriggerProperty" + }, + { + "animatable": true, + "animated": false, + "key": "facelist", + "type": "FaceListProperty", + "value": { + "path": 0 + } } ], "type": "StyleTag" @@ -8755,6 +9124,15 @@ "animated": false, "key": "edit-style", "type": "TriggerProperty" + }, + { + "animatable": true, + "animated": false, + "key": "facelist", + "type": "FaceListProperty", + "value": { + "path": 0 + } } ], "type": "StyleTag" @@ -8977,6 +9355,15 @@ "animated": false, "key": "edit-style", "type": "TriggerProperty" + }, + { + "animatable": true, + "animated": false, + "key": "facelist", + "type": "FaceListProperty", + "value": { + "path": 0 + } } ], "type": "StyleTag" @@ -9115,6 +9502,15 @@ "animated": false, "key": "edit-style", "type": "TriggerProperty" + }, + { + "animatable": true, + "animated": false, + "key": "facelist", + "type": "FaceListProperty", + "value": { + "path": 0 + } } ], "type": "StyleTag" @@ -9369,6 +9765,15 @@ "animated": false, "key": "edit-style", "type": "TriggerProperty" + }, + { + "animatable": true, + "animated": false, + "key": "facelist", + "type": "FaceListProperty", + "value": { + "path": 0 + } } ], "type": "StyleTag" @@ -9789,6 +10194,15 @@ "animated": false, "key": "edit-style", "type": "TriggerProperty" + }, + { + "animatable": true, + "animated": false, + "key": "facelist", + "type": "FaceListProperty", + "value": { + "path": 0 + } } ], "type": "StyleTag" @@ -9927,6 +10341,15 @@ "animated": false, "key": "edit-style", "type": "TriggerProperty" + }, + { + "animatable": true, + "animated": false, + "key": "facelist", + "type": "FaceListProperty", + "value": { + "path": 0 + } } ], "type": "StyleTag" @@ -10241,6 +10664,15 @@ "animated": false, "key": "edit-style", "type": "TriggerProperty" + }, + { + "animatable": true, + "animated": false, + "key": "facelist", + "type": "FaceListProperty", + "value": { + "path": 0 + } } ], "type": "StyleTag" @@ -10557,6 +10989,15 @@ "animated": false, "key": "edit-style", "type": "TriggerProperty" + }, + { + "animatable": true, + "animated": false, + "key": "facelist", + "type": "FaceListProperty", + "value": { + "path": 0 + } } ], "type": "StyleTag" @@ -11001,6 +11442,15 @@ "animated": false, "key": "edit-style", "type": "TriggerProperty" + }, + { + "animatable": true, + "animated": false, + "key": "facelist", + "type": "FaceListProperty", + "value": { + "path": 0 + } } ], "type": "StyleTag" @@ -11221,6 +11671,15 @@ "animated": false, "key": "edit-style", "type": "TriggerProperty" + }, + { + "animatable": true, + "animated": false, + "key": "facelist", + "type": "FaceListProperty", + "value": { + "path": 0 + } } ], "type": "StyleTag" @@ -11454,6 +11913,15 @@ "animated": false, "key": "edit-style", "type": "TriggerProperty" + }, + { + "animatable": true, + "animated": false, + "key": "facelist", + "type": "FaceListProperty", + "value": { + "path": 0 + } } ], "type": "StyleTag" @@ -11594,6 +12062,15 @@ "animated": false, "key": "edit-style", "type": "TriggerProperty" + }, + { + "animatable": true, + "animated": false, + "key": "facelist", + "type": "FaceListProperty", + "value": { + "path": 0 + } } ], "type": "StyleTag" @@ -11737,6 +12214,15 @@ "animated": false, "key": "edit-style", "type": "TriggerProperty" + }, + { + "animatable": true, + "animated": false, + "key": "facelist", + "type": "FaceListProperty", + "value": { + "path": 0 + } } ], "type": "StyleTag" diff --git a/keybindings/default_keybindings.cfg b/keybindings/default_keybindings.cfg index 95d5a6c3b..6bf021c85 100644 --- a/keybindings/default_keybindings.cfg +++ b/keybindings/default_keybindings.cfg @@ -13,7 +13,6 @@ open ...: Ctrl+O save: Ctrl+S save as ...: Ctrl+Shift+S reset viewport: R -show point dialog: Ctrl+K new style: Alt+N, S previous tool: evaluate: F5 @@ -27,6 +26,7 @@ export ...: Ctrl+E remove selected points: Ctrl+Del remove selected items: Del scene_mode.vertex: +scene_mode.face: scene_mode.object: scene_mode.cycle: Ctrl+Tab convert objects: C @@ -78,6 +78,7 @@ StyleTag: NodesTag: # Tools: +SelectFacesTool: M, F SelectObjectsTool: O, O SelectPointsTool: P, P BrushSelectTool: P, B diff --git a/layouts/default_layout.ini b/layouts/default_layout.ini index 82919d6f7..c355e0e0f 100644 --- a/layouts/default_layout.ini +++ b/layouts/default_layout.ini @@ -11,7 +11,7 @@ managers\5\name=TimeLine_0 managers\5\type=TimeLine managers\size=5 toolbars\1\name=ToolBar -toolbars\1\tools="{\"items\":[{\"items\":[\"Ellipse\",\"RectangleObject\",\"Cloner\",\"Empty\",\"PathObject\",\"Mirror\",\"LineObject\",\"Tip\",\"ImageObject\",\"Instance\",\"ProceduralPath\",\"View\",\"Text\"],\"type\":1002},{\"items\":[\"SelectObjectsTool\",\"SelectPointsTool\",\"BrushSelectTool\",\"KnifeTool\",\"PathTool\"],\"type\":1002},{\"name\":\"scene_mode\",\"type\":1004},\"previous tool\",\"new style\",{\"items\":[\"deselect all\",\"select all\",\"invert selection\",\"show point dialog\",\"make smooth\",\"make linear\",\"subdivide\"],\"type\":1002},\"convert objects\",\"previous tool\"]}" +toolbars\1\tools="{\"items\":[{\"items\":[\"Ellipse\",\"RectangleObject\",\"Cloner\",\"Empty\",\"PathObject\",\"Mirror\",\"LineObject\",\"Tip\",\"ImageObject\",\"Instance\",\"ProceduralPath\",\"View\",\"Text\"],\"type\":1002},{\"items\":[\"SelectObjectsTool\",\"SelectPointsTool\",\"BrushSelectTool\",\"KnifeTool\",\"PathTool\"],\"type\":1002},{\"name\":\"scene_mode\",\"type\":1004},\"previous tool\",\"new style\",{\"items\":[\"deselect all\",\"select all\",\"invert selection\",\"make smooth\",\"make linear\",\"subdivide\"],\"type\":1002},\"convert objects\",\"previous tool\"]}" toolbars\1\type=ToolBar toolbars\size=1 window_state="@ByteArray(\0\0\0\xff\0\0\0\0\xfd\0\0\0\x3\0\0\0\x1\0\0\x1\xc1\0\0\x2\xc4\xfc\x2\0\0\0\x4\xfb\0\0\0\x1e\0O\0\x62\0j\0\x65\0\x63\0t\0M\0\x61\0n\0\x61\0g\0\x65\0r\0_\0\x30\x1\0\0\0\x14\0\0\x3%\0\0\0\0\0\0\0\0\xfb\0\0\0\"\0P\0r\0o\0p\0\x65\0r\0t\0y\0M\0\x61\0n\0\x61\0g\0\x65\0r\0_\0\x30\x1\0\0\x3?\0\0\x1U\0\0\0\0\0\0\0\0\xfb\0\0\0\x1a\0O\0\x62\0j\0\x65\0\x63\0t\0M\0\x61\0n\0\x61\0g\0\x65\0r\x1\0\0\0;\0\0\x1\xdd\0\0\0Y\0\xff\xff\xff\xfb\0\0\0\x1e\0P\0r\0o\0p\0\x65\0r\0t\0y\0M\0\x61\0n\0\x61\0g\0\x65\0r\x1\0\0\x2\x1e\0\0\0\xe1\0\0\0\x9a\0\xff\xff\xff\0\0\0\x2\0\0\x3\xbc\0\0\0X\xfc\x1\0\0\0\x3\xfb\0\0\0$\0\x44\0o\0p\0\x65\0S\0h\0\x65\0\x65\0t\0M\0\x61\0n\0\x61\0g\0\x65\0r\0_\0\x30\x2\0\0\x4v\0\0\x2R\0\0\x1\x14\0\0\0\xe7\xfb\0\0\0\x1c\0H\0i\0s\0t\0o\0r\0y\0M\0\x61\0n\0\x61\0g\0\x65\0r\x2\0\0\n\xb6\0\0\x1\x9e\0\0\x1\x14\0\0\0\xe7\xfb\0\0\0\x1a\0P\0y\0t\0h\0o\0n\0\x43\0o\0n\0s\0o\0l\0\x65\x2\0\0\x4}\0\0\x2\x61\0\0\x1\a\0\0\0\xc9\0\0\0\x3\0\0\a|\0\0\x1\x3\xfc\x2\0\0\0\x2\xfc\0\0\0\0\xff\xff\xff\xff\0\0\0\0\0\xff\xff\xff\xfc\x1\0\0\0\x2\xfc\0\0\0\0\0\0\t\xfc\0\0\0\0\0\xff\xff\xff\xfc\x2\0\0\0\x2\xfb\0\0\0\x10\0T\0i\0m\0\x65\0L\0i\0n\0\x65\x1\0\0\x4\x9a\0\0\0\x37\0\0\0\0\0\0\0\0\xfc\0\0\x4\xd7\0\0\0\x99\0\0\0\0\0\xff\xff\xff\xfc\x1\0\0\0\x2\xfb\0\0\0\x18\0S\0t\0y\0l\0\x65\0M\0\x61\0n\0\x61\0g\0\x65\0r\x1\0\0\0\0\0\0\a\xde\0\0\0\0\0\0\0\0\xfb\0\0\0$\0\x42\0o\0u\0n\0\x64\0i\0n\0g\0\x42\0o\0x\0M\0\x61\0n\0\x61\0g\0\x65\0r\x1\0\0\a\xe4\0\0\x2\x18\0\0\0\0\0\0\0\0\xfc\0\0\0\0\0\0\x3\xb7\0\0\0\0\0\xff\xff\xff\xfc\x2\0\0\0\x1\xfb\0\0\0 \0\x44\0o\0p\0\x65\0S\0h\0\x65\0\x65\0t\0M\0\x61\0n\0\x61\0g\0\x65\0r\x2\0\0\t9\0\0\x2\x85\0\0\x3\xb7\0\0\0\xa5\xfc\0\0\x3\x5\0\0\x1\x3\0\0\0w\0\xff\xff\xff\xfc\x1\0\0\0\x2\xfc\0\0\0\0\0\0\x4\xcf\0\0\x2\xd2\0\xff\xff\xff\xfc\x2\0\0\0\x2\xfb\0\0\0\x14\0T\0i\0m\0\x65\0L\0i\0n\0\x65\0_\0\x30\x1\0\0\x3\x5\0\0\0Y\0\0\0\x18\0\xff\xff\xff\xfb\0\0\0\x1c\0S\0t\0y\0l\0\x65\0M\0\x61\0n\0\x61\0g\0\x65\0r\0_\0\x30\x1\0\0\x3\x64\0\0\0\xa4\0\0\0Y\0\xff\xff\xff\xfb\0\0\0(\0\x42\0o\0u\0n\0\x64\0i\0n\0g\0\x42\0o\0x\0M\0\x61\0n\0\x61\0g\0\x65\0r\0_\0\x30\x1\0\0\x4\xd5\0\0\x2\xa7\0\0\0\xc5\0\xff\xff\xff\0\0\x5\xb5\0\0\x2\xc4\0\0\0\x4\0\0\0\x4\0\0\0\b\0\0\0\b\xfc\0\0\0\x1\0\0\0\x2\0\0\0\x1\0\0\0\xe\0T\0o\0o\0l\0\x42\0\x61\0r\x1\0\0\0\0\xff\xff\xff\xff\0\0\0\0\0\0\0\0)" diff --git a/lists/properties.lst b/lists/properties.lst index f4ab5101d..81fbe3bf2 100644 --- a/lists/properties.lst +++ b/lists/properties.lst @@ -5,6 +5,7 @@ "items": [ "BoolProperty", "ColorProperty", + "FaceListProperty", "FloatProperty", "IntegerProperty", "OptionProperty", diff --git a/lists/tools.lst b/lists/tools.lst index 4e4455929..aa7552192 100644 --- a/lists/tools.lst +++ b/lists/tools.lst @@ -6,6 +6,7 @@ "BrushSelectTool", "KnifeTool", "PathTool", + "SelectFacesTool", "SelectObjectsTool", "SelectPointsTool", "SelectSimilarTool", diff --git a/sample-scenes/basic.omm b/sample-scenes/basic.omm index c6499bc0f..5721309a6 100644 --- a/sample-scenes/basic.omm +++ b/sample-scenes/basic.omm @@ -158,6 +158,15 @@ "animated": false, "key": "edit-style", "type": "TriggerProperty" + }, + { + "animatable": true, + "animated": false, + "key": "facelist", + "type": "FaceListProperty", + "value": { + "path": 0 + } } ], "type": "StyleTag" @@ -300,6 +309,15 @@ "animated": false, "key": "edit-style", "type": "TriggerProperty" + }, + { + "animatable": true, + "animated": false, + "key": "facelist", + "type": "FaceListProperty", + "value": { + "path": 0 + } } ], "type": "StyleTag" @@ -443,6 +461,15 @@ "animated": false, "key": "edit-style", "type": "TriggerProperty" + }, + { + "animatable": true, + "animated": false, + "key": "facelist", + "type": "FaceListProperty", + "value": { + "path": 0 + } } ], "type": "StyleTag" diff --git a/sample-scenes/glshader.omm b/sample-scenes/glshader.omm index 830008491..f96646656 100644 --- a/sample-scenes/glshader.omm +++ b/sample-scenes/glshader.omm @@ -152,6 +152,15 @@ "animated": false, "key": "edit-style", "type": "TriggerProperty" + }, + { + "animatable": true, + "animated": false, + "key": "facelist", + "type": "FaceListProperty", + "value": { + "path": 0 + } } ], "type": "StyleTag" @@ -296,6 +305,15 @@ "animated": false, "key": "edit-style", "type": "TriggerProperty" + }, + { + "animatable": true, + "animated": false, + "key": "facelist", + "type": "FaceListProperty", + "value": { + "path": 0 + } } ], "type": "StyleTag" diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 8eecbdd14..9c234a2ac 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -5,10 +5,11 @@ target_sources(libommpfritt PRIVATE cache.h cachedgetter.h common.h - disjointset.h dnf.h enumnames.cpp enumnames.h + facelist.cpp + facelist.h logging.cpp logging.h maybeowner.h diff --git a/src/aspects/abstractpropertyowner.cpp b/src/aspects/abstractpropertyowner.cpp index d37da2bc0..f211c4fe9 100644 --- a/src/aspects/abstractpropertyowner.cpp +++ b/src/aspects/abstractpropertyowner.cpp @@ -35,8 +35,8 @@ AbstractPropertyOwner::AbstractPropertyOwner(Kind kind, Scene* scene) : kind(kin AbstractPropertyOwner::AbstractPropertyOwner(const AbstractPropertyOwner& other) : QObject() // NOLINT(readability-redundant-member-init) - , - kind(other.kind), m_scene(other.m_scene) + , kind(other.kind) + , m_scene(other.m_scene) { for (auto&& key : other.m_properties.keys()) { AbstractPropertyOwner::add_property(key, other.m_properties.at(key)->clone()); @@ -80,6 +80,7 @@ void AbstractPropertyOwner::serialize(serialization::SerializerWorker& worker) c void AbstractPropertyOwner::deserialize(serialization::DeserializerWorker& worker) { + using DeserializeError = serialization::AbstractDeserializer::DeserializeError; m_id = worker.sub(ID_POINTER)->get_size_t(); worker.deserializer().register_reference(m_id, *this); @@ -88,7 +89,9 @@ void AbstractPropertyOwner::deserialize(serialization::DeserializerWorker& worke const auto property_type = worker_i.sub(PROPERTY_TYPE_POINTER)->get_string(); if (properties().contains(property_key)) { - assert(property_type == property(property_key)->type()); + if (property_type != property(property_key)->type()) { + throw DeserializeError("Built-in property does not have expected type."); + } property(property_key)->deserialize(worker_i); } else { std::unique_ptr property; @@ -96,9 +99,9 @@ void AbstractPropertyOwner::deserialize(serialization::DeserializerWorker& worke property = Property::make(property_type); } catch (const std::out_of_range&) { const auto msg = "Failed to retrieve property type '" + property_type + "'."; - throw serialization::AbstractDeserializer::DeserializeError(msg.toStdString()); + throw DeserializeError(msg.toStdString()); } catch (const Property::InvalidKeyError& e) { - throw serialization::AbstractDeserializer::DeserializeError(e.what()); + throw DeserializeError(e.what()); } property->deserialize(worker_i); [[maybe_unused]] Property& ref = add_property(property_key, std::move(property)); diff --git a/src/cachedgetter.h b/src/cachedgetter.h index 6408cce4a..a48a36a49 100644 --- a/src/cachedgetter.h +++ b/src/cachedgetter.h @@ -2,6 +2,9 @@ #include #include +#include +#include +#include template class ArgsCachedGetter { @@ -67,3 +70,28 @@ template class CachedGetter mutable T m_cache; mutable bool m_is_dirty = true; }; + +template +auto make_simple_cached_getter(const Self& self, const Compute& compute) +{ + using R = decltype(std::invoke(std::declval(), std::declval())); + class SimpleCachedGetter + : public CachedGetter + { + public: + explicit SimpleCachedGetter(const Self& self, const Compute& compute) + : CachedGetter(self) + , m_compute(compute) + { + } + + R compute() const override + { + return std::invoke(m_compute, this->m_self); + } + + private: + Compute m_compute; + }; + return static_cast>>(std::make_unique(self, compute)); +} diff --git a/src/commands/CMakeLists.txt b/src/commands/CMakeLists.txt index e8119a94f..8fc2eeade 100644 --- a/src/commands/CMakeLists.txt +++ b/src/commands/CMakeLists.txt @@ -1,13 +1,21 @@ target_sources(libommpfritt PRIVATE + addcommand.h + addpointscommand.cpp + addpointscommand.h + addremovepointscommand.cpp + addremovepointscommand.h + addremovepointscommandchangeset.cpp + addremovepointscommandchangeset.h command.cpp command.h - addcommand.h composecommand.cpp composecommand.h copycommand.cpp copycommand.h cutpathcommand.cpp cutpathcommand.h + forwardingportcommand.cpp + forwardingportcommand.h joinpointscommand.cpp joinpointscommand.h keyframecommand.cpp @@ -16,8 +24,6 @@ target_sources(libommpfritt PRIVATE modifypointscommand.h modifysegmentscommand.cpp modifysegmentscommand.h - forwardingportcommand.h - forwardingportcommand.cpp movecommand.cpp movecommand.h movetagscommand.cpp @@ -30,10 +36,14 @@ target_sources(libommpfritt PRIVATE objectselectioncommand.h objectstransformationcommand.cpp objectstransformationcommand.h + ownedlocatedpath.cpp + ownedlocatedpath.h propertycommand.cpp propertycommand.h removecommand.cpp removecommand.h + removepointscommand.cpp + removepointscommand.h setinterpolationcommand.cpp setinterpolationcommand.h subdividepathcommand.cpp diff --git a/src/commands/addpointscommand.cpp b/src/commands/addpointscommand.cpp new file mode 100644 index 000000000..1ac737ae8 --- /dev/null +++ b/src/commands/addpointscommand.cpp @@ -0,0 +1,49 @@ +#include "commands/addpointscommand.h" + +#include "commands/addremovepointscommandchangeset.h" +#include "commands/ownedlocatedpath.h" +#include "path/edge.h" +#include "path/pathview.h" + +namespace +{ + +auto make_change_set_for_add(omm::OwnedLocatedPath points_to_add) +{ + omm::PathView path_view_to_remove{*points_to_add.path(), points_to_add.point_offset(), 0}; + return omm::AddRemovePointsCommandChangeSet{path_view_to_remove, points_to_add.create_edges(), + points_to_add.single_point()}; +} + +} // namespace + +namespace omm +{ + +AddPointsCommand::AddPointsCommand(OwnedLocatedPath&& points_to_add, PathObject* const path_object) + : AddRemovePointsCommand(static_label(), make_change_set_for_add(std::move(points_to_add)), path_object) + , m_new_edges(owned_edges()) // owned_edges are new edges before calling redo. +{ +} + +void AddPointsCommand::undo() +{ + restore_bridges(); +} + +void AddPointsCommand::redo() +{ + restore_edges(); +} + +QString AddPointsCommand::static_label() +{ + return QObject::tr("AddPointsCommand"); +} + +std::deque AddPointsCommand::new_edges() const +{ + return m_new_edges; +} + +} // namespace omm diff --git a/src/commands/addpointscommand.h b/src/commands/addpointscommand.h new file mode 100644 index 000000000..fda7cb842 --- /dev/null +++ b/src/commands/addpointscommand.h @@ -0,0 +1,27 @@ +#pragma once + +#include "commands/addremovepointscommand.h" + +namespace omm +{ + +class OwnedLocatedPath; + +class AddPointsCommand : public AddRemovePointsCommand +{ +public: + explicit AddPointsCommand(OwnedLocatedPath&& points_to_add, PathObject* path_object = nullptr); + void undo() override; + void redo() override; + static QString static_label(); + /** + * @brief new_edges the edges that are created when calling @code redo. + * After calling @code undo, this is the same as @code owned_edges. + */ + std::deque new_edges() const; + +private: + const std::deque m_new_edges; +}; + +} // namespace omm diff --git a/src/commands/addremovepointscommand.cpp b/src/commands/addremovepointscommand.cpp new file mode 100644 index 000000000..b9491fa06 --- /dev/null +++ b/src/commands/addremovepointscommand.cpp @@ -0,0 +1,54 @@ +#include "commands/addremovepointscommand.h" +#include "commands/addremovepointscommandchangeset.h" +#include "logging.h" +#include "objects/pathobject.h" +#include "path/edge.h" +#include "path/path.h" +#include "path/pathpoint.h" +#include "path/pathview.h" +#include "scene/scene.h" +#include "transform.h" +#include + +namespace omm +{ + +AddRemovePointsCommand::AddRemovePointsCommand(const QString& label, AddRemovePointsCommandChangeSet changes, + PathObject* const path_object) + : Command(label) + , m_change_set(std::make_unique(std::move(changes))) + , m_path_object(path_object) +{ +} + +AddRemovePointsCommand::~AddRemovePointsCommand() = default; + +void AddRemovePointsCommand::restore_bridges() +{ + m_change_set->swap(); + update(); +} + +void AddRemovePointsCommand::restore_edges() +{ + m_change_set->swap(); + update(); +} + +std::deque AddRemovePointsCommand::owned_edges() const +{ + std::deque new_edges; + const auto& oe = m_change_set->owned_edges(); + new_edges.insert(new_edges.end(), oe.begin(), oe.end()); + return new_edges; +} + +void AddRemovePointsCommand::update() +{ + if (m_path_object != nullptr) { + m_path_object->update(); + m_path_object->scene()->update_tool(); + } +} + +} // namespace omm diff --git a/src/commands/addremovepointscommand.h b/src/commands/addremovepointscommand.h new file mode 100644 index 000000000..594fdafdb --- /dev/null +++ b/src/commands/addremovepointscommand.h @@ -0,0 +1,38 @@ +#pragma once + +#include "commands/command.h" +#include +#include + +namespace omm +{ + +class AddRemovePointsCommandChangeSet; +class Edge; +class PathObject; + +class AddRemovePointsCommand : public Command +{ +public: + + /** + * @brief owned_edges the edges that this command owns. + * This changes when calling @code undo and @code redo, however, it is invariant when calling + * @code redo and @code undo subsequentially. + */ + std::deque owned_edges() const; + +protected: + explicit AddRemovePointsCommand(const QString& label, AddRemovePointsCommandChangeSet changes, + PathObject* path_object = nullptr); + ~AddRemovePointsCommand() override; + void restore_bridges(); + void restore_edges(); + +private: + std::unique_ptr m_change_set; + PathObject* m_path_object; + void update(); +}; + +} // namespace omm diff --git a/src/commands/addremovepointscommandchangeset.cpp b/src/commands/addremovepointscommandchangeset.cpp new file mode 100644 index 000000000..dbc7d326f --- /dev/null +++ b/src/commands/addremovepointscommandchangeset.cpp @@ -0,0 +1,55 @@ +#include "commands/addremovepointscommandchangeset.h" + +#include "path/edge.h" +#include "path/path.h" +#include "transform.h" + +namespace omm +{ + +AddRemovePointsCommandChangeSet::AddRemovePointsCommandChangeSet(const PathView& view, + std::deque> edges, + std::shared_ptr single_point) + : m_view(view), m_owned_edges(std::move(edges)), m_owned_point(std::move(single_point)) +{ + assert(Path::is_valid(m_owned_edges)); + assert((m_owned_point == nullptr) || m_owned_edges.empty()); +} + +void AddRemovePointsCommandChangeSet::swap() +{ + std::size_t added_point_count = 0; + + if (m_view.path().points().empty() && m_owned_point) { + // path empty, add single point + assert(m_owned_edges.empty()); + m_view.path().set_single_point(std::move(m_owned_point)); + added_point_count = 1; + } else if (m_view.path().points().size() == 1 && m_view.point_count() == 1) { + // path contains only a single point which is going to be removed + m_owned_point = m_view.path().extract_single_point(); + added_point_count = 0; + } else { + // all other cases are handled by Path::replace + auto& path = m_view.path(); + if (m_owned_edges.empty()) { + added_point_count = 0; + } else if (path.points().empty()) { + added_point_count = m_owned_edges.size() + 1; + } else if (m_view.begin() == 0 || m_view.end() == path.points().size()) { + added_point_count = m_owned_edges.size(); + } else { + added_point_count = m_owned_edges.size() - 1; + } + m_owned_edges = path.replace(m_view, std::move(m_owned_edges)); + } + + m_view = PathView{m_view.path(), m_view.begin(), added_point_count}; +} + +std::vector AddRemovePointsCommandChangeSet::owned_edges() const +{ + return util::transform(m_owned_edges, &std::unique_ptr::get); +} + +} // namespace omm diff --git a/src/commands/addremovepointscommandchangeset.h b/src/commands/addremovepointscommandchangeset.h new file mode 100644 index 000000000..1cc264863 --- /dev/null +++ b/src/commands/addremovepointscommandchangeset.h @@ -0,0 +1,28 @@ +#pragma once + +#include "path/pathview.h" +#include +#include + +namespace omm +{ + +class PathPoint; +class Edge; + +class AddRemovePointsCommandChangeSet +{ +public: + explicit AddRemovePointsCommandChangeSet(const PathView& view, std::deque> edges, + std::shared_ptr single_point); + + void swap(); + std::vector owned_edges() const; + +private: + PathView m_view; + std::deque> m_owned_edges; + std::shared_ptr m_owned_point; +}; + +} // namespace omm diff --git a/src/commands/cutpathcommand.cpp b/src/commands/cutpathcommand.cpp index 94c3fb91b..c5082f8f5 100644 --- a/src/commands/cutpathcommand.cpp +++ b/src/commands/cutpathcommand.cpp @@ -1,103 +1,133 @@ #include "commands/cutpathcommand.h" +#include "commands/addpointscommand.h" #include "commands/modifypointscommand.h" +#include "commands/ownedlocatedpath.h" #include "objects/pathobject.h" +#include "path/edge.h" +#include "path/lib2geomadapter.h" #include "path/path.h" #include "path/pathpoint.h" #include "path/pathvector.h" +#include "transform.h" namespace { -using namespace omm; - -std::deque> cut(PathPoint& a, PathPoint& b, - const InterpolationMode interpolation, - std::deque&& positions, - std::map& modified_points) +struct Index { - const auto control_points = Path::compute_control_points(a.geometry(), b.geometry(), interpolation); - const auto geom_control_points = util::transform(control_points, std::mem_fn(&Vec2f::to_geom_point)); - const auto curve = std::unique_ptr(Geom::BezierCurve::create(geom_control_points)); - assert(std::is_sorted(positions.begin(), positions.end())); - assert(!positions.empty()); - assert(positions.front() >= 0.0 && positions.back() <= 1.0); - if (!positions.empty() && positions.front() == 0.0) { - positions.pop_front(); - } - if (!positions.empty() && positions.back() == 1.0) { - positions.pop_back(); - } - if (positions.empty()) { - return {}; + Index(const Geom::PathVectorTime& pvt) : path_index(pvt.path_index), edge_index(pvt.curve_index) + { } - std::deque> new_curves; - new_curves.emplace_back(dynamic_cast(curve->portion(0.0, positions.front()))); - for (std::size_t i = 0; i < positions.size() - 1; ++i) { - new_curves.emplace_back(dynamic_cast(curve->portion(positions[i], positions[i+1]))); + std::size_t path_index; + std::size_t edge_index; + + [[nodiscard]] bool operator>(const Index& other) const noexcept + { + if (path_index == other.path_index) { + return edge_index > other.edge_index; + } + return path_index > other.path_index; } - new_curves.emplace_back(dynamic_cast(curve->portion(positions.back(), 1.0))); - - Point left_point = a.geometry(); - left_point.set_right_position(Vec2f{new_curves.front()->controlPoint(1)}); - modified_points[&a] = left_point; - - Point right_point = b.geometry(); - right_point.set_left_position(Vec2f{new_curves.back()->controlPoint(2)}); - modified_points[&b] = right_point; - - std::deque> new_points; - for (std::size_t i = 1; i < new_curves.size(); ++i) { - // the last point of the previous curve must match the first point of the current one - assert(new_curves[i-1]->controlPoint(3) == new_curves[i]->controlPoint(0)); - Point point{Vec2f{new_curves[i-1]->controlPoint(3)}}; - point.set_left_position(Vec2f{new_curves[i-1]->controlPoint(2)}); - point.set_right_position(Vec2f{new_curves[i]->controlPoint(1)}); - new_points.push_back(std::make_unique(point, a.path())); +}; + +auto convert_cuts(const std::vector& positions) +{ + std::map, std::greater<>> cuts; + for (const auto& p : positions) { + static constexpr auto eps = 0.0001; + if (p.t > eps && p.t < 1.0 - eps) { + cuts[p].insert(p.t); + } } + return cuts; +} - assert(new_points.size() == positions.size()); - return new_points; +using CurveType = Geom::BezierCurveN<3>; + +auto compute_portions(const omm::Edge& edge, std::set ts) +{ + assert(!ts.empty()); + ts.insert(0.0); + ts.insert(1.0); + const auto ts_vec = std::vector(ts.begin(), ts.end()); + std::vector> portions; + portions.reserve(ts_vec.size() - 1); + + const auto curve = omm::omm_to_geom(edge); + + for (std::size_t i = 1; i < ts_vec.size(); ++i) { + // ownership of the curve portion is passed to the vector of unique_ptr. + portions.emplace_back(static_cast(curve.portion(ts_vec.at(i - 1), ts_vec.at(i)))); + } + return portions; } -void cut(Path& path, - std::vector&& positions, - const InterpolationMode interpolation, - std::deque& new_point_sequences, - std::map& modified_points) +class Cutter { - assert(std::is_sorted(positions.begin(), positions.end())); +public: + Cutter(omm::Path& path, const std::size_t edge_index, std::set ts, + omm::ModifyPointsCommand::ModifiedPointsMap& modified_points) + : m_path(path) + , m_edge_index(edge_index) + , m_edge(path.edge(edge_index)) + , m_portions(compute_portions(m_edge, std::move(ts))) + , m_new_points(compute_new_points(compute_new_point_geometries())) + { + compute_end_point_modifications(modified_points); + } - std::map> curve_positions; - for (const auto& position : positions) { - curve_positions[position.curve_index].push_back(position.t); + [[nodiscard]] omm::OwnedLocatedPath&& new_points() noexcept + { + return std::move(m_new_points); } - for (auto&& [i, positions] : curve_positions) { - const auto j = i == path.size() - 1 ? 0 : i + 1; - auto new_points = cut(path.at(i), path.at(j), interpolation, std::move(positions), modified_points); - if (!new_points.empty()) { - new_point_sequences.emplace_back(&path, i + 1, std::move(new_points)); +private: + [[nodiscard]] omm::OwnedLocatedPath compute_new_points(const std::deque& geometries) const + { + auto points = util::transform(geometries, [this](const omm::Point& geometry) { + return std::make_shared(geometry, m_path.path_vector()); + }); + + return omm::OwnedLocatedPath(&m_path, m_edge_index + 1, std::move(points)); + } + + [[nodiscard]] std::deque compute_new_point_geometries() const + { + std::deque point_geometries; + for (std::size_t i = 1; i < m_portions.size(); ++i) { + const auto& curve = *m_portions.at(i); + auto& geometry = point_geometries.emplace_back(omm::Vec2f(curve.initialPoint())); + set_tangent(geometry, omm::Direction::Forward, curve); + set_tangent(geometry, omm::Direction::Backward, *m_portions.at(i - 1)); } + return point_geometries; } -} -void cut(PathVector& path_vector, - const InterpolationMode interpolation, - const std::vector& positions, - std::deque& new_points, - std::map& modified_points) -{ - const auto paths = path_vector.paths(); - std::map> path_positions; - for (const auto position : positions) { - path_positions[paths.at(position.path_index)].push_back(position.asPathTime()); + void set_tangent(omm::Point& point, const omm::Direction direction, const CurveType& curve) const + { + const auto d = direction == omm::Direction::Forward ? curve.controlPoint(1) - curve.controlPoint(0) + : curve.controlPoint(2) - curve.controlPoint(3); + point.set_tangent({&m_path, direction}, omm::PolarCoordinates(omm::Vec2f(d))); } - for (auto&& [path, positions] : path_positions) { - cut(*path, std::move(positions), interpolation, new_points, modified_points); + void compute_end_point_modifications(omm::ModifyPointsCommand::ModifiedPointsMap& modified_points) const + { + const auto set_tangent = [&modified_points, this](omm::PathPoint& point, const omm::Direction direction, + const CurveType& curve) { + auto& geometry = modified_points.try_emplace(&point, point.geometry()).first->second; + this->set_tangent(geometry, direction, curve); + }; + set_tangent(*m_edge.a(), omm::Direction::Forward, *m_portions.front()); + set_tangent(*m_edge.b(), omm::Direction::Backward, *m_portions.back()); } -} + + omm::Path& m_path; + const std::size_t m_edge_index; + const omm::Edge& m_edge; + const std::vector> m_portions; + omm::OwnedLocatedPath m_new_points; +}; } // namespace @@ -105,22 +135,40 @@ namespace omm { CutPathCommand::CutPathCommand(PathObject& path_object, const std::vector& cuts) - : CutPathCommand(QObject::tr("CutPathCommand"), path_object, cuts) + : CutPathCommand(QObject::tr("CutPathCommand"), path_object, cuts) { } -CutPathCommand::CutPathCommand(const QString& label, - PathObject& path_object, +const std::set& CutPathCommand::new_points() const noexcept +{ + return m_new_points; +} + +CutPathCommand::CutPathCommand(const QString& label, PathObject& path_object, const std::vector& cuts) - : ComposeCommand(label) + : ComposeCommand(label) { - const auto interpolation = path_object.property(PathObject::INTERPOLATION_PROPERTY_KEY)->value(); - std::deque new_points; - std::map modified_points; - cut(path_object.geometry(), interpolation, cuts, new_points, modified_points); + // TODO respect path interpolation mode (linear, smooth, bezier) + const auto m_cuts = ::convert_cuts(cuts); + + omm::ModifyPointsCommand::ModifiedPointsMap modified_points; + std::deque new_path_segments; + for (const auto& [index, ts] : m_cuts) { + auto& path = path_object.path_vector().path(index.path_index); + Cutter cutter(path, index.edge_index, ts, modified_points); + new_path_segments.emplace_back(cutter.new_points()); + } + std::vector> commands; - commands.push_back(std::make_unique(modified_points)); - commands.push_back(std::make_unique(path_object, std::move(new_points))); + commands.reserve(1 + new_path_segments.size()); + if (!modified_points.empty()) { + commands.emplace_back(std::make_unique(modified_points)); + } + for (auto& np : new_path_segments) { + const auto points = np.points(); + m_new_points.insert(points.begin(), points.end()); + commands.emplace_back(std::make_unique(std::move(np), &path_object)); + } set_commands(std::move(commands)); } diff --git a/src/commands/cutpathcommand.h b/src/commands/cutpathcommand.h index a5eb35b51..4613a645d 100644 --- a/src/commands/cutpathcommand.h +++ b/src/commands/cutpathcommand.h @@ -2,10 +2,12 @@ #include "commands/composecommand.h" #include <2geom/pathvector.h> +#include namespace omm { +class PathPoint; class PathObject; class CutPathCommand : public ComposeCommand @@ -15,6 +17,10 @@ class CutPathCommand : public ComposeCommand public: explicit CutPathCommand(PathObject& path, const std::vector& cuts); + const std::set& new_points() const noexcept; + +private: + std::set m_new_points; }; } // namespace omm diff --git a/src/commands/joinpointscommand.cpp b/src/commands/joinpointscommand.cpp index c88f609be..70b786d12 100644 --- a/src/commands/joinpointscommand.cpp +++ b/src/commands/joinpointscommand.cpp @@ -1,123 +1 @@ -#include "commands/joinpointscommand.h" -#include "objects/pathobject.h" -#include "path/pathpoint.h" -#include "path/pathvector.h" -#include "scene/scene.h" - -namespace omm -{ - -JoinPointsCommand::JoinPointsCommand(Scene& scene, const DisjointPathPointSetForest& forest) - : AbstractJoinPointsCommand(QObject::tr("Join Points"), scene, forest) -{ -} - -void JoinPointsCommand::undo() -{ - scene().joined_points() = m_old_forest; - for (const auto& set : forest().sets()) { - for (auto* point : set) { - point->set_geometry(m_old_positions[point]); - } - } - update_affected_paths(); -} - -void JoinPointsCommand::redo() -{ - m_old_forest = scene().joined_points(); - for (const auto& set : forest().sets()) { - const auto joined = scene().joined_points().insert(set); - const auto new_pos = compute_position(joined); - for (auto* point : set) { - m_old_positions[point] = point->geometry(); - auto geometry = point->geometry(); - geometry.set_position(new_pos); - point->set_geometry(geometry); - } - } - update_affected_paths(); -} - -Vec2f JoinPointsCommand::compute_position(const ::transparent_set& points) -{ - Vec2f pos{0.F, 0.F}; - for (const auto* p : points) { - pos += p->geometry().position(); - } - return pos / static_cast(points.size()); -} - -DisjoinPointsCommand::DisjoinPointsCommand(Scene& scene, const DisjointPathPointSetForest& forest) - : AbstractJoinPointsCommand(QObject::tr("Disjoin Points"), scene, forest) -{ -} - -void DisjoinPointsCommand::undo() -{ - scene().joined_points() = m_old_forest; -} - -void DisjoinPointsCommand::redo() -{ - m_old_forest = scene().joined_points(); - for (const auto& set : forest().sets()) { - for (auto* point : set) { - scene().joined_points().remove({point}); - } - } -} - -AbstractJoinPointsCommand::AbstractJoinPointsCommand(const QString& label, - Scene& scene, - const DisjointPathPointSetForest& forest) - : Command(label) - , m_scene(scene) - , m_forest(forest) -{ - -} - -const DisjointPathPointSetForest& AbstractJoinPointsCommand::forest() const -{ - return m_forest; -} - -Scene& AbstractJoinPointsCommand::scene() const -{ - return m_scene; -} - -void AbstractJoinPointsCommand::update_affected_paths() const -{ - std::set path_vectors; - for (const auto& set : forest().sets()) { - for (const auto* point : set) { - path_vectors.insert(point->path_vector()); - } - } - for (auto* path_vector : path_vectors) { - path_vector->path_object()->update(); - } -} - -ShareJoinedPointsCommand::ShareJoinedPointsCommand(Scene& scene, PathVector& pv) - : Command("Join shared points") - , m_scene(scene) - , m_pv(pv) -{ -} - -void ShareJoinedPointsCommand::undo() -{ - m_scene.joined_points() = m_old_scene_joined_points; - m_pv.unshare_joined_points(std::move(m_old_other_joined_points)); -} - -void ShareJoinedPointsCommand::redo() -{ - m_old_scene_joined_points = m_scene.joined_points(); - m_old_other_joined_points = m_pv.share_joined_points(m_scene.joined_points()); -} - -} // namespace omm +// TODO diff --git a/src/commands/joinpointscommand.h b/src/commands/joinpointscommand.h index a6d28cb9c..e69de29bb 100644 --- a/src/commands/joinpointscommand.h +++ b/src/commands/joinpointscommand.h @@ -1,69 +0,0 @@ -#pragma once - -#include "commands/command.h" -#include "scene/disjointpathpointsetforest.h" -#include "geometry/point.h" - -#include -#include - -namespace omm -{ - -class PathVector; // NOLINT(bugprone-forward-declaration-namespace) - -class AbstractJoinPointsCommand : public Command -{ -public: - explicit AbstractJoinPointsCommand(const QString& label, Scene& scene, const DisjointPathPointSetForest& forest); - -protected: - [[nodiscard]] const DisjointPathPointSetForest& forest() const; - [[nodiscard]] Scene& scene() const; - void update_affected_paths() const; - -private: - Scene& m_scene; - const DisjointPathPointSetForest m_forest; -}; - -class JoinPointsCommand : public AbstractJoinPointsCommand -{ -public: - explicit JoinPointsCommand(Scene& scene, const DisjointPathPointSetForest& forest); - void undo() override; - void redo() override; - -private: - DisjointPathPointSetForest m_old_forest; - std::map m_old_positions; - - static Vec2f compute_position(const::transparent_set& points); -}; - -class DisjoinPointsCommand : public AbstractJoinPointsCommand -{ -public: - explicit DisjoinPointsCommand(Scene& scene, const DisjointPathPointSetForest& forest); - void undo() override; - void redo() override; - -private: - DisjointPathPointSetForest m_old_forest; -}; - -class ShareJoinedPointsCommand : public Command -{ -public: - explicit ShareJoinedPointsCommand(Scene& scene, PathVector& pv); - void undo() override; - void redo() override; - -private: - Scene& m_scene; - PathVector& m_pv; - DisjointPathPointSetForest m_old_scene_joined_points; - std::unique_ptr m_old_other_joined_points; -}; - -} // namespace omm diff --git a/src/commands/modifypointscommand.cpp b/src/commands/modifypointscommand.cpp index 213aa5faf..1a2ea1d8e 100644 --- a/src/commands/modifypointscommand.cpp +++ b/src/commands/modifypointscommand.cpp @@ -1,19 +1,17 @@ #include "commands/modifypointscommand.h" #include "common.h" -#include "scene/scene.h" #include "objects/pathobject.h" -#include "path/path.h" #include "path/pathpoint.h" #include "path/pathvector.h" -#include "path/pathview.h" + namespace omm { -ModifyPointsCommand ::ModifyPointsCommand(const std::map& points) - : Command(QObject::tr("ModifyPointsCommand")), m_data(points) +ModifyPointsCommand ::ModifyPointsCommand(ModifiedPointsMap points) + : Command(QObject::tr("ModifyPointsCommand")), m_data(std::move(points)) { - assert(!points.empty()); + assert(!m_data.empty()); } void ModifyPointsCommand::undo() @@ -33,17 +31,14 @@ int ModifyPointsCommand::id() const void ModifyPointsCommand::exchange() { - std::set path_vectors; + std::set path_vectors; for (auto& [ptr, point] : m_data) { const auto geometry = ptr->geometry(); ptr->set_geometry(point); point = geometry; path_vectors.insert(ptr->path_vector()); - for (auto* buddy : ptr->joined_points()) { - path_vectors.insert(buddy->path_vector()); - } } - for (auto* path_vector : path_vectors) { + for (const auto* const path_vector : path_vectors) { path_vector->path_object()->update(); } } @@ -63,135 +58,4 @@ bool ModifyPointsCommand::is_noop() const }); } -AbstractPointsCommand::AbstractPointsCommand(const QString& label, - PathObject& path_object, - std::deque&& points_to_add) - : Command(label) - , m_path_object(path_object) - , m_points_to_add(std::move(points_to_add)) -{ - std::sort(m_points_to_add.rbegin(), m_points_to_add.rend()); - assert(std::is_sorted(m_points_to_add.rbegin(), m_points_to_add.rend())); -} - -AbstractPointsCommand::AbstractPointsCommand(const QString& label, - PathObject& path_object, - std::deque&& points_to_remove) - : Command(label) - , m_path_object(path_object) - , m_points_to_remove(std::move(points_to_remove)) -{ - std::sort(m_points_to_remove.rbegin(), m_points_to_remove.rend()); - assert(std::is_sorted(m_points_to_remove.rbegin(), m_points_to_remove.rend())); -} - -void AbstractPointsCommand::add() -{ - assert(m_points_to_remove.empty()); - for (auto& located_path : m_points_to_add) { - auto located_path_view = located_path.insert_into(m_path_object.geometry()); - m_points_to_remove.push_front(located_path_view); - } - m_points_to_add.clear(); - m_path_object.update(); - m_path_object.scene()->update_tool(); -} - -void AbstractPointsCommand::remove() -{ - assert(m_points_to_add.empty()); - for (const auto& segment_view : m_points_to_remove) { - m_points_to_add.push_front([this, &view=segment_view]() { - const auto remove_segment = view.size == view.path->size(); - if (remove_segment) { - auto owned_segment = m_path_object.geometry().remove_path(*view.path); - return OwnedLocatedPath{std::move(owned_segment)}; - } else { - auto points = view.path->extract(view.index, view.size); - return OwnedLocatedPath{view.path, view.index, std::move(points)}; - } - }()); - } - m_points_to_remove.clear(); - m_path_object.update(); - m_path_object.scene()->update_tool(); -} - -AbstractPointsCommand::~AbstractPointsCommand() = default; - -AddPointsCommand::AddPointsCommand(PathObject& path_object, std::deque&& added_points) - : AbstractPointsCommand(static_label(), path_object, std::move(added_points)) -{ -} - -void AddPointsCommand::redo() -{ - add(); -} - -void AddPointsCommand::undo() -{ - remove(); -} - -QString AddPointsCommand::static_label() -{ - return QObject::tr("AddPointsCommand"); -} - -RemovePointsCommand::RemovePointsCommand(PathObject& path_object, std::deque&& removed_points) - : AbstractPointsCommand(QObject::tr("RemovePointsCommand"), path_object, std::move(removed_points)) -{ -} - -void RemovePointsCommand::redo() -{ - remove(); -} - -void RemovePointsCommand::undo() -{ - add(); -} - -AbstractPointsCommand::OwnedLocatedPath:: -OwnedLocatedPath(Path* path, std::size_t index, std::deque >&& points) - : m_path(path), m_index(index), m_points(std::move(points)) -{ - assert(m_path != nullptr); - assert(index <= path->size()); - assert(!m_points.empty()); -} - -AbstractPointsCommand::OwnedLocatedPath::OwnedLocatedPath(std::unique_ptr path) - : m_owned_path(std::move(path)), m_index(0) -{ - assert(m_owned_path != nullptr); -} - -AbstractPointsCommand::OwnedLocatedPath::~OwnedLocatedPath() = default; - -PathView AbstractPointsCommand::OwnedLocatedPath::insert_into(PathVector& path_vector) -{ - if (m_path == nullptr) { - auto& path = path_vector.add_path(std::move(m_owned_path)); - return PathView{path, 0, path.size()}; - } else { - const auto n_points = m_points.size(); - m_path->insert_points(m_index, std::move(m_points)); - return PathView{*m_path, m_index, n_points}; - } -} - -bool operator<(const AbstractPointsCommand::OwnedLocatedPath& a, - const AbstractPointsCommand::OwnedLocatedPath& b) -{ - static constexpr auto as_tuple = [](const auto& ola) { - return std::tuple{ola.m_path, ola.m_owned_path.get(), ola.m_index}; - }; - - // NOLINTNEXTLINE(modernize-use-nullptr) - return as_tuple(a) < as_tuple(b); -} - } // namespace omm diff --git a/src/commands/modifypointscommand.h b/src/commands/modifypointscommand.h index 03c8d8c6e..3d62e9342 100644 --- a/src/commands/modifypointscommand.h +++ b/src/commands/modifypointscommand.h @@ -3,22 +3,18 @@ #include "commands/command.h" #include #include -#include "path/pathview.h" namespace omm { -class Path; class PathPoint; -class PathObject; -class PathVector; class Point; -struct PathView; class ModifyPointsCommand : public Command { public: - ModifyPointsCommand(const std::map& points); + using ModifiedPointsMap = std::map; + ModifyPointsCommand(ModifiedPointsMap points); void redo() override; void undo() override; [[nodiscard]] int id() const override; @@ -26,73 +22,8 @@ class ModifyPointsCommand : public Command [[nodiscard]] bool is_noop() const override; private: - std::map m_data; + ModifiedPointsMap m_data; void exchange(); }; -class AbstractPointsCommand : public Command -{ -public: - class OwnedLocatedPath - { - public: - explicit OwnedLocatedPath(Path* path, std::size_t index, std::deque>&& points); - explicit OwnedLocatedPath(std::unique_ptr path); - ~OwnedLocatedPath(); - OwnedLocatedPath(OwnedLocatedPath&& other) = default; - OwnedLocatedPath& operator=(OwnedLocatedPath&& other) = default; - OwnedLocatedPath(const OwnedLocatedPath& other) = delete; - OwnedLocatedPath& operator=(const OwnedLocatedPath& other) = delete; - PathView insert_into(PathVector& path_vector); - friend bool operator<(const OwnedLocatedPath& a, const OwnedLocatedPath& b); - - private: - Path* m_path = nullptr; - std::unique_ptr m_owned_path{}; - std::size_t m_index; - std::deque> m_points; - }; - -protected: - explicit AbstractPointsCommand(const QString& label, - PathObject& path_object, - std::deque&& points_to_add); - explicit AbstractPointsCommand(const QString& label, - PathObject& path_object, - std::deque&& points_to_remove); - void add(); - void remove(); - - [[nodiscard]] Scene& scene() const; - ~AbstractPointsCommand() override; - -public: - AbstractPointsCommand(const AbstractPointsCommand&) = delete; - AbstractPointsCommand(AbstractPointsCommand&&) = delete; - AbstractPointsCommand& operator=(const AbstractPointsCommand&) = delete; - AbstractPointsCommand& operator=(AbstractPointsCommand&&) = delete; - -private: - PathObject& m_path_object; - std::deque m_points_to_add; - std::deque m_points_to_remove; -}; - -class AddPointsCommand : public AbstractPointsCommand -{ -public: - AddPointsCommand(PathObject& path_object, std::deque&& added_points); - void redo() override; - void undo() override; - static QString static_label(); -}; - -class RemovePointsCommand : public AbstractPointsCommand -{ -public: - RemovePointsCommand(PathObject& path_object, std::deque&& removed_points); - void redo() override; - void undo() override; -}; - } // namespace omm diff --git a/src/commands/ownedlocatedpath.cpp b/src/commands/ownedlocatedpath.cpp new file mode 100644 index 000000000..2a1ea1d3a --- /dev/null +++ b/src/commands/ownedlocatedpath.cpp @@ -0,0 +1,88 @@ +#include "commands/ownedlocatedpath.h" + +#include "path/edge.h" +#include "path/path.h" + +#include + + +namespace omm +{ + +OwnedLocatedPath::OwnedLocatedPath(Path* const path, + const std::size_t point_offset, + std::deque> points) + : m_path(path) + , m_point_offset(point_offset) + , m_points(std::move(points)) +{ + assert(std::none_of(m_points.begin(), m_points.end(), [](const auto& p) { return p.get() == nullptr; })); +} + +OwnedLocatedPath::~OwnedLocatedPath() = default; + +std::deque> OwnedLocatedPath::create_edges() const +{ + std::deque> edges; + for (std::size_t i = 1; i < m_points.size(); ++i) { + edges.emplace_back(std::make_unique(std::move(m_points[i - 1]), std::move(m_points[i]), m_path)); + } + + std::shared_ptr front = edges.empty() ? m_points.front() : edges.front()->a(); + std::shared_ptr back = edges.empty() ? m_points.back() : edges.back()->b(); + + if (m_point_offset > 0) { + // if there is something left of this, add the linking edge + std::shared_ptr right_fringe; + if (m_path->edges().empty()) { + right_fringe = m_path->last_point(); + } else if (m_point_offset > 1) { + right_fringe = m_path->edges()[m_point_offset - 2]->b(); + } else { + right_fringe = m_path->edges()[m_point_offset - 1]->a(); + } + edges.emplace_front(std::make_unique(right_fringe, front, m_path)); + } + + if (m_point_offset < m_path->points().size()) { + // if there is something right of this, add the linking edge + std::shared_ptr left_fringe; + if (m_path->edges().empty()) { + left_fringe = m_path->first_point(); + } else if (m_point_offset > 0) { + left_fringe = m_path->edges().at(m_point_offset - 1)->b(); + } else { + left_fringe = m_path->edges().at(m_point_offset)->a(); + } + edges.emplace_back(std::make_unique(back, left_fringe, m_path)); + } + + assert(Path::is_valid(edges)); + return edges; +} + +std::shared_ptr OwnedLocatedPath::single_point() const +{ + if (m_points.size() == 1 && m_path->points().size() == 0) { + return m_points.front(); + } else { + return {}; + } +} + +std::size_t OwnedLocatedPath::point_offset() const +{ + return m_point_offset; +} + +Path* OwnedLocatedPath::path() const +{ + return m_path; +} + +std::set OwnedLocatedPath::points() const +{ + return util::transform(m_points, [](const auto& ptr) { return ptr.get(); }); +} + +} // namespace omm diff --git a/src/commands/ownedlocatedpath.h b/src/commands/ownedlocatedpath.h new file mode 100644 index 000000000..74b8ef08c --- /dev/null +++ b/src/commands/ownedlocatedpath.h @@ -0,0 +1,36 @@ +#pragma once + +#include +#include +#include + +namespace omm +{ + +class Edge; +class Path; +class PathPoint; + +class OwnedLocatedPath +{ +public: + explicit OwnedLocatedPath(Path* path, std::size_t point_offset, std::deque> points); + ~OwnedLocatedPath(); + OwnedLocatedPath(OwnedLocatedPath&& other) = default; + OwnedLocatedPath& operator=(OwnedLocatedPath&& other) = default; + OwnedLocatedPath(const OwnedLocatedPath& other) = delete; + OwnedLocatedPath& operator=(const OwnedLocatedPath& other) = delete; + friend bool operator<(const OwnedLocatedPath& a, const OwnedLocatedPath& b); + std::deque> create_edges() const; + std::shared_ptr single_point() const; + std::size_t point_offset() const; + Path* path() const; + std::set points() const; + +private: + Path* m_path = nullptr; + std::size_t m_point_offset; + std::deque> m_points; +}; + +} // namespace omm diff --git a/src/commands/removepointscommand.cpp b/src/commands/removepointscommand.cpp new file mode 100644 index 000000000..3a0fd9f42 --- /dev/null +++ b/src/commands/removepointscommand.cpp @@ -0,0 +1,48 @@ +#include "commands/removepointscommand.h" +#include "commands/addremovepointscommandchangeset.h" +#include "path/edge.h" +#include "path/path.h" +#include "path/pathview.h" + +namespace +{ + +auto make_change_set_for_remove(const omm::PathView& path_view) +{ + std::deque> edges; + auto& path = path_view.path(); + + if (path_view.begin() > 0 && path_view.end() < path.edges().size() + 1) { + auto& left = *path.edges().at(path_view.begin() - 1); + auto& right = *path.edges().at(path_view.end() - 1); + edges.emplace_back(std::make_unique(left.a(), right.b(), &path)); + } + return omm::AddRemovePointsCommandChangeSet{path_view, std::move(edges), {}}; +} + +} // namespace + +namespace omm +{ + +RemovePointsCommand::RemovePointsCommand(const PathView& points_to_remove, PathObject* const path_object) + : AddRemovePointsCommand(static_label(), ::make_change_set_for_remove(points_to_remove), path_object) +{ +} + +void RemovePointsCommand::undo() +{ + restore_edges(); +} + +void RemovePointsCommand::redo() +{ + restore_bridges(); +} + +QString RemovePointsCommand::static_label() +{ + return QObject::tr("RemovePointsCommand"); +} + +} // namespace omm diff --git a/src/commands/removepointscommand.h b/src/commands/removepointscommand.h new file mode 100644 index 000000000..79af30be7 --- /dev/null +++ b/src/commands/removepointscommand.h @@ -0,0 +1,20 @@ +#pragma once + +#include "commands/addremovepointscommand.h" +#include + +namespace omm +{ + +class PathView; + +class RemovePointsCommand : public AddRemovePointsCommand +{ +public: + explicit RemovePointsCommand(const PathView& points_to_remove, PathObject* path_object = nullptr); + void undo() override; + void redo() override; + static QString static_label(); +}; + +} // namespace omm diff --git a/src/commands/subdividepathcommand.cpp b/src/commands/subdividepathcommand.cpp index 75d00489f..208b13c9e 100644 --- a/src/commands/subdividepathcommand.cpp +++ b/src/commands/subdividepathcommand.cpp @@ -1,5 +1,7 @@ #include "commands/subdividepathcommand.h" +#include "commands/cutpathcommand.h" #include "objects/pathobject.h" +#include "path/edge.h" #include "path/path.h" #include "path/pathpoint.h" #include "path/pathvector.h" @@ -8,27 +10,18 @@ namespace { using namespace omm; -auto compute_cuts(const Path& path) -{ - std::list cuts; - const auto n = path.size(); - const auto points = path.points(); - for (std::size_t i = 0; i < n - 1; ++i) { - if (points[i]->is_selected() && points[i + 1]->is_selected()) { - static const double HALF_TIME = 0.5; - cuts.emplace_back(i, HALF_TIME); - } - } - return std::vector(cuts.begin(), cuts.end()); -} auto compute_cuts(const PathVector& path_vector) { std::list cuts; const auto paths = path_vector.paths(); - for (std::size_t i = 0; i < paths.size(); ++i) { - for (auto&& cut : compute_cuts(*paths[i])) { - cuts.emplace_back(i, cut); + for (std::size_t path_index = 0; path_index < paths.size(); ++path_index) { + const auto edges = paths.at(path_index)->edges(); + for (std::size_t edge_index = 0; edge_index < edges.size(); ++edge_index) { + const auto& edge = *edges.at(edge_index); + if (edge.a()->is_selected() && edge.b()->is_selected()) { + cuts.emplace_back(path_index, edge_index, 0.5); + } } } return std::vector(cuts.begin(), cuts.end()); @@ -38,9 +31,13 @@ auto compute_cuts(const PathVector& path_vector) namespace omm { + SubdividePathCommand::SubdividePathCommand(PathObject& path_object) - : CutPathCommand(QObject::tr("Subdivide Path"), path_object, compute_cuts(path_object.geometry())) + : CutPathCommand(QObject::tr("Subdivide Path"), path_object, compute_cuts(path_object.path_vector())) { + for (auto* const point : new_points()) { + point->set_selected(true); + } } } // namespace omm diff --git a/src/common.h b/src/common.h index b8dcb7d7d..c8f79b196 100644 --- a/src/common.h +++ b/src/common.h @@ -46,7 +46,7 @@ enum class Flag { enum class InterpolationMode { Linear, Smooth, Bezier }; enum class HandleStatus { Hovered, Active, Inactive }; -enum class SceneMode { Object, Vertex }; +enum class SceneMode { Object, Vertex, Face }; } // namespace omm @@ -100,14 +100,10 @@ SetA merge(SetA&& a, SetB&& b, Sets&&... sets) } template -bool contains(const Container& set, S&& key) +bool contains(const Container& set, const S& key) + requires requires { { *begin(set) == key } -> std::same_as; } { - if constexpr (std::is_pointer_v || std::is_reference_v) { - // NOLINTNEXTLINE(cppcoreguidelines-pro-type-const-cast) - return std::find(set.begin(), set.end(), const_cast>(key)) != set.end(); - } else { - return std::find(set.begin(), set.end(), key) != set.end(); - } + return std::find_if(begin(set), end(set), [&key](const auto& v) { return v == key; }) != end(set); } template bool contains(const std::map& map, S&& key) @@ -370,6 +366,7 @@ template auto find_coherent_ranges(const Vs& vs, F&& f) { std::size_t start; std::size_t size; + bool operator<(const Range& other) const noexcept { return start < other.start; } }; std::deque ranges; @@ -396,7 +393,5 @@ template auto python_like_mod(const T& dividend, const T& divisor) } // namespace omm -template<> struct omm::EnableBitMaskOperators : std::true_type { -}; -template<> struct omm::EnableBitMaskOperators : std::true_type { -}; +template<> struct omm::EnableBitMaskOperators : std::true_type { }; +template<> struct omm::EnableBitMaskOperators : std::true_type { }; diff --git a/src/config.h.in b/src/config.h.in index 6148c152f..b2c0626e5 100644 --- a/src/config.h.in +++ b/src/config.h.in @@ -8,3 +8,6 @@ static constexpr auto ommpfritt_version_patch = "@CMAKE_PROJECT_VERSION_PATCH@"; static constexpr auto source_directory = "@CMAKE_SOURCE_DIR@"; static constexpr auto qt_qm_path = "@qt_qm_path@"; std::string_view git_describe(); + +#cmakedefine01 DRAW_POINT_IDS +#cmakedefine01 PATH_DRAW_DIRECTION diff --git a/src/disjointset.h b/src/disjointset.h deleted file mode 100644 index 72f68c1bb..000000000 --- a/src/disjointset.h +++ /dev/null @@ -1,111 +0,0 @@ -#pragma once - -#include -#include -#include "common.h" - -namespace omm -{ - -template class DisjointSetForest; -template void swap(DisjointSetForest& a, DisjointSetForest& b) noexcept; - -/** - * @brief The DisjointSetForest class implements the disjoint set data structure. - * It does not follow the standard implementation because we typically want to lookup all - * members of a set. - * The classical find method only checks whether two items belong to the same set. - */ -template class DisjointSetForest -{ -public: - using Joint = ::transparent_set; - DisjointSetForest(std::deque&& forest = {}) - : m_forest(forest) - { - } - -private: - void join(const Joint& items_to_join, Joint& join_target) - { - for (auto it = m_forest.begin(); it != m_forest.end(); ++it) { - if (!sets_disjoint(items_to_join, *it) && &*it != &join_target) { - join_target.insert(it->begin(), it->end()); - it->clear(); - } - } - std::erase_if(m_forest, [](const auto& set) { return set.empty(); }); - } - -public: - static bool sets_disjoint(const Joint& a, const Joint& b) - { - return a.end() == std::find_first_of(a.begin(), a.end(), b.begin(), b.end()); - } - - /** - * @brief insert insert set into the forest. - * If any item of set is already known, the sets are joined accordingly. - * Examples: - * - insert {A, B} into () -> ({A, B}) - * - insert {C, D} into ({A, B}) -> ({A, B}, {C, D}) - * - insert {B, E} into ({A, B}, {C, D}) -> ({A, B, E}, {C, D}) - * - insert {A, C} into ({A, B}, {C, D, E}) -> ({A, B, C, D, E}) - * @return The set from the forest that includes the given set - */ - Joint insert(const Joint& set) - { - for (auto it = m_forest.begin(); it != m_forest.end(); ++it) { - if (!sets_disjoint(set, *it)) { - it->insert(set.begin(), set.end()); - join(set, *it); - return *it; - } - } - m_forest.push_back(set); - return set; - } - - /** - * @brief get returns the set that contains `key` or the empty set if there is no such. - */ - template Joint get(const K& key) const - { - for (const auto& set : m_forest) { - if (set.contains(key)) { - return set; - } - } - return {}; - } - - /** - * @brief remove removes all items in `set` from the forest. - */ - void remove(const Joint& set) - { - const auto overlap = [&set](const auto& other_set) { return !sets_disjoint(other_set, set); }; - const auto it = std::remove_if(m_forest.begin(), m_forest.end(), overlap); - m_forest.erase(it, m_forest.end()); - } - - friend void swap<>(DisjointSetForest& a, DisjointSetForest& b) noexcept; - - const std::deque& sets() const { return m_forest; } - -protected: - std::deque m_forest; - void remove_empty_sets() - { - m_forest.erase(std::remove_if(m_forest.begin(), m_forest.end(), [](const auto& set) { - return set.empty(); - }), m_forest.end()); - } -}; - -template void swap(DisjointSetForest& a, DisjointSetForest& b) noexcept -{ - swap(a.m_forest, b.m_forest); -} - -} // namespace omm diff --git a/src/facelist.cpp b/src/facelist.cpp new file mode 100644 index 000000000..8217cc1bf --- /dev/null +++ b/src/facelist.cpp @@ -0,0 +1,114 @@ +#include "facelist.h" +#include "path/face.h" +#include "path/edge.h" +#include "path/pathpoint.h" +#include "path/pathvector.h" +#include "serializers/deserializerworker.h" +#include "serializers/serializerworker.h" +#include "serializers/abstractdeserializer.h" +#include "transform.h" +#include "objects/pathobject.h" + +namespace +{ + +//static constexpr auto FACES_POINTER = "faces"; +//static constexpr auto PATH_ID_POINTER = "path"; + +} // namespace + +namespace omm +{ + + +FaceList::FaceList() = default; +FaceList::~FaceList() = default; + +FaceList::FaceList(const FaceList& other) + : m_path_object(other.m_path_object) + , m_faces(::copy(other.m_faces)) +{ +} + +FaceList::FaceList(FaceList&& other) noexcept +{ + swap(*this, other); +} + +FaceList& FaceList::operator=(FaceList other) +{ + swap(*this, other); + return *this; +} + +FaceList& FaceList::operator=(FaceList&& other) noexcept +{ + swap(*this, other); + return *this; +} + +void swap(FaceList& a, FaceList& b) noexcept +{ + std::swap(a.m_path_object, b.m_path_object); + std::swap(a.m_faces, b.m_faces); +} + +void FaceList::serialize(serialization::SerializerWorker& worker) const +{ + (void) worker; +} + +void FaceList::deserialize(serialization::DeserializerWorker& worker) +{ + (void) worker; +} + +bool FaceList::operator==(const FaceList& other) const +{ + if (m_path_object != other.m_path_object || m_faces.size() != other.m_faces.size()) { + return false; + } + + for (std::size_t i = 0; i < m_faces.size(); ++i) { + if (*m_faces[i] != *other.m_faces[i]) { + return false; + } + } + return true; +} + +bool FaceList::operator!=(const FaceList& other) const +{ + return !(*this == other); +} + +bool FaceList::operator<(const FaceList& other) const +{ + if (m_path_object == nullptr || other.m_path_object == nullptr) { + return m_path_object < other.m_path_object; + } + + if (m_path_object != other.m_path_object) { + return m_path_object->id() < other.m_path_object->id(); + } + + if (m_faces.size() != other.m_faces.size()) { + return m_faces.size() < other.m_faces.size(); + } + + for (std::size_t i = 0; i < m_faces.size(); ++i) { + auto& face_i = *m_faces.at(i); + auto& other_face_i = *other.m_faces.at(i); + if (face_i != other_face_i) { + return face_i < other_face_i; + } + } + return false; // face lists are equal +} + +std::deque FaceList::faces() const +{ + return util::transform(m_faces, [](const auto& face) { return *face; }); +} + +} // namespace omm diff --git a/src/facelist.h b/src/facelist.h new file mode 100644 index 000000000..54cbb4f2a --- /dev/null +++ b/src/facelist.h @@ -0,0 +1,43 @@ +#pragma once + +#include +#include "memory" + +namespace omm +{ + +namespace serialization +{ +class SerializerWorker; +class DeserializerWorker; +} // namespace serialization + +class Face; +class PathObject; + +class FaceList +{ +public: + explicit FaceList(); + ~FaceList(); + FaceList(const FaceList& other); + FaceList(FaceList&& other) noexcept; + FaceList& operator=(FaceList other); + FaceList& operator=(FaceList&& other) noexcept; + friend void swap(FaceList& a, FaceList& b) noexcept; + class ReferencePolisher; + void serialize(serialization::SerializerWorker& worker) const; + void deserialize(serialization::DeserializerWorker& worker); + [[nodiscard]] bool operator==(const FaceList& other) const; + [[nodiscard]] bool operator!=(const FaceList& other) const; + [[nodiscard]] bool operator<(const FaceList& other) const; + + PathObject* path_object() const; + std::deque faces() const; + +private: + PathObject* m_path_object = nullptr; + std::deque> m_faces; +}; + +} // namespace diff --git a/src/geometry/CMakeLists.txt b/src/geometry/CMakeLists.txt index 1cf9ce90b..2d2b6b591 100644 --- a/src/geometry/CMakeLists.txt +++ b/src/geometry/CMakeLists.txt @@ -1,10 +1,15 @@ target_sources(libommpfritt PRIVATE boundingbox.cpp boundingbox.h + direction.h + direction.cpp + line.h + line.cpp matrix.cpp matrix.h objecttransformation.cpp objecttransformation.h + orientedposition.h point.cpp point.h polarcoordinates.cpp diff --git a/src/geometry/boundingbox.cpp b/src/geometry/boundingbox.cpp index 8d1a3e9b7..e354cf695 100644 --- a/src/geometry/boundingbox.cpp +++ b/src/geometry/boundingbox.cpp @@ -26,11 +26,12 @@ double max(const std::set& ds) std::set get_all_control_points(const std::set& points) { - std::set control_points; + std::set control_points; //TODO I think a list would be more appropriate here. for (auto&& p : points) { - control_points.insert(p.left_position()); - control_points.insert(p.position()); - control_points.insert(p.right_position()); + control_points.emplace(p.position()); + for (const auto& key : ::get_keys(p.tangents())) { + control_points.emplace(p.tangent_position(key)); + } } return control_points; } diff --git a/src/geometry/direction.cpp b/src/geometry/direction.cpp new file mode 100644 index 000000000..54a05f51b --- /dev/null +++ b/src/geometry/direction.cpp @@ -0,0 +1,6 @@ +#include "geometry/direction.h" + +namespace omm +{ + +} // namespace omm diff --git a/src/geometry/direction.h b/src/geometry/direction.h new file mode 100644 index 000000000..080145097 --- /dev/null +++ b/src/geometry/direction.h @@ -0,0 +1,12 @@ +#pragma once + +namespace omm +{ + +enum class Direction { Forward = 0, Backward = 1 }; +constexpr Direction other(const Direction d) +{ + return static_cast(1 - static_cast(d)); +} + +} // namespace omm diff --git a/src/geometry/line.cpp b/src/geometry/line.cpp new file mode 100644 index 000000000..26d6084d9 --- /dev/null +++ b/src/geometry/line.cpp @@ -0,0 +1,45 @@ +#include "geometry/line.h" +#include "fmt/format.h" + +namespace omm +{ + +double Line::intersect(const Line& other) const noexcept +{ + // https://en.wikipedia.org/wiki/Line%E2%80%93line_intersection#Given_two_points_on_each_line_segment + const auto d = (a.x - other.a.x) * (other.a.y - other.b.y) - (a.y - other.a.y) * (other.a.x - other.b.x); + const auto n = (a.x - b.x) * (other.a.y - other.b.y) - (a.y - b.y) * (other.a.x - other.b.x); + return d / n; +} + +Vec2f Line::lerp(const double t) const noexcept +{ + return a + t * (b - a); +} + +QString Line::to_string() const +{ + return QString("Line({%1, %2}, {%3, %4})").arg(a.x).arg(a.y).arg(b.x).arg(b.y); +} + +double Line::distance(const Vec2f& p) const noexcept +{ + // https://en.wikipedia.org/wiki/Line%E2%80%93line_intersection#Given_two_points_on_each_line_segment + const auto d = (b.x - a.x) * (a.y - p.y) - (a.x - p.x) * (b.y - a.y); + const auto n = (a - b).euclidean_norm(); + return d / n; +} + +double Line::project(const Vec2f& p) const noexcept +{ + const auto ab = b - a; + const auto ap = p - a; + return Vec2f::dot(ap, ab) / ab.euclidean_norm2(); +} + +std::ostream& operator<<(std::ostream& os, const Line& line) +{ + return os << fmt::format("Line[({}, {}), ({}, {})]", line.a.x, line.a.y, line.b.x, line.b.y); +} + +} // namespace omm diff --git a/src/geometry/line.h b/src/geometry/line.h new file mode 100644 index 000000000..b495e3e9a --- /dev/null +++ b/src/geometry/line.h @@ -0,0 +1,47 @@ +#pragma once + +#include "geometry/vec2.h" +#include + +namespace omm +{ + +struct Line +{ + Vec2f a; + Vec2f b; + + /** + * @brief line_intersection computes the intersection of lines this and other. + * @param other the other line + * @return The intersection in line coordinates (wrt. this). + */ + [[nodiscard]] double intersect(const Line& other) const noexcept; + + /** + * @brief distance computes the distance between point p and this line. + * @param p the point to test + * @note This line is assumed to extend infintely beyond. + * @note The distance is oriented, i.e., may be negative. + */ + [[nodiscard]] double distance(const Vec2f& p) const noexcept; + + /** + * @brief projects p onto this line and returns the line coordinate of the projection. + * @param p the point to project + * @note The position of the projection can be computed with lerp(project(p)). + */ + [[nodiscard]] double project(const Vec2f& p) const noexcept; + + /** + * @brief lerp linearly interpolates between the start and the end point. + * @param t + * @return The start point if t=0 or the end point if t=1.0. + */ + [[nodiscard]] Vec2f lerp(double t) const noexcept; + + [[nodiscard]] QString to_string() const; + friend std::ostream& operator<<(std::ostream& os, const Line& line); +}; + +} // namespace omm diff --git a/src/geometry/objecttransformation.cpp b/src/geometry/objecttransformation.cpp index 88e0db574..4678b91a1 100644 --- a/src/geometry/objecttransformation.cpp +++ b/src/geometry/objecttransformation.cpp @@ -1,12 +1,16 @@ +#include "geometry/objecttransformation.h" + #include "logging.h" +#include "geometry/orientedposition.h" + #include #include #include -#include "geometry/objecttransformation.h" namespace omm { + ObjectTransformation::ObjectTransformation() : m_translation(0, 0), m_scaling(1, 1) { } @@ -235,12 +239,21 @@ ObjectTransformation ObjectTransformation::apply(const ObjectTransformation& t) Point ObjectTransformation::apply(const Point& point) const { - Point p(apply_to_position(point.position())); - p.set_left_tangent(apply_to_direction(point.left_tangent())); - p.set_right_tangent(apply_to_direction(point.right_tangent())); + Point p = point; + p.set_position(apply_to_position(point.position())); + for (auto& [key, tangent] : p.tangents()) { + tangent = apply_to_direction(tangent); + } return p; } +OrientedPosition ObjectTransformation::apply(const OrientedPosition& op) const +{ + static constexpr auto arbitrary_magnitude = 1.0; + const PolarCoordinates pc(op.rotation, arbitrary_magnitude); + return {apply_to_position(op.position), apply_to_direction(pc).argument}; +} + PolarCoordinates ObjectTransformation::apply_to_position(const PolarCoordinates& point) const { // TODO isn't there something smarter? diff --git a/src/geometry/objecttransformation.h b/src/geometry/objecttransformation.h index 8302871f0..f01836dea 100644 --- a/src/geometry/objecttransformation.h +++ b/src/geometry/objecttransformation.h @@ -13,6 +13,7 @@ namespace omm class Point; struct PolarCoordinates; +struct OrientedPosition; class ObjectTransformation { @@ -59,6 +60,7 @@ class ObjectTransformation [[nodiscard]] BoundingBox apply(const BoundingBox& bb) const; [[nodiscard]] ObjectTransformation apply(const ObjectTransformation& t) const; [[nodiscard]] Point apply(const Point& point) const; + [[nodiscard]] OrientedPosition apply(const OrientedPosition& op) const; [[nodiscard]] ObjectTransformation normalized() const; [[nodiscard]] bool contains_nan() const; [[nodiscard]] bool is_identity() const; diff --git a/src/geometry/orientedposition.h b/src/geometry/orientedposition.h new file mode 100644 index 000000000..2089dcf45 --- /dev/null +++ b/src/geometry/orientedposition.h @@ -0,0 +1,13 @@ +#pragma once +#include "geometry/vec2.h" + +namespace omm +{ + +struct OrientedPosition +{ + Vec2f position; + double rotation; +}; + +} diff --git a/src/geometry/point.cpp b/src/geometry/point.cpp index 2ae031740..6a9f9bd6f 100644 --- a/src/geometry/point.cpp +++ b/src/geometry/point.cpp @@ -4,23 +4,31 @@ #include "serializers/serializerworker.h" #include +namespace +{ + +constexpr auto KEY_KEY = "key"; +constexpr auto VAL_KEY = "val"; +constexpr auto PATH_KEY = "path"; +constexpr auto DIRECTION_KEY = "direction"; +constexpr auto PATH_IS_SET_KEY = "path is set"; + +} // namespace + namespace omm { -Point::Point(const Vec2f& position, - const PolarCoordinates& left_tangent, - const PolarCoordinates& right_tangent) - : m_position(position), m_left_tangent(left_tangent), m_right_tangent(right_tangent) + +Point::Point(const Vec2f& position) : Point(position, {}) { } -Point::Point(const Vec2f& position, const double rotation, const double tangent_length) - : Point(position, - PolarCoordinates(rotation, tangent_length), - PolarCoordinates(M_PI + rotation, tangent_length)) +Point::Point(const Vec2f& position, const PolarCoordinates& backward_tangent, const PolarCoordinates& forward_tangent) + : Point(position, {{Direction::Backward, backward_tangent}, {Direction::Forward, forward_tangent}}) { } -Point::Point(const Vec2f& position) : Point(position, 0.0, 0.0) +Point::Point(const Vec2f& position, const std::map& tangents) + : m_position(position), m_tangents(tangents) { } @@ -38,140 +46,159 @@ void Point::set_position(const Vec2f& position) m_position = position; } -Vec2f Point::left_position() const +Vec2f Point::tangent_position(const TangentKey& key) const { - return m_position + m_left_tangent.to_cartesian(); + return m_position + tangent(key).to_cartesian(); } -void Point::set_left_position(const Vec2f& position) +void Point::set_tangent_position(const TangentKey& key, const Vec2f& position) { - m_left_tangent = PolarCoordinates(position - m_position); + set_tangent(key, PolarCoordinates(position - m_position)); } -Vec2f Point::right_position() const +PolarCoordinates Point::tangent(const TangentKey& key) const { - return m_position + m_right_tangent.to_cartesian(); + return m_tangents.at(key); } -void Point::set_right_position(const Vec2f& position) +void Point::set_tangent(const TangentKey& key, const PolarCoordinates& vector) { - m_right_tangent = PolarCoordinates(position - m_position); + m_tangents[key] = vector; } -PolarCoordinates Point::left_tangent() const +Point::TangentsMap& Point::tangents() { - return m_left_tangent; + return m_tangents; } -void Point::set_left_tangent(const PolarCoordinates& vector) +const Point::TangentsMap& Point::tangents() const { - m_left_tangent = vector; + return m_tangents; } -PolarCoordinates Point::right_tangent() const +/** + * @brief replace_tangents_key replaces the key `{old_path, *}` with `{new_path, *}` + * or adds null-tangents at `{new_path, *}` (`*` stands for both directions). + * `old_path` and `new_path` are passed as key-value pairs via `paths_map`. + */ +void Point::replace_tangents_key(const std::map& paths_map) { - return m_right_tangent; -} - -void Point::set_right_tangent(const PolarCoordinates& vector) -{ - m_right_tangent = vector; + for (const auto& [old_path, new_path] : paths_map) { + for (const auto& direction : {omm::Direction::Backward, omm::Direction::Forward}) { + const auto node = m_tangents.extract({old_path, direction}); + m_tangents.try_emplace({new_path, direction}, node.empty() ? omm::PolarCoordinates() : node.mapped()); + } + } } void swap(Point& a, Point& b) { swap(a.m_position, b.m_position); - swap(a.m_left_tangent, b.m_left_tangent); - swap(a.m_right_tangent, b.m_right_tangent); + swap(a.m_tangents, b.m_tangents); } bool Point::has_nan() const { - return m_position.has_nan() || m_left_tangent.has_nan() || m_right_tangent.has_nan(); + return m_position.has_nan() || std::any_of(m_tangents.begin(), m_tangents.end(), [](const auto& p) { + return p.second.has_nan(); + }); } bool Point::has_inf() const { - return m_position.has_inf() || m_left_tangent.has_inf() || m_right_tangent.has_inf(); + return m_position.has_inf() || std::any_of(m_tangents.begin(), m_tangents.end(), [](const auto& p) { + return p.second.has_inf(); + }); } double Point::rotation() const { - return PolarCoordinates(m_left_tangent).argument; -} - -Point Point::rotated(const double rad) const -{ - auto copy = *this; - copy.m_left_tangent.argument += rad; - copy.m_right_tangent.argument += rad; - return copy; + if (const auto it = m_tangents.begin(); it != m_tangents.end()) { + return it->second.argument; + } else { + return 0.0; + } } Point Point::nibbed() const { auto copy = *this; - copy.m_left_tangent.magnitude = 0; - copy.m_right_tangent.magnitude = 0; + for (auto& [key, tangent] : copy.m_tangents) { + tangent.magnitude = 0.0; + } return copy; } -Point Point::flipped() const -{ - return Point{position(), right_tangent(), left_tangent()}; -} - -void Point::serialize(serialization::SerializerWorker& worker) const +void Point::serialize(serialization::SerializerWorker& worker, + const std::map& path_indices) const { worker.sub(POSITION_POINTER)->set_value(m_position); - worker.sub(LEFT_TANGENT_POINTER)->set_value(m_left_tangent); - worker.sub(RIGHT_TANGENT_POINTER)->set_value(m_right_tangent); + worker.sub(TANGENTS_POINTER)->set_value(m_tangents, [&path_indices](const auto& pair, auto& worker) { + pair.first.serialize(*worker.sub(KEY_KEY), path_indices); + worker.sub(VAL_KEY)->set_value(pair.second); + }); } -void Point::deserialize(serialization::DeserializerWorker& worker) +void Point::deserialize(serialization::DeserializerWorker& worker, const std::vector paths) { m_position = worker.sub(POSITION_POINTER)->get(); - m_left_tangent = worker.sub(LEFT_TANGENT_POINTER)->get(); - m_right_tangent = worker.sub(RIGHT_TANGENT_POINTER)->get(); + worker.sub(TANGENTS_POINTER)->get_items([&paths, this](auto& worker) { + const auto value = worker.sub(VAL_KEY)->template get(); + TangentKey key; + key.deserialize(*worker.sub(KEY_KEY), paths); + m_tangents.emplace(key, value); + }); } -Point Point::flattened(const double t) const +QString Point::to_string() const { - Point copy(*this); - double center = (m_left_tangent.argument + m_right_tangent.argument) / 2.0; - if (center - m_left_tangent.argument < 0) { - center += M_PI; + static constexpr bool verbose = false; + if constexpr (verbose) { + const auto tangents = util::transform(m_tangents, [](const auto& pair) { + const auto& [key, tangent] = pair; + return QString("%1: %2").arg(key.to_string(), tangent.to_string()); + }); + return QString{"Point[%1;\n %2]"}.arg(m_position.to_string(), QStringList(tangents).join("\n ")); + } else { + return QString{"[%1]"}.arg(m_position.to_string()); } +} - const auto lerp_angle = [](double rad1, double rad2, const double t) { - const Vec2f v = t * PolarCoordinates(rad2, 1.0).to_cartesian() - + (1 - t) * PolarCoordinates(rad1, 1.0).to_cartesian(); - return (v / 2.0).arg(); - }; +QRectF Point::bounding_box() const +{ + const auto get_tangent_position = [this](const auto& t) { return tangent_position(t.first); }; + auto ps = util::transform(m_tangents, get_tangent_position); + ps.emplace_back(position()); - copy.m_left_tangent.argument = lerp_angle(m_left_tangent.argument, center - M_PI_2, t); - copy.m_right_tangent.argument = lerp_angle(m_right_tangent.argument, center + M_PI_2, t); + static constexpr auto cmp_x = [](const auto& a, const auto& b) { return a.x < b.x; }; + static constexpr auto cmp_y = [](const auto& a, const auto& b) { return a.y < b.y; }; - return copy; + const QPointF min(std::min_element(ps.begin(), ps.end(), cmp_x)->x, + std::min_element(ps.begin(), ps.end(), cmp_y)->y); + const QPointF max(std::max_element(ps.begin(), ps.end(), cmp_x)->x, + std::max_element(ps.begin(), ps.end(), cmp_y)->y); + return {min, max}; } -QString Point::to_string() const +QRectF Point::bounding_box(const std::list& points) { - static constexpr bool verbose = false; - if constexpr (verbose) { - return QString{"Point[%1, %2, %3]"}.arg(m_position.to_string(), - m_left_tangent.to_string(), - m_right_tangent.to_string()); - } else { - return QString{"[%1]"}.arg(m_position.to_string()); - } + static constexpr auto get_bb = [](const auto& p) { return p.bounding_box(); }; + const auto bbs = util::transform(points, get_bb); + const auto tls = util::transform(bbs, &QRectF::topLeft); + const auto brs = util::transform(bbs, &QRectF::bottomRight); + static constexpr auto cmp_x = [](const auto& a, const auto& b) { return a.x() < b.x(); }; + static constexpr auto cmp_y = [](const auto& a, const auto& b) { return a.y() < b.y(); }; + + const QPointF tl(std::min_element(tls.begin(), tls.end(), cmp_x)->x(), + std::min_element(tls.begin(), tls.end(), cmp_y)->y()); + const QPointF br(std::max_element(brs.begin(), brs.end(), cmp_x)->x(), + std::max_element(brs.begin(), brs.end(), cmp_y)->y()); + return {tl, br}; } bool Point::operator==(const Point& point) const { - return m_position == point.m_position - && m_left_tangent.to_cartesian() == point.m_left_tangent.to_cartesian() - && m_right_tangent.to_cartesian() == point.m_right_tangent.to_cartesian(); + return m_position == point.m_position && m_tangents == point.m_tangents; } bool Point::operator!=(const Point& point) const @@ -179,122 +206,93 @@ bool Point::operator!=(const Point& point) const return !(*this == point); } -PolarCoordinates Point::mirror_tangent(const PolarCoordinates& old_pos, - const PolarCoordinates& old_other_pos, - const PolarCoordinates& new_other_pos) +bool Point::operator<(const Point& point) const { - PolarCoordinates new_pos; - static constexpr double mag_eps = 0.00001; - new_pos.argument = old_pos.argument + new_other_pos.argument - old_other_pos.argument; - if (old_other_pos.magnitude > mag_eps) { - new_pos.magnitude = old_pos.magnitude * new_other_pos.magnitude / old_other_pos.magnitude; + if (m_position == point.m_position) { + return m_tangents < point.m_tangents; } else { - new_pos.magnitude = new_other_pos.magnitude; + return m_position < point.m_position; } - return new_pos; } -double Point::get_direction(const Point* left_neighbor, const Point* right_neighbor) const +bool fuzzy_eq(const Point& a, const Point& b) { - double left_arg = 0; - double right_arg = 0; - bool has_left_direction = true; - bool has_right_direction = true; - static constexpr auto eps = 0.001; - - if (m_right_tangent.magnitude >= eps) { - left_arg = m_right_tangent.argument; - } else if (left_neighbor != nullptr) { - left_arg = (m_position - left_neighbor->m_position).arg(); - } else { - has_left_direction = false; + if (!fuzzy_eq(a.position(), b.position())) { + return false; } - if (m_left_tangent.magnitude >= eps) { - right_arg = m_left_tangent.argument; - } else if (right_neighbor != nullptr) { - right_arg = (m_position - right_neighbor->m_position).arg(); - } else { - has_right_direction = false; + const auto keys = get_keys(a.m_tangents); + if (keys != get_keys(b.m_tangents)) { + return false; } - if (has_left_direction && has_right_direction) { - double a = (left_arg + right_arg) / 2.0; - if (a - right_arg < 0) { - a -= M_PI; - } - return a; + return std::all_of(keys.begin(), keys.end(), [&a, &b](const auto& key) { + return fuzzy_eq(a.tangent_position(key), b.tangent_position(key)); + }); +} - } else if (has_left_direction) { - return left_arg + M_PI_2; - } else if (has_right_direction) { - return right_arg - M_PI_2; - } else { - LWARNING << "Directed point must have at least one neighbor or tangent"; - return 0.0; - } +bool omm::Point::TangentKey::operator<(const omm::Point::TangentKey& other) const noexcept +{ + static constexpr auto to_tuple = [](const auto& self) { + return std::tuple(self.path, self.direction); + }; + return to_tuple(*this) < to_tuple(other); } -Point Point::offset(double t, const Point* left_neighbor, const Point* right_neighbor) const +bool Point::TangentKey::operator==(const TangentKey& other) const noexcept { - const double arg = get_direction(left_neighbor, right_neighbor); - const auto direction = PolarCoordinates(arg, 1.0).to_cartesian(); - const Vec2f pdirection = direction; // (direction.y, -direction.x); + return path == other.path && direction == other.direction; +} - const auto f = [](const double t, const double mag) { - return mag + std::clamp(t, -mag, 0.0) + std::max(0.0, t) / 2.0; - }; +QString omm::Point::TangentKey::to_string() const +{ + return QString::asprintf("%p-%s", static_cast(path), direction == Direction::Forward ? "fwd" : "bwd"); +} - auto left_tanget = this->m_left_tangent; - auto right_tangent = this->m_right_tangent; - left_tanget.magnitude = f(t, m_left_tangent.magnitude); - right_tangent.magnitude = f(t, right_tangent.magnitude); - Point offset(m_position + t * pdirection, left_tanget, right_tangent); - const double tn = t / Vec2f(left_tanget.magnitude, right_tangent.magnitude).euclidean_norm(); - return offset.flattened(std::clamp(tn, 0.0, 1.0)); +Point::TangentKey::TangentKey(const Path* const path, const Direction direction) : path(path), direction(direction) +{ } -std::vector -Point::offset(const double t, const std::vector& points, const bool is_closed) +Point::TangentKey::TangentKey(const Direction direction) : TangentKey(nullptr, direction) { - const auto n = points.size(); - if (n >= 2) { - std::vector off_points; - off_points.reserve(n); - const auto* left = is_closed ? &points.back() : nullptr; - off_points.push_back(points[0].offset(t, left, &points[1])); +} - for (std::size_t i = 1; i < n - 1; ++i) { - off_points.push_back(points[i].offset(t, &points[i - 1], &points[i + 1])); - } +Point::TangentKey::TangentKey() +{ +} - const auto* right = is_closed ? &points.front() : nullptr; - off_points.push_back(points[n - 1].offset(t, &points[n - 2], right)); - return off_points; - } else if (n == 1) { - return {points[0].offset(t, nullptr, nullptr)}; - } else { - return {}; +void omm::Point::TangentKey::serialize(serialization::SerializerWorker& worker, + const std::map& path_indices) const +{ + worker.sub(PATH_IS_SET_KEY)->set_value(path != nullptr); + if (path != nullptr) { + worker.sub(PATH_KEY)->set_value(path_indices.at(path)); } + worker.sub(DIRECTION_KEY)->set_value(static_cast>(direction)); } -bool Point::operator<(const Point& point) const +void omm::Point::TangentKey::deserialize(serialization::DeserializerWorker& worker, + const std::vector& paths) { - if (m_position == point.m_position) { - if (m_left_tangent == point.m_left_tangent) { - return m_right_tangent < point.m_right_tangent; - } else { - return m_left_tangent < point.m_left_tangent; - } + if (worker.sub(PATH_IS_SET_KEY)->get_bool()) { + this->path = paths.at(worker.sub(PATH_KEY)->get()); } else { - return m_position < point.m_position; + this->path = nullptr; } + this->direction = static_cast(worker.sub(DIRECTION_KEY)->get>()); } -bool fuzzy_eq(const Point& a, const Point& b) +PolarCoordinates Point::mirror_tangent(const PolarCoordinates& old_pos, + const PolarCoordinates& old_other_pos, + const PolarCoordinates& new_other_pos) { - return fuzzy_eq(a.position(), b.position()) && fuzzy_eq(a.left_position(), b.left_position()) - && fuzzy_eq(a.right_position(), b.right_position()); + PolarCoordinates new_pos; + static constexpr double mag_eps = 0.00001; + new_pos.argument = old_pos.argument + new_other_pos.argument - old_other_pos.argument; + if (old_other_pos.magnitude > mag_eps) { + new_pos.magnitude = old_pos.magnitude * new_other_pos.magnitude / old_other_pos.magnitude; + } + return new_pos; } } // namespace omm diff --git a/src/geometry/point.h b/src/geometry/point.h index 604e61ba9..89bc50110 100644 --- a/src/geometry/point.h +++ b/src/geometry/point.h @@ -1,8 +1,14 @@ #pragma once +#include "geometry/direction.h" #include "geometry/polarcoordinates.h" #include "geometry/vec2.h" +#include #include +#include +#include +#include +#include namespace omm { @@ -13,61 +19,79 @@ class SerializerWorker; class DeserializerWorker; } // namespace serialization +class Path; + class Point { public: - explicit Point(const Vec2f& position, - const PolarCoordinates& left_tangent, - const PolarCoordinates& right_tangent); - explicit Point(const Vec2f& position, double rotation, double tangent_length = 1.0); + struct TangentKey + { + TangentKey(const Path* const path, Direction direction); + TangentKey(const Direction direction); + explicit TangentKey(); + const Path* path; + Direction direction; + + void serialize(serialization::SerializerWorker& worker, + const std::map& path_indices) const; + void deserialize(serialization::DeserializerWorker& worker, const std::vector& paths); + + QString to_string() const; + bool operator<(const TangentKey& other) const noexcept; + bool operator==(const TangentKey& other) const noexcept; + }; + + using TangentsMap = std::map; + explicit Point(const Vec2f& position); + explicit Point(const Vec2f& position, const PolarCoordinates& backward_tangent, + const PolarCoordinates& forward_tangent); + explicit Point(const Vec2f& position, const std::map& tangents); Point(); [[nodiscard]] Vec2f position() const; void set_position(const Vec2f& position); - [[nodiscard]] Vec2f left_position() const; - void set_left_position(const Vec2f& position); - [[nodiscard]] Vec2f right_position() const; - void set_right_position(const Vec2f& position); - [[nodiscard]] PolarCoordinates left_tangent() const; - void set_left_tangent(const PolarCoordinates& vector); - [[nodiscard]] PolarCoordinates right_tangent() const; - void set_right_tangent(const PolarCoordinates& vector); - [[nodiscard]] double rotation() const; + + void set_tangent_position(const TangentKey& key, const Vec2f& position); + [[nodiscard]] Vec2f tangent_position(const TangentKey& key) const; + [[nodiscard]] PolarCoordinates tangent(const TangentKey& key) const; + void set_tangent(const TangentKey& key, const PolarCoordinates& vector); + TangentsMap& tangents(); + const TangentsMap& tangents() const; + void replace_tangents_key(const std::map& paths_map); + void set_tangents(TangentsMap tangents); static constexpr auto TYPE = QT_TRANSLATE_NOOP("Point", "Point"); friend void swap(Point& a, Point& b); + [[nodiscard]] double rotation() const; [[nodiscard]] bool has_nan() const; [[nodiscard]] bool has_inf() const; [[nodiscard]] Point rotated(double rad) const; [[nodiscard]] Point nibbed() const; - [[nodiscard]] Point flipped() const; static constexpr auto POSITION_POINTER = "position"; - static constexpr auto LEFT_TANGENT_POINTER = "left"; - static constexpr auto RIGHT_TANGENT_POINTER = "right"; + static constexpr auto TANGENTS_POINTER = "tangents"; - void serialize(serialization::SerializerWorker& worker) const; - void deserialize(serialization::DeserializerWorker& worker); + void serialize(serialization::SerializerWorker& worker, const std::map& path_indices) const; + void deserialize(serialization::DeserializerWorker& worker, const std::vector paths); /** - * @brief flattened means adjust the tangents such that the angle between them approaches - * 180 degree. - * @param t control the amount of the effect. - * t = 0: returns a unmodified copy of the this. - * t = 1: return a copy where the tangents are spread by 180 degree. - * Values outside that range have not been tested, the result of such an operation is a surprise. - * The magnitude of tangents is not modified. The angle bisector is an invariant. - * @return the flattened point. + * @brief operator == returns true iff this equals `point`. Makes sense only if + * - both points are part of the same PathVector + * - or have no tangents assigned to a Path. */ - [[nodiscard]] Point flattened(double t) const; - bool operator==(const Point& point) const; + + /** + * @brief operator != returns true iff this doesn't equal `point`. + * Same constrains apply as for @see operator==. + */ bool operator!=(const Point& point) const; bool operator<(const Point& point) const; + friend bool fuzzy_eq(const Point& a, const Point& b); - Point offset(double t, const Point* left_neighbor, const Point* right_neighbor) const; - static std::vector offset(double t, const std::vector& points, bool is_closed); [[nodiscard]] QString to_string() const; + QRectF bounding_box() const; + static QRectF bounding_box(const std::list& points); /** * @brief When a tangent is at `old_pos` and it is mirror-coupled with its sibling which moves @@ -83,15 +107,11 @@ class Point const PolarCoordinates& new_other_pos); private: - double get_direction(const Point* left_neighbor, const Point* right_neighbor) const; Vec2f m_position; - PolarCoordinates m_left_tangent; - PolarCoordinates m_right_tangent; + TangentsMap m_tangents; }; constexpr PolarCoordinates to_polar(Vec2f cartesian); constexpr Vec2f to_cartesian(const PolarCoordinates& polar); -bool fuzzy_eq(const Point& a, const Point& b); - } // namespace omm diff --git a/src/geometry/polarcoordinates.cpp b/src/geometry/polarcoordinates.cpp index 3e2962c7b..f25516ab3 100644 --- a/src/geometry/polarcoordinates.cpp +++ b/src/geometry/polarcoordinates.cpp @@ -87,4 +87,49 @@ void PolarCoordinates::deserialize(serialization::DeserializerWorker& worker) magnitude = v.y; } +PolarCoordinates operator*(const PolarCoordinates& pc, double d) +{ + return PolarCoordinates(pc.argument, pc.magnitude * d); +} + +PolarCoordinates operator*(double d, const PolarCoordinates& pc) +{ + return pc * d; +} + +PolarCoordinates operator/(const PolarCoordinates& pc, double d) +{ + return pc * (1.0 / d); +} + +PolarCoordinates operator+(const PolarCoordinates& pc, double d) +{ + return PolarCoordinates(pc.argument + d, pc.magnitude); +} + +PolarCoordinates operator-(const PolarCoordinates& pc, double d) +{ + return pc + (-d); +} + +PolarCoordinates& operator+=(PolarCoordinates& pc, double d) +{ + return pc = pc + d; +} + +PolarCoordinates& operator-=(PolarCoordinates& pc, double d) +{ + return pc = pc - d; +} + +PolarCoordinates& operator*=(PolarCoordinates& pc, double d) +{ + return pc = pc * d; +} + +PolarCoordinates& operator/=(PolarCoordinates& pc, double d) +{ + return pc = pc / d; +} + } // namespace omm diff --git a/src/geometry/polarcoordinates.h b/src/geometry/polarcoordinates.h index 23bc1b41d..8316943b2 100644 --- a/src/geometry/polarcoordinates.h +++ b/src/geometry/polarcoordinates.h @@ -42,6 +42,16 @@ struct PolarCoordinates [[nodiscard]] QString to_string() const; void serialize(serialization::SerializerWorker& worker) const; void deserialize(serialization::DeserializerWorker& worker); + + friend PolarCoordinates operator*(const PolarCoordinates& pc, double d); + friend PolarCoordinates operator*(double d, const PolarCoordinates& pc); + friend PolarCoordinates operator/(const PolarCoordinates& pc, double d); + friend PolarCoordinates operator+(const PolarCoordinates& pc, double d); + friend PolarCoordinates operator-(const PolarCoordinates& pc, double d); + friend PolarCoordinates& operator+=(PolarCoordinates& pc, double d); + friend PolarCoordinates& operator-=(PolarCoordinates& pc, double d); + friend PolarCoordinates& operator*=(PolarCoordinates& pc, double d); + friend PolarCoordinates& operator/=(PolarCoordinates& pc, double d); }; } // namespace omm diff --git a/src/main/application.cpp b/src/main/application.cpp index b69b0a25c..9bba34024 100644 --- a/src/main/application.cpp +++ b/src/main/application.cpp @@ -34,7 +34,6 @@ #include "tags/tag.h" #include "tools/tool.h" #include "tools/toolbox.h" -#include "widgets/pointdialog.h" #include "objects/pathobject.h" namespace @@ -89,7 +88,7 @@ auto init_mode_selectors() activation_actions)}); }; - insert("scene_mode", "scene_mode.cycle", {"scene_mode.object", "scene_mode.vertex"}); + insert("scene_mode", "scene_mode.cycle", {"scene_mode.object", "scene_mode.vertex", "scene_mode.face"}); return map; } @@ -117,11 +116,6 @@ bool dispatch_named_action(Application& app, const QString& action_name) app.scene->submit(app.scene->styles(), std::move(style)); }}, {"reset viewport", [&app]() { app.main_window()->viewport().reset(); }}, - {"show point dialog", [&app](){ - if (const auto paths = app.scene->item_selection(); !paths.empty()) { - PointDialog(paths, app.main_window()).exec(); - } - }}, {"preferences", []() { PreferenceDialog().exec(); }} }; diff --git a/src/mainwindow/mainwindow.cpp b/src/mainwindow/mainwindow.cpp index f13ce3949..bbb325b1e 100644 --- a/src/mainwindow/mainwindow.cpp +++ b/src/mainwindow/mainwindow.cpp @@ -127,7 +127,6 @@ std::vector MainWindow::path_menu_entries() "path/fill selection", "path/extend selection", "path/shrink selection", - "path/show point dialog", }; } diff --git a/src/mainwindow/pathactions.cpp b/src/mainwindow/pathactions.cpp index 4c2079950..14ab2f875 100644 --- a/src/mainwindow/pathactions.cpp +++ b/src/mainwindow/pathactions.cpp @@ -6,24 +6,25 @@ #include "commands/objectselectioncommand.h" #include "commands/propertycommand.h" #include "commands/removecommand.h" +#include "commands/removepointscommand.h" #include "commands/subdividepathcommand.h" #include "common.h" #include "main/application.h" #include "mainwindow/mainwindow.h" -#include "properties/optionproperty.h" -#include "properties/referenceproperty.h" #include "objects/pathobject.h" -#include "path/pathpoint.h" +#include "path/face.h" #include "path/path.h" +#include "path/pathpoint.h" #include "path/pathvector.h" +#include "path/pathview.h" +#include "properties/optionproperty.h" +#include "properties/referenceproperty.h" +#include "removeif.h" #include "scene/history/historymodel.h" #include "scene/history/macro.h" #include "scene/mailbox.h" -#include "scene/pointselection.h" #include "scene/scene.h" #include "scene/toplevelsplit.h" -#include "tools/toolbox.h" -#include "removeif.h" #include #include #include @@ -37,7 +38,7 @@ using namespace omm; template void foreach_subpath(Application& app, F&& f) { for (auto* path_object : app.scene->item_selection()) { - for (auto* path : path_object->geometry().paths()) { + for (auto* path : path_object->path_vector().paths()) { f(path); } } @@ -48,21 +49,9 @@ void modify_tangents(InterpolationMode mode, Application& app) std::map map; const auto paths = app.scene->item_selection(); for (PathObject* path_object : paths) { - for (const Path* path : path_object->geometry().paths()) { - const auto points = path->points(); - for (std::size_t i = 0; i < points.size(); ++i) { - PathPoint* point = points[i]; - if (point->is_selected()) { - switch (mode) { - case InterpolationMode::Bezier: - break; // do nothing. - case InterpolationMode::Smooth: - map[point] = path->smoothen_point(i); - break; - case InterpolationMode::Linear: - map[point] = point->geometry().nibbed(); - } - } + for (auto* point : path_object->path_vector().points()) { + if (point->is_selected()) { + map[point] = point->set_interpolation(mode); } } } @@ -107,13 +96,14 @@ void convert_object(Application& app, } context.subject.capture(std::move(converted_object)); app.scene->submit>(app.scene->object_tree(), std::move(context)); - if (auto* const po = type_cast(&ref); po != nullptr) { - app.scene->submit(*app.scene, po->geometry()); - } assert(ref.scene() == app.scene.get()); ref.set_transformation(object_to_convert.transformation()); converted_objects.insert(&ref); + if (auto* const po = type_cast(&ref); po != nullptr) { + assert(po->path_vector().path_object() == po); + } + if (keep_children) { const auto old_children = object_to_convert.tree_children(); std::transform(old_children.rbegin(), @@ -147,26 +137,26 @@ void remove_selected_points(Application& app) { std::unique_ptr macro; for (auto* path_object : app.scene->item_selection()) { - std::deque removed_points; - for (Path* path : path_object->geometry().paths()) { - const auto selected_ranges = find_coherent_ranges(path->points(), - std::mem_fn(&PathPoint::is_selected)); + for (Path* path : path_object->path_vector().paths()) { + auto selected_ranges = find_coherent_ranges(path->points(), std::mem_fn(&PathPoint::is_selected)); + std::sort(selected_ranges.rbegin(), selected_ranges.rend()); for (const auto& range : selected_ranges) { - removed_points.emplace_back(*path, range.start, range.size); - } - } - - if (!removed_points.empty()) { - auto command = std::make_unique(*path_object, std::move(removed_points)); - if (!macro) { - macro = app.scene->history().start_macro(command->actionText()); + auto command = std::make_unique(PathView(*path, range.start, range.size), path_object); + if (!macro) { + macro = app.scene->history().start_macro(command->actionText()); + } + app.scene->submit(std::move(command)); + app.scene->update_tool(); } - app.scene->submit(std::move(command)); - app.scene->update_tool(); } } } +void remove_selected_faces(Application& app) +{ + Q_UNUSED(app) +} + void remove_selected_items(Application& app) { switch (app.scene_mode()) { @@ -176,6 +166,9 @@ void remove_selected_items(Application& app) case SceneMode::Object: app.scene->remove(app.main_window(), app.scene->selection()); break; + case SceneMode::Face: + remove_selected_faces(app); + break; } } @@ -203,7 +196,7 @@ void select_all(Application& app) switch (app.scene_mode()) { case SceneMode::Vertex: for (auto* path_object : app.scene->item_selection()) { - for (auto* point : path_object->geometry().points()) { + for (auto* point : path_object->path_vector().points()) { point->set_selected(true); } } @@ -212,6 +205,14 @@ void select_all(Application& app) case SceneMode::Object: app.scene->set_selection(down_cast(app.scene->object_tree().items())); break; + case SceneMode::Face: + for (auto* path_object : app.scene->item_selection()) { + for (const auto& face : path_object->path_vector().faces()) { + path_object->set_face_selected(face, true); + } + } + Q_EMIT app.scene->mail_box().face_selection_changed(); + break; } Q_EMIT app.mail_box().scene_appearance_changed(); } @@ -221,7 +222,7 @@ void deselect_all(Application& app) switch (app.scene_mode()) { case SceneMode::Vertex: for (auto* path_object : app.scene->item_selection()) { - for (auto* point : path_object->geometry().points()) { + for (auto* point : path_object->path_vector().points()) { point->set_selected(false); } } @@ -230,6 +231,14 @@ void deselect_all(Application& app) case SceneMode::Object: app.scene->set_selection({}); break; + case SceneMode::Face: + for (auto* path_object : app.scene->item_selection()) { + for (const auto& face : path_object->path_vector().faces()) { + path_object->set_face_selected(face, false); + } + } + Q_EMIT app.scene->mail_box().face_selection_changed(); + break; } Q_EMIT app.mail_box().scene_appearance_changed(); } @@ -248,7 +257,7 @@ void invert_selection(Application& app) switch (app.scene_mode()) { case SceneMode::Vertex: for (auto* path_object : app.scene->item_selection()) { - for (auto* point : path_object->geometry().points()) { + for (auto* point : path_object->path_vector().points()) { point->set_selected(!point->is_selected()); } } @@ -258,47 +267,27 @@ void invert_selection(Application& app) app.scene->set_selection(down_cast(set_difference(app.scene->object_tree().items(), app.scene->item_selection()))); break; + case SceneMode::Face: + for (auto* path_object : app.scene->item_selection()) { + for (const auto& face : path_object->path_vector().faces()) { + path_object->set_face_selected(face, path_object->is_face_selected(face)); + } + } + Q_EMIT app.scene->mail_box().face_selection_changed(); + break; } Q_EMIT app.mail_box().scene_appearance_changed(); } void select_connected_points(Application& app) { - std::set selected_paths; - foreach_subpath(app, [&selected_paths](const auto* path) { - const auto points = path->points(); - if (std::any_of(points.begin(), points.end(), std::mem_fn(&PathPoint::is_selected))) { - selected_paths.insert(path); - } - }); - - for (bool selected_paths_changed = true; selected_paths_changed;) { - selected_paths_changed = false; - for (const auto* path : selected_paths) { - for (auto* point : path->points()) { - for (auto* joined_point : point->joined_points()) { - const auto& other_path = joined_point->path(); - if (const auto [_, was_inserted] = selected_paths.insert(&other_path); was_inserted) { - selected_paths_changed = true; - } - } - } - } - } - - for (const auto* path : selected_paths) { - for (auto* point : path->points()) { - point->set_selected(true); - } - } - - Q_EMIT app.mail_box().scene_appearance_changed(); + (void) app; } void fill_selection(Application& app) { - foreach_subpath(app, [](const auto* segment) { - const auto points = segment->points(); + foreach_subpath(app, [](const auto* path) { + const auto points = path->points(); auto first_it = std::find_if(points.begin(), points.end(), std::mem_fn(&PathPoint::is_selected)); auto last_it = std::find_if(points.rbegin(), points.rend(), std::mem_fn(&PathPoint::is_selected)); if (first_it != points.end() && last_it != points.rend()) { @@ -310,9 +299,9 @@ void fill_selection(Application& app) void extend_selection(Application& app) { - foreach_subpath(app, [](const auto* segment) { + foreach_subpath(app, [](const auto* path) { std::set selection; - const auto points = segment->points(); + const auto points = path->points(); for (std::size_t i = 1; i < points.size() - 1; ++i) { if (points[i]->is_selected()) { selection.insert(i - 1); @@ -345,12 +334,12 @@ void shrink_selection(Application& app) void join_points(Application& app) { - app.scene->submit(*app.scene, std::deque{app.scene->point_selection->points()}); + (void) app; } void disjoin_points(Application& app) { - app.scene->submit(*app.scene, std::deque{app.scene->point_selection->points()}); + (void) app; } const std::map> actions{ diff --git a/src/objects/boolean.cpp b/src/objects/boolean.cpp index db9c75778..63281c1f6 100644 --- a/src/objects/boolean.cpp +++ b/src/objects/boolean.cpp @@ -1,7 +1,7 @@ #include "objects/boolean.h" -#include "path/pathvector.h" #include "properties/optionproperty.h" #include "path/lib2geomadapter.h" +#include "path/pathvector.h" #include <2geom/2geom.h> #include <2geom/intersection-graph.h> #include <2geom/pathvector.h> @@ -94,28 +94,9 @@ void Boolean::polish() listen_to_children_changes(); } -PathVector Boolean::compute_path_vector() const +std::unique_ptr Boolean::compute_geometry() const { - const auto children = tree_children(); - if (is_active() && children.size() == 2) { - static constexpr auto get_path_vector = [](const Object& object) { - const auto t = object.transformation(); - return t.apply(omm_to_geom(object.path_vector())); - }; - Geom::PathIntersectionGraph pig{get_path_vector(*children[0]), get_path_vector(*children[1])}; - if (pig.valid()) { - const auto i = property(MODE_PROPERTY_KEY)->value(); - auto path_vector = *geom_to_omm(dispatcher.at(i).compute(pig)); - path_vector.join_points_by_position(util::transform(pig.intersectionPoints(), [](const auto& p) { - return Vec2f{p}; - })); - return path_vector; - } else { - return {}; - } - } else { - return {}; - } + return std::make_unique(); } } // namespace omm diff --git a/src/objects/boolean.h b/src/objects/boolean.h index 3ffe1981c..b5ac3bf74 100644 --- a/src/objects/boolean.h +++ b/src/objects/boolean.h @@ -5,6 +5,8 @@ namespace omm { + +class PathVector; class Scene; class Boolean : public Object @@ -26,7 +28,7 @@ class Boolean : public Object static constexpr auto MODE_PROPERTY_KEY = "mode"; void on_property_value_changed(Property* property) override; void polish(); - PathVector compute_path_vector() const override; + std::unique_ptr compute_geometry() const override; }; } // namespace omm diff --git a/src/objects/cloner.cpp b/src/objects/cloner.cpp index 94dc0a169..ccfffbf00 100644 --- a/src/objects/cloner.cpp +++ b/src/objects/cloner.cpp @@ -2,6 +2,7 @@ #include +#include "geometry/orientedposition.h" #include "objects/empty.h" #include "path/pathvector.h" #include "properties/boolproperty.h" @@ -192,7 +193,7 @@ void Cloner::update() Object::update(); } -PathVector Cloner::compute_path_vector() const +std::unique_ptr Cloner::compute_geometry() const { return join(util::transform(m_clones, [](const auto& up) { return up.get(); })); } @@ -417,8 +418,9 @@ void Cloner::set_radial(Object& object, std::size_t i) { const double angle = 2 * M_PI * get_t(i); const double r = property(RADIUS_PROPERTY_KEY)->value(); - const Point op({std::cos(angle) * r, std::sin(angle) * r}, angle + M_PI / 2.0); - object.set_oriented_position(op, property(PathProperties::ALIGN_PROPERTY_KEY)->value()); + const Vec2f pos{std::cos(angle) * r, std::sin(angle) * r}; + const auto rotation = angle + M_PI / 2.0; + object.set_oriented_position({pos, rotation}, property(PathProperties::ALIGN_PROPERTY_KEY)->value()); } void Cloner::set_path(Object& object, std::size_t i) @@ -465,7 +467,7 @@ void Cloner::set_fillrandom(Object& object, std::mt19937& rng) LINFO << "Return a random point on edge instead."; const auto t = dist(rng); - return o->pos(o->compute_path_vector_time(t)).position(); + return o->pos(o->compute_path_vector_time(t)).position; }(); if (property(ANCHOR_PROPERTY_KEY)->value() == Anchor::Path) { diff --git a/src/objects/cloner.h b/src/objects/cloner.h index dce1a60c1..492216d2e 100644 --- a/src/objects/cloner.h +++ b/src/objects/cloner.h @@ -56,7 +56,7 @@ class Cloner : public Object void update_property_visibility(Mode mode); private: - PathVector compute_path_vector() const override; + std::unique_ptr compute_geometry() const override; std::vector> make_clones(); std::vector> copy_children(std::size_t count); diff --git a/src/objects/ellipse.cpp b/src/objects/ellipse.cpp index 72685a2b5..4614aad62 100644 --- a/src/objects/ellipse.cpp +++ b/src/objects/ellipse.cpp @@ -1,15 +1,13 @@ #include "objects/ellipse.h" -#include "objects/pathobject.h" +#include "path/edge.h" #include "path/pathpoint.h" #include "path/path.h" #include "path/pathvector.h" #include "properties/boolproperty.h" -#include "properties/floatproperty.h" #include "properties/floatvectorproperty.h" #include "properties/integerproperty.h" #include "scene/scene.h" -#include "scene/disjointpathpointsetforest.h" #include namespace omm @@ -50,30 +48,38 @@ void Ellipse::on_property_value_changed(Property* property) } } -PathVector Ellipse::compute_path_vector() const +std::unique_ptr Ellipse::compute_geometry() const { const auto n_raw = property(CORNER_COUNT_PROPERTY_KEY)->value(); const auto n = static_cast(std::max(3, n_raw)); const auto r = property(RADIUS_PROPERTY_KEY)->value(); const bool smooth = property(SMOOTH_PROPERTY_KEY)->value(); - std::deque points; + std::vector> points; + points.reserve(n + 1); + auto path_vector = std::make_unique(); for (std::size_t i = 0; i <= n; ++i) { const double theta = static_cast(i) * 2.0 / static_cast(n) * M_PI; const double x = std::cos(theta) * r.x; const double y = std::sin(theta) * r.y; + + PolarCoordinates bwd; if (smooth) { - const Vec2f d(std::sin(theta) * r.x, -std::cos(theta) * r.y); - points.emplace_back(Vec2f{x, y}, d.arg(), 2.0 * d.euclidean_norm() / static_cast(n)); - } else { - points.emplace_back(Vec2f{x, y}); - } + Vec2f d(std::sin(theta) * r.x, -std::cos(theta) * r.y); + bwd.argument = d.arg(); + bwd.magnitude = 2.0 * d.euclidean_norm() / static_cast(n); + }; + points.emplace_back(std::make_shared(Point(Vec2f{x, y}, bwd, -bwd), path_vector.get())); + } + if (points.empty()) { + return {}; + } + points.push_back(points.front()); + + auto& path = path_vector->add_path(); + for (std::size_t i = 1; i < points.size(); ++i) { + path.add_edge(std::make_unique(points.at(i - 1), points.at(i), &path)); } - PathVector path_vector; - auto path = std::make_unique(std::move(points)); - const auto path_points = path->points(); - path_vector.add_path(std::move(path)); - path_vector.joined_points().insert({path_points.front(), path_points.back()}); return path_vector; } diff --git a/src/objects/ellipse.h b/src/objects/ellipse.h index a7218e070..4a5f5f99c 100644 --- a/src/objects/ellipse.h +++ b/src/objects/ellipse.h @@ -20,7 +20,7 @@ class Ellipse : public Object void on_property_value_changed(Property* property) override; private: - PathVector compute_path_vector() const override; + std::unique_ptr compute_geometry() const override; }; } // namespace omm diff --git a/src/objects/empty.cpp b/src/objects/empty.cpp index b50ab2696..da87ac894 100644 --- a/src/objects/empty.cpp +++ b/src/objects/empty.cpp @@ -1,6 +1,6 @@ #include "objects/empty.h" -#include "path/pathvector.h" #include "properties/boolproperty.h" +#include "path/pathvector.h" namespace omm { @@ -23,12 +23,12 @@ QString Empty::type() const return TYPE; } -PathVector Empty::compute_path_vector() const +std::unique_ptr Empty::compute_geometry() const { if (property(JOIN_PROPERTY_KEY)->value()) { return join(tree_children()); } else { - return PathVector{}; + return std::make_unique(); } } diff --git a/src/objects/empty.h b/src/objects/empty.h index 1a30eb8c4..4a2c9ba80 100644 --- a/src/objects/empty.h +++ b/src/objects/empty.h @@ -13,7 +13,7 @@ class Empty : public Object BoundingBox bounding_box(const ObjectTransformation& transformation) const override; QString type() const override; static constexpr auto TYPE = QT_TRANSLATE_NOOP("any-context", "Empty"); - PathVector compute_path_vector() const override; + std::unique_ptr compute_geometry() const override; Flag flags() const override; }; diff --git a/src/objects/instance.cpp b/src/objects/instance.cpp index 6811798ba..09d0e5d3a 100644 --- a/src/objects/instance.cpp +++ b/src/objects/instance.cpp @@ -2,10 +2,10 @@ #include "commands/propertycommand.h" #include "objects/empty.h" -#include "path/pathvector.h" #include "properties/boolproperty.h" #include "properties/referenceproperty.h" #include "renderers/painteroptions.h" +#include "path/pathvector.h" #include "scene/mailbox.h" #include "scene/scene.h" #include "tags/scripttag.h" @@ -158,12 +158,12 @@ void Instance::update() Object::update(); } -PathVector Instance::compute_path_vector() const +std::unique_ptr Instance::compute_geometry() const { if (m_reference) { - return PathVector{m_reference->path_vector(), nullptr}; + return std::make_unique(m_reference->geometry()); } else { - return {}; + return std::make_unique(); } } diff --git a/src/objects/instance.h b/src/objects/instance.h index cdecf965a..2c3b3d89b 100644 --- a/src/objects/instance.h +++ b/src/objects/instance.h @@ -33,7 +33,7 @@ class Instance : public Object Flag flags() const override; void post_create_hook() override; void update() override; - PathVector compute_path_vector() const override; + std::unique_ptr compute_geometry() const override; protected: void on_property_value_changed(Property* property) override; diff --git a/src/objects/lineobject.cpp b/src/objects/lineobject.cpp index 766f0cc8a..87584b248 100644 --- a/src/objects/lineobject.cpp +++ b/src/objects/lineobject.cpp @@ -2,8 +2,8 @@ #include "properties/boolproperty.h" #include "properties/floatproperty.h" #include "path/path.h" -#include "path/pathvector.h" #include +#include "path/pathvector.h" namespace omm { @@ -36,17 +36,9 @@ Flag LineObject::flags() const return Object::flags() | Flag::Convertible; } -PathVector LineObject::compute_path_vector() const +std::unique_ptr LineObject::compute_geometry() const { - const auto length = property(LENGTH_PROPERTY_KEY)->value(); - const auto angle = property(ANGLE_PROPERTY_KEY)->value(); - const auto centered = property(CENTER_PROPERTY_KEY)->value(); - const PolarCoordinates a(angle, centered ? -length / 2.0 : 0.0); - const PolarCoordinates b(angle, centered ? length / 2.0 : length); - std::vector points{Point(a.to_cartesian()), Point(b.to_cartesian())}; - PathVector pv; - pv.add_path(std::make_unique(std::move(points))); - return pv; + return std::make_unique(); } void LineObject::on_property_value_changed(Property* property) diff --git a/src/objects/lineobject.h b/src/objects/lineobject.h index defe653e3..c7a438b0a 100644 --- a/src/objects/lineobject.h +++ b/src/objects/lineobject.h @@ -16,7 +16,7 @@ class LineObject : public Object static constexpr auto ANGLE_PROPERTY_KEY = "angle"; static constexpr auto CENTER_PROPERTY_KEY = "center"; - PathVector compute_path_vector() const override; + std::unique_ptr compute_geometry() const override; protected: void on_property_value_changed(Property* property) override; diff --git a/src/objects/mirror.cpp b/src/objects/mirror.cpp index 5e763273a..a50d7d744 100644 --- a/src/objects/mirror.cpp +++ b/src/objects/mirror.cpp @@ -11,7 +11,6 @@ #include "properties/optionproperty.h" #include "renderers/painter.h" #include "renderers/painteroptions.h" -#include "scene/disjointpathpointsetforest.h" #include "scene/scene.h" #include #include "path/path.h" @@ -20,53 +19,46 @@ namespace { -using namespace omm; - -Path& make_reflection(PathVector& pv, const Path& original, const Mirror::Direction direction, const double eps) +omm::ObjectTransformation get_mirror_t(omm::Mirror::Direction direction) { - auto& path = pv.add_path(std::make_unique(original, &pv)); - const auto s = Vec2f{direction == Mirror::Direction::Horizontal ? -1.0 : 1.0, - direction == Mirror::Direction::Vertical ? -1.0 : 1.0}; - const auto transform = ObjectTransformation{}.scaled(s); - for (auto* p : path.points()) { - p->set_geometry(transform.apply(p->geometry())); - } - - const auto join_if_close = [&pv, eps2 = eps * eps](PathPoint& p1, PathPoint& p2) { - if ((p1.geometry().position() - p2.geometry().position()).euclidean_norm2() < eps2) { - pv.joined_points().insert({&p1, &p2}); - auto g1 = p1.geometry(); - auto g2 = p2.geometry(); - const auto p = (g1.position() + g2.position()) / 2.0; - g1.set_position(p); - p1.set_geometry(g1); - - g2.set_position(p); - p2.set_geometry(g2); - } - }; - - if (const auto n = path.size(); n > 1) { - join_if_close(path.at(0), original.at(0)); - join_if_close(path.at(n - 1), original.at(n - 1)); + switch (direction) { + case omm::Mirror::Direction::Horizontal: + return omm::ObjectTransformation().scaled(omm::Vec2f(-1.0, 1.0)); + case omm::Mirror::Direction::Vertical: + return omm::ObjectTransformation().scaled(omm::Vec2f(1.0, -1.0)); + case omm::Mirror::Direction::Both: + return omm::ObjectTransformation().scaled(omm::Vec2f(-1.0, -1.0)); + default: + Q_UNREACHABLE(); + return omm::ObjectTransformation(); } - return path; } - -ObjectTransformation get_mirror_t(Mirror::Direction direction) +std::unique_ptr reflect(const omm::PathVector& pv, const omm::Mirror::Direction direction) { - switch (direction) { - case Mirror::Direction::Horizontal: - return ObjectTransformation().scaled(Vec2f(-1.0, 1.0)); - case Mirror::Direction::Vertical: - return ObjectTransformation().scaled(Vec2f(1.0, -1.0)); - case Mirror::Direction::Both: - return ObjectTransformation().scaled(Vec2f(-1.0, -1.0)); - default: - Q_UNREACHABLE(); - return ObjectTransformation(); + const auto reflect = [direction](omm::Vec2f p) { + switch (direction) { + case omm::Mirror::Direction::Horizontal: + p.x = -p.x; + break; + case omm::Mirror::Direction::Vertical: + p.y = -p.y; + break; + case omm::Mirror::Direction::Both: + p = -p; + break; + } + return p; + }; + auto copy = std::make_unique(pv); + for (auto* const point : copy->points()) { + auto& geom = point->geometry(); + geom.set_position(reflect(geom.position())); + for (auto& [key, tangent] : geom.tangents()) { + tangent = omm::PolarCoordinates(reflect(tangent.to_cartesian())); + } } + return copy; } } // namespace @@ -160,16 +152,16 @@ std::unique_ptr Mirror::convert(bool& keep_children) const } } -PathVector Mirror::compute_path_vector() const +std::unique_ptr Mirror::compute_geometry() const { if (!is_active()) { - return {}; + return std::make_unique(); } switch (property(AS_PATH_PROPERTY_KEY)->value()) { case Mode::Path: - return type_cast(*m_reflection).geometry(); + return std::make_unique(type_cast(*m_reflection).geometry()); case Mode::Object: - return PathVector{m_reflection->path_vector(), nullptr}; + return std::make_unique(m_reflection->geometry()); default: Q_UNREACHABLE(); } @@ -207,26 +199,28 @@ void Mirror::update_path_mode() m_reflection.reset(); } else { const auto eps = property(TOLERANCE_PROPERTY_KEY)->value(); - Object& child = this->tree_child(0); - auto reflection = std::make_unique(scene()); - reflection->geometry().unshare_joined_points(std::make_unique()); - assert(!reflection->geometry().joined_points_shared()); - auto& pv = reflection->geometry(); - for (const auto* const path : child.path_vector().paths()) { - auto& original = pv.add_path(std::make_unique(*path, &pv)); - if (const auto direction = property(DIRECTION_PROPERTY_KEY)->value(); - direction == Direction::Both) - { - auto& reflection = make_reflection(pv, original, Direction::Horizontal, eps); - make_reflection(pv, original, Direction::Vertical, eps); - make_reflection(pv, reflection, Direction::Vertical, eps); - } else { - make_reflection(pv, original, direction, eps); - } + auto* child = type_cast(&this->tree_child(0)); + if (child == nullptr) { + m_reflection.reset(); + return; + } + const auto& original = child->path_vector(); + std::deque> reflections; + reflections.emplace_back(std::make_unique(original)); + const auto direction = property(DIRECTION_PROPERTY_KEY)->value(); + if (direction == Mirror::Direction::Both) { + reflections.emplace_back(reflect(original, Mirror::Direction::Horizontal)); + reflections.emplace_back(reflect(original, Mirror::Direction::Vertical)); + reflections.emplace_back(reflect(original, Mirror::Direction::Both)); + } else { + reflections.emplace_back(reflect(original, direction)); } - const auto interpolation = child.has_property(PathObject::INTERPOLATION_PROPERTY_KEY) - ? child.property(PathObject::INTERPOLATION_PROPERTY_KEY)->value() - : InterpolationMode::Bezier; + + const auto reflection_ptrs = util::transform(reflections, &std::unique_ptr::get); + auto reflection = std::make_unique(scene(), PathVector::join(reflection_ptrs, eps)); + const auto interpolation = child->has_property(PathObject::INTERPOLATION_PROPERTY_KEY) + ? child->property(PathObject::INTERPOLATION_PROPERTY_KEY)->value() + : InterpolationMode::Bezier; reflection->property(PathObject::INTERPOLATION_PROPERTY_KEY)->set(interpolation); m_reflection = std::move(reflection); diff --git a/src/objects/mirror.h b/src/objects/mirror.h index 5a4e0a383..14feb75d4 100644 --- a/src/objects/mirror.h +++ b/src/objects/mirror.h @@ -32,7 +32,7 @@ class Mirror : public Object static constexpr auto AS_PATH_PROPERTY_KEY = "as_path"; static constexpr auto TOLERANCE_PROPERTY_KEY = "eps"; - PathVector compute_path_vector() const override; + std::unique_ptr compute_geometry() const override; std::unique_ptr convert(bool& keep_children) const override; void update() override; diff --git a/src/objects/object.cpp b/src/objects/object.cpp index da0d7a685..77e353a87 100644 --- a/src/objects/object.cpp +++ b/src/objects/object.cpp @@ -1,18 +1,18 @@ #include "objects/object.h" #include "common.h" +#include "geometry/orientedposition.h" #include "logging.h" #include "objects/pathobject.h" #include "path/lib2geomadapter.h" #include "path/path.h" #include "path/pathvector.h" +#include "path/face.h" +#include "path/pathvectorview.h" #include "properties/boolproperty.h" #include "properties/floatproperty.h" #include "properties/floatvectorproperty.h" -#include "properties/integerproperty.h" -#include "properties/integervectorproperty.h" #include "properties/optionproperty.h" -#include "properties/propertygroups/markerproperties.h" #include "properties/referenceproperty.h" #include "properties/stringproperty.h" #include "removeif.h" @@ -20,14 +20,10 @@ #include "renderers/painteroptions.h" #include "renderers/style.h" #include "scene/contextes.h" -#include "scene/disjointpathpointsetforest.h" -#include "scene/disjointpathpointsetforest.h" #include "scene/mailbox.h" #include "scene/objecttree.h" #include "scene/scene.h" #include "serializers/abstractdeserializer.h" -#include "serializers/json/jsonserializer.h" -#include "serializers/json/jsonserializer.h" #include "tags/styletag.h" #include "tags/tag.h" @@ -103,20 +99,11 @@ std::pair factor_time_by_distance(const Geometry& geom, dou namespace omm { -class Object::CachedGeomPathVectorGetter : public CachedGetter -{ -public: - using CachedGetter::CachedGetter; -private: - PathVector compute() const override; -}; - const QPen Object::m_bounding_box_pen = make_bounding_box_pen(); -const QBrush Object::m_bounding_box_brush = Qt::NoBrush; - Object::Object(Scene* scene) : PropertyOwner(scene) - , m_cached_geom_path_vector_getter(std::make_unique(*this)) + , m_cached_geometry_getter(make_simple_cached_getter(*this, &Object::compute_geometry)) + , m_cached_faces_getter(make_simple_cached_getter(*this, &Object::compute_faces)) , tags(*this) { static constexpr double STEP = 0.1; @@ -163,7 +150,8 @@ Object::Object(Scene* scene) Object::Object(const Object& other) : PropertyOwner(other) , TreeElement(other) - , m_cached_geom_path_vector_getter(std::make_unique(*this)) + , m_cached_geometry_getter(make_simple_cached_getter(*this, &Object::compute_geometry)) + , m_cached_faces_getter(make_simple_cached_getter(*this, &Object::compute_faces)) , tags(other.tags, *this) , m_draw_children(other.m_draw_children) , m_object_tree(other.m_object_tree) @@ -265,13 +253,11 @@ QString Object::to_string() const return QString("%1[%2]").arg(type(), name()); } -PathVector Object::join(const std::vector& objects) +std::unique_ptr Object::join(const std::vector& objects) { - PathVector path_vector; - for (const auto* object : objects) { - for (const auto* path : object->path_vector().paths()) { - path_vector.add_path(std::make_unique(*path)); - } + auto path_vector = std::make_unique(); + for (const auto* const object : objects) { + path_vector->copy_from(object->geometry()); } return path_vector; } @@ -363,7 +349,7 @@ void Object::draw_recursive(Painter& renderer, PainterOptions options) const BoundingBox Object::bounding_box(const ObjectTransformation& transformation) const { if (is_active()) { - return BoundingBox{(path_vector().outline() * transformation.to_qtransform()).boundingRect()}; + return BoundingBox{(geometry().to_painter_path() * transformation.to_qtransform()).boundingRect()}; } else { return BoundingBox{}; } @@ -399,7 +385,7 @@ Object& Object::adopt(std::unique_ptr adoptee, const std::size_t pos) std::unique_ptr Object::convert(bool& keep_children) const { - auto converted = std::make_unique(scene(), this->path_vector()); + auto converted = std::make_unique(scene(), this->geometry()); copy_properties(*converted, CopiedProperties::Compatible | CopiedProperties::User); copy_tags(*converted); converted->property(PathObject::INTERPOLATION_PROPERTY_KEY)->set(InterpolationMode::Bezier); @@ -460,7 +446,8 @@ void Object::post_create_hook() void Object::update() { - m_cached_geom_path_vector_getter->invalidate(); + m_cached_faces_getter->invalidate(); + m_cached_geometry_getter->invalidate(); if (Scene* scene = this->scene(); scene != nullptr) { Q_EMIT scene->mail_box().object_appearance_changed(*this); } @@ -484,13 +471,13 @@ double Object::apply_border(double t, Border border) Q_UNREACHABLE(); } -void Object::set_oriented_position(const Point& op, const bool align) +void Object::set_oriented_position(const OrientedPosition& op, const bool align) { auto transformation = global_transformation(Space::Scene); if (align) { - transformation.set_rotation(op.rotation()); + transformation.set_rotation(op.rotation); } - transformation.set_translation(op.position()); + transformation.set_translation(op.position); set_global_transformation(transformation, Space::Scene); } @@ -542,15 +529,15 @@ std::deque Object::find_styles() const }); } -Point Object::pos(const Geom::PathVectorTime& t) const +OrientedPosition Object::pos(const Geom::PathVectorTime& t) const { - const auto paths = omm_to_geom(path_vector()); + const auto paths = omm_to_geom(geometry()); if (const auto n = paths.curveCount(); n == 0) { - return Point{}; + return OrientedPosition{}; } else if (t.path_index >= paths.size()) { - return Point{}; + return OrientedPosition{}; } else if (auto&& path = paths[t.path_index]; t.curve_index >= path.size()) { - return Point{}; + return OrientedPosition{}; } else { auto&& curve = path[t.curve_index]; @@ -560,28 +547,31 @@ Point Object::pos(const Geom::PathVectorTime& t) const const auto tangent = curve.unitTangentAt(s); auto position = curve.pointAt(s); const auto convert = [](const Geom::Point& p) { return Vec2{p.x(), p.y()}; }; - return Point(convert(position), - PolarCoordinates(convert(tangent)), - PolarCoordinates(-convert(tangent))); + return {convert(position), PolarCoordinates(convert(tangent)).argument}; } } bool Object::contains(const Vec2f& point) const { - const auto path_vector = omm_to_geom(this->path_vector()); + const auto path_vector = omm_to_geom(this->geometry()); const auto winding = path_vector.winding(Geom::Point{point.x, point.y}); return std::abs(winding) % 2 == 1; } -PathVector Object::compute_path_vector() const +std::unique_ptr Object::compute_geometry() const +{ + return std::make_unique(); +} + +std::set Object::compute_faces() const { - return {}; + return geometry().faces(); } Geom::PathVectorTime Object::compute_path_vector_time(double t, Interpolation interpolation) const { t = std::clamp(t, 0.0, almost_one); - const auto& path_vector = this->path_vector(); + const auto& path_vector = this->geometry(); if (path_vector.paths().empty()) { return {0, 0, 0.0}; } @@ -609,7 +599,7 @@ Object::compute_path_vector_time(int path_index, double t, Interpolation interpo } t = std::clamp(t, 0.0, almost_one); - const auto path_vector = omm_to_geom(this->path_vector()); + const auto path_vector = omm_to_geom(this->geometry()); if (static_cast(path_index) >= path_vector.size()) { return {static_cast(path_index), 0, 0.0}; } @@ -656,20 +646,21 @@ void Object::draw_object(Painter& renderer, { options.object_id = id(); if (QPainter* painter = renderer.painter; painter != nullptr && is_active()) { - const auto& path_vector = this->path_vector(); - const auto faces = path_vector.faces(); - const auto& outline = path_vector.outline(); - if (!faces.empty() || !outline.isEmpty()) { - - for (std::size_t f = 0; f < faces.size(); ++f) { - options.path_id = f; - renderer.set_style(style, *this, options); - painter->save(); - painter->setPen(Qt::NoPen); - painter->drawPath(faces.at(f)); - painter->restore(); - } + const auto& faces = this->faces(); + const auto& geometry = this->geometry(); + const auto& outline = geometry.to_painter_path(); + std::size_t face_id = 0; + for (const auto& face : faces) { + options.path_id = face_id; + renderer.set_style(style, *this, options); + painter->save(); + painter->setPen(Qt::NoPen); + painter->drawPath(face.path_vector_view().to_painter_path()); + painter->restore(); + face_id += 1; + } + if (!outline.isEmpty()) { painter->save(); options.path_id = 0; renderer.set_style(style, *this, options); @@ -677,17 +668,17 @@ void Object::draw_object(Painter& renderer, painter->drawPath(outline); painter->restore(); - const auto marker_color = style.property(Style::PEN_COLOR_KEY)->value(); - const auto width = style.property(Style::PEN_WIDTH_KEY)->value(); - - for (std::size_t path_index = 0; path_index < path_vector.paths().size(); ++path_index) { - const auto pos = [this, path_index](const double t) { - const auto tt = compute_path_vector_time(static_cast(path_index), t); - return this->pos(tt).rotated(M_PI_2); - }; - style.start_marker->draw_marker(renderer, pos(0.0), marker_color, width); - style.end_marker->draw_marker(renderer, pos(1.0), marker_color, width); - } +// const auto marker_color = style.property(Style::PEN_COLOR_KEY)->value(); +// const auto width = style.property(Style::PEN_WIDTH_KEY)->value(); + +// for (std::size_t path_index = 0; path_index < geometry.paths().size(); ++path_index) { +// const auto pos = [this, path_index](const double t) { +// const auto tt = compute_path_vector_time(static_cast(path_index), t); +// return this->pos(tt).rotated(M_PI_2); +// }; +// style.start_marker->draw_marker(renderer, pos(0.0), marker_color, width); +// style.end_marker->draw_marker(renderer, pos(1.0), marker_color, width); +// } } } } @@ -749,14 +740,14 @@ void Object::listen_to_children_changes() connect(&scene()->mail_box(), &MailBox::object_appearance_changed, this, on_change); } -PathVector Object::CachedGeomPathVectorGetter::compute() const +const PathVector& Object::geometry() const { - return m_self.compute_path_vector(); + return *m_cached_geometry_getter->operator()(); } -const PathVector& Object::path_vector() const +const std::set& Object::faces() const { - return m_cached_geom_path_vector_getter->operator()(); + return m_cached_faces_getter->operator()(); } } // namespace omm diff --git a/src/objects/object.h b/src/objects/object.h index 602f929df..b9d24d301 100644 --- a/src/objects/object.h +++ b/src/objects/object.h @@ -23,6 +23,7 @@ class Property; class Scene; struct PainterOptions; class PathVector; +struct OrientedPosition; class Object : public PropertyOwner @@ -75,7 +76,7 @@ class Object bool is_visible(bool viewport) const; virtual std::deque find_styles() const; - virtual Point pos(const Geom::PathVectorTime& t) const; + virtual OrientedPosition pos(const Geom::PathVectorTime& t) const; virtual bool contains(const Vec2f& point) const; private: @@ -85,7 +86,9 @@ class Object * @note use `Object::geom_paths` or `Object::painter_path` to access the paths. * @return the paths. */ - virtual PathVector compute_path_vector() const; +public: + virtual std::unique_ptr compute_geometry() const; + virtual std::set compute_faces() const; public: enum class Interpolation { Natural, Distance }; @@ -95,10 +98,12 @@ class Object compute_path_vector_time(int path_index, double t, Interpolation = Interpolation::Natural) const; private: - class CachedGeomPathVectorGetter; - std::unique_ptr m_cached_geom_path_vector_getter; + std::unique_ptr, Object>> m_cached_geometry_getter; + std::unique_ptr, Object>> m_cached_faces_getter; + public: - const PathVector& path_vector() const; + const PathVector& geometry() const; + const std::set& faces() const; TagList tags; @@ -141,7 +146,7 @@ class Object public: void set_object_tree(ObjectTree& object_tree); void set_position_on_path(const Object& path, bool align, const Geom::PathVectorTime& t); - void set_oriented_position(const Point& op, bool align); + void set_oriented_position(const OrientedPosition& op, bool align); QString to_string() const override; @@ -156,7 +161,7 @@ class Object static const QBrush m_bounding_box_brush; protected: - static PathVector join(const std::vector& objects); + static std::unique_ptr join(const std::vector& objects); }; } // namespace omm diff --git a/src/objects/pathobject.cpp b/src/objects/pathobject.cpp index af4cf17ca..9dfffc9e4 100644 --- a/src/objects/pathobject.cpp +++ b/src/objects/pathobject.cpp @@ -1,21 +1,15 @@ #include "objects/pathobject.h" -#include "commands/modifypointscommand.h" #include "common.h" -#include "path/path.h" #include "path/pathvector.h" -#include "properties/boolproperty.h" #include "properties/optionproperty.h" -#include "renderers/style.h" -#include "scene/disjointpathpointsetforest.h" -#include "scene/mailbox.h" #include "scene/scene.h" #include -#ifdef DRAW_POINT_IDS -#include "path/pathpoint.h" -#include "renderers/painter.h" -#include +#if DRAW_POINT_IDS +# include "path/pathpoint.h" +# include "renderers/painter.h" +# include #endif // DRAW_POINT_IDS @@ -24,10 +18,27 @@ namespace omm class Style; +PathObject::PathObject(Scene* scene, PathVector path_vector) + : PathObject(scene, std::make_unique(std::move(path_vector), this)) +{ + // TODO handle INTERPOLATION_PROPERTY_KEY +} + +PathObject::PathObject(Scene* scene) + : PathObject(scene, std::make_unique(this)) +{ +} + +PathObject::PathObject(const PathObject& other) + : Object(other), m_path_vector(std::make_unique(*other.m_path_vector, this)) +{ +} + PathObject::PathObject(Scene* scene, std::unique_ptr path_vector) : Object(scene) , m_path_vector(std::move(path_vector)) { + m_path_vector->set_path_object(this); static const auto category = QObject::tr("path"); create_property(INTERPOLATION_PROPERTY_KEY) @@ -35,36 +46,6 @@ PathObject::PathObject(Scene* scene, std::unique_ptr path_vector) .set_label(QObject::tr("interpolation")) .set_category(category); PathObject::update(); - - if (scene != nullptr) { - connect(&scene->mail_box(), &MailBox::transformation_changed, this, [this](const Object& o) { - if (&o == this) { - geometry().update_joined_points_geometry(); - } - }); - } -} - -PathObject::PathObject(Scene* scene, const PathVector& path_vector) - : PathObject(scene, std::make_unique(path_vector, this)) -{ -} - -PathObject::PathObject(Scene* scene) - : PathObject(scene, std::make_unique(this)) -{ - if (const auto* const scene = this->scene(); scene != nullptr) { - m_path_vector->share_joined_points(scene->joined_points()); - } -} - -PathObject::PathObject(const PathObject& other) - : Object(other) - , m_path_vector(copy_unique_ptr(other.m_path_vector, this)) -{ - if (const auto* const scene = this->scene(); scene != nullptr && other.path_vector().joined_points_shared()) { - m_path_vector->share_joined_points(scene->joined_points()); - } } PathObject::~PathObject() = default; @@ -100,37 +81,41 @@ Flag PathObject::flags() const return Flag::None; } -const PathVector& PathObject::geometry() const +const PathVector& PathObject::path_vector() const { return *m_path_vector; } -PathVector& PathObject::geometry() +PathVector& PathObject::path_vector() { return *m_path_vector; } -PathVector PathObject::compute_path_vector() const +std::unique_ptr PathObject::compute_geometry() const { - const auto interpolation = property(INTERPOLATION_PROPERTY_KEY)->value(); + return std::make_unique(*m_path_vector); +} - PathVector pv{*m_path_vector}; - for (auto* path : pv.paths()) { - path->set_interpolation(interpolation); - } - return pv; +void PathObject::set_face_selected(const Face& face, bool s) +{ + Q_UNUSED(face) + Q_UNUSED(s) } -#ifdef DRAW_POINT_IDS +bool PathObject::is_face_selected(const Face& face) const +{ + Q_UNUSED(face) + return false; +} + +#if DRAW_POINT_IDS void PathObject::draw_object(Painter& renderer, const Style& style, const PainterOptions& options) const { Object::draw_object(renderer, style, options); renderer.painter->save(); renderer.painter->setPen(Qt::white); - for (const auto* point : path_vector().points()) { - static constexpr QPointF offset{10.0, 10.0}; - renderer.painter->drawText(point->geometry().position().to_pointf() + offset, point->debug_id()); - } + path_vector().draw_point_ids(*renderer.painter); + path_vector().draw_path_ids(*renderer.painter); renderer.painter->restore(); } #endif // DRAW_POINT_IDS diff --git a/src/objects/pathobject.h b/src/objects/pathobject.h index 3e671b79c..446d7aaf5 100644 --- a/src/objects/pathobject.h +++ b/src/objects/pathobject.h @@ -1,6 +1,7 @@ #pragma once #include "objects/object.h" +#include "config.h" #include namespace omm @@ -15,7 +16,7 @@ class PathObject : public Object { public: explicit PathObject(Scene* scene); - explicit PathObject(Scene* scene, const PathVector& path_vector); + explicit PathObject(Scene* scene, PathVector path_vector); explicit PathObject(Scene* scene, std::unique_ptr path_vector); PathObject(const PathObject& other); PathObject(PathObject&&) = delete; @@ -34,12 +35,14 @@ class PathObject : public Object void on_property_value_changed(Property* property) override; Flag flags() const override; - const PathVector& geometry() const; - PathVector& geometry(); + const PathVector& path_vector() const; + PathVector& path_vector(); - PathVector compute_path_vector() const override; + std::unique_ptr compute_geometry() const override; + void set_face_selected(const Face& face, bool s); + [[nodiscard]] bool is_face_selected(const Face& face) const; -#ifdef DRAW_POINT_IDS +#if DRAW_POINT_IDS void draw_object(Painter& renderer, const Style& style, const PainterOptions& options) const override; #endif // DRAW_POINT_IDS diff --git a/src/objects/proceduralpath.cpp b/src/objects/proceduralpath.cpp index ba51b16b1..3d9b1d8dc 100644 --- a/src/objects/proceduralpath.cpp +++ b/src/objects/proceduralpath.cpp @@ -12,7 +12,6 @@ #include "python/pythonengine.h" #include "python/scenewrapper.h" #include "scene/scene.h" -#include "scene/disjointpathpointsetforest.h" #include namespace @@ -92,24 +91,25 @@ void ProceduralPath::update() Object::update(); } -PathVector ProceduralPath::compute_path_vector() const +std::unique_ptr ProceduralPath::compute_geometry() const { - PathVector pv; - for (const auto& points : m_points) { - pv.add_path(std::make_unique(std::deque(points))); - } - - try { - for (const auto& index_set : m_joined_points) { - pv.joined_points().insert(util::transform<::transparent_set>(index_set, [&pv](const int i) { - return &pv.point_at_index(i); - })); - } - } catch (const std::runtime_error& e) { - LERROR << e.what(); - } - - return pv; + return std::make_unique(); +// PathVector pv; +// for (const auto& points : m_points) { +// pv.add_path(std::make_unique(std::deque(points))); +// } + +// try { +// for (const auto& index_set : m_joined_points) { +// pv.joined_points().insert(util::transform<::transparent_set>(index_set, [&pv](const int i) { +// return &pv.point_at_index(i); +// })); +// } +// } catch (const std::runtime_error& e) { +// LERROR << e.what(); +// } + +// return pv; } void ProceduralPath::on_property_value_changed(Property* property) diff --git a/src/objects/proceduralpath.h b/src/objects/proceduralpath.h index bc32b6ea1..f1652447d 100644 --- a/src/objects/proceduralpath.h +++ b/src/objects/proceduralpath.h @@ -19,7 +19,7 @@ class ProceduralPath : public Object static constexpr auto COUNT_PROPERTY_KEY = "count"; void update() override; - PathVector compute_path_vector() const override; + std::unique_ptr compute_geometry() const override; protected: void on_property_value_changed(Property* property) override; diff --git a/src/objects/rectangleobject.cpp b/src/objects/rectangleobject.cpp index 1bbfa974d..35cc2c2a9 100644 --- a/src/objects/rectangleobject.cpp +++ b/src/objects/rectangleobject.cpp @@ -1,8 +1,8 @@ #include "objects/rectangleobject.h" +#include "path/edge.h" #include "path/path.h" #include "path/pathpoint.h" #include "path/pathvector.h" -#include "scene/disjointpathpointsetforest.h" #include "properties/floatvectorproperty.h" namespace omm @@ -35,9 +35,9 @@ QString RectangleObject::type() const return TYPE; } -PathVector RectangleObject::compute_path_vector() const +std::unique_ptr RectangleObject::compute_geometry() const { - std::deque points; + std::deque> points; const auto size = property(SIZE_PROPERTY_KEY)->value() / 2.0; const auto r = property(RADIUS_PROPERTY_KEY)->value(); const auto t = property(TENSION_PROPERTY_KEY)->value(); @@ -46,11 +46,13 @@ PathVector RectangleObject::compute_path_vector() const const PolarCoordinates null(0.0, 0.0); const PolarCoordinates v(Vec2f(0.0, -ar.y * t.y)); const PolarCoordinates h(Vec2f(ar.x * t.x, 0.0)); - - auto add = [&points](auto... args) { - points.emplace_back(args...); + auto path_vector = std::make_unique(); + auto add = [&points, &path_vector](const auto& pos, const auto& fwd, const auto& bwd) { + points.emplace_back(std::make_shared(Point(pos, fwd, bwd), path_vector.get())); }; - const bool p = ar != Vec2f::o(); + + static constexpr auto eps = 0.00001; + const bool p = ar.euclidean_norm2() > eps * eps; if (p) { add(Vec2f(-size.x + ar.x, -size.y), null, -h); } @@ -69,12 +71,12 @@ PathVector RectangleObject::compute_path_vector() const add(Vec2f(size.x - ar.x, -size.y), h, null); points.emplace_back(points.front()); - auto path = std::make_unique(std::move(points)); - const auto path_points = path->points(); - PathVector pv; - pv.add_path(std::move(path)); - pv.joined_points().insert({path_points.front(), path_points.back()}); - return pv; + + auto& path = path_vector->add_path(); + for (std::size_t i = 1; i < points.size(); ++i) { + path.add_edge(std::make_unique(points.at(i - 1), points.at(i), &path)); + } + return path_vector; } void RectangleObject::on_property_value_changed(Property* property) diff --git a/src/objects/rectangleobject.h b/src/objects/rectangleobject.h index 55994cc2e..484aafb25 100644 --- a/src/objects/rectangleobject.h +++ b/src/objects/rectangleobject.h @@ -14,7 +14,7 @@ class RectangleObject : public Object protected: void on_property_value_changed(Property* property) override; - PathVector compute_path_vector() const override; + std::unique_ptr compute_geometry() const override; static constexpr auto SIZE_PROPERTY_KEY = "size"; static constexpr auto RADIUS_PROPERTY_KEY = "r"; diff --git a/src/objects/text.cpp b/src/objects/text.cpp index 94d394bb9..a1fa63ff2 100644 --- a/src/objects/text.cpp +++ b/src/objects/text.cpp @@ -117,9 +117,9 @@ QRectF Text::rect(Qt::Alignment alignment) const return {QPointF(left, top), QSizeF(width, height)}; } -PathVector Text::compute_path_vector() const +std::unique_ptr Text::compute_geometry() const { - return {}; + return std::make_unique(); } void Text::on_property_value_changed(Property* property) diff --git a/src/objects/text.h b/src/objects/text.h index 76ef36689..01cdc587a 100644 --- a/src/objects/text.h +++ b/src/objects/text.h @@ -33,7 +33,7 @@ class Text : public Object protected: void on_property_value_changed(Property* property) override; - PathVector compute_path_vector() const override; + std::unique_ptr compute_geometry() const override; private: FontProperties m_font_properties; diff --git a/src/objects/tip.cpp b/src/objects/tip.cpp index 4a511dc36..7106b0d19 100644 --- a/src/objects/tip.cpp +++ b/src/objects/tip.cpp @@ -1,10 +1,9 @@ #include "objects/tip.h" #include "path/path.h" #include "path/pathpoint.h" -#include "path/pathvector.h" #include "properties/floatproperty.h" #include "properties/optionproperty.h" -#include "scene/disjointpathpointsetforest.h" +#include "path/pathvector.h" namespace { @@ -40,15 +39,16 @@ void Tip::on_property_value_changed(Property* property) } } -PathVector Tip::compute_path_vector() const +std::unique_ptr Tip::compute_geometry() const { - auto points = m_marker_properties.shape(1.0); - PathVector pv; - auto path = std::make_unique(std::move(points)); - const auto path_points = path->points(); - pv.add_path(std::move(path)); - pv.joined_points().insert({path_points.front(), path_points.back()}); - return pv; + return std::make_unique(); +// auto points = m_marker_properties.shape(1.0); +// PathVector pv; +// auto path = std::make_unique(std::move(points)); +// const auto path_points = path->points(); +// pv.add_path(std::move(path)); +// pv.joined_points().insert({path_points.front(), path_points.back()}); +// return pv; } } // namespace omm diff --git a/src/objects/tip.h b/src/objects/tip.h index 87c09b9f1..13961d933 100644 --- a/src/objects/tip.h +++ b/src/objects/tip.h @@ -23,7 +23,7 @@ class Tip : public Object static constexpr auto TYPE = QT_TRANSLATE_NOOP("any-context", "Tip"); protected: - PathVector compute_path_vector() const override; + std::unique_ptr compute_geometry() const override; private: MarkerProperties m_marker_properties; diff --git a/src/path/CMakeLists.txt b/src/path/CMakeLists.txt index 2be17df3d..294e9ced3 100644 --- a/src/path/CMakeLists.txt +++ b/src/path/CMakeLists.txt @@ -1,8 +1,14 @@ target_sources(libommpfritt PRIVATE + ccwcomparator.cpp + ccwcomparator.h edge.cpp edge.h + dedge.cpp + dedge.h face.cpp face.h + facedetector.cpp + facedetector.h graph.cpp graph.h lib2geomadapter.cpp @@ -13,6 +19,10 @@ target_sources(libommpfritt PRIVATE path.h pathvector.cpp pathvector.h + pathvectorisomorphism.cpp + pathvectorisomorphism.h + pathvectorview.cpp + pathvectorview.h pathview.cpp pathview.h ) diff --git a/src/path/ccwcomparator.cpp b/src/path/ccwcomparator.cpp new file mode 100644 index 000000000..d3ee88742 --- /dev/null +++ b/src/path/ccwcomparator.cpp @@ -0,0 +1,34 @@ +#include "path/ccwcomparator.h" +#include "common.h" +#include "path/pathpoint.h" + +namespace +{ + +double compare(const omm::DEdge& a, const omm::DEdge& b, const double base_arg) noexcept +{ + static constexpr auto eps = 0.001; + assert(&a.start_point() == &b.start_point()); + const auto arg = [origin = a.start_point().geometry().position(), base_arg](const omm::DEdge& edge) { + const auto eps_pos = omm::Vec2f(edge.to_geom_curve()->pointAt(eps)) - origin; + return omm::python_like_mod(eps_pos.arg() - base_arg, 2.0 * M_PI); + }; + + return arg(a) < arg(b); +} + +} // namespace + +namespace omm +{ + +CCWComparator::CCWComparator(const DEdge& base) : m_base(base), m_base_arg(base.end_angle()) +{ +} + +bool CCWComparator::operator()(const DEdge& a, const DEdge& b) const noexcept +{ + return compare(a, b, m_base_arg); +} + +} // namespace omm diff --git a/src/path/ccwcomparator.h b/src/path/ccwcomparator.h new file mode 100644 index 000000000..a011692b9 --- /dev/null +++ b/src/path/ccwcomparator.h @@ -0,0 +1,26 @@ +#pragma once + +#include "path/dedge.h" + +namespace omm +{ + +/** + * @brief The CCWComparator class induces an order over edges starting at the end point of a common base. + * Starting on the base edge, rotating around its end point counter clockwise, the `operator()` + * returns whether its first argument is met first. + * You can use it to decide along which edge (a or b) you want to travel if you want to go the + * left- or right-most way, when coming via base. + */ +class CCWComparator +{ +public: + explicit CCWComparator(const omm::DEdge& base); + [[nodiscard]] bool operator()(const omm::DEdge& a, const omm::DEdge& b) const noexcept; + +private: + omm::DEdge m_base; + double m_base_arg; +}; + +} // namespace omm diff --git a/src/path/dedge.cpp b/src/path/dedge.cpp new file mode 100644 index 000000000..473b90f90 --- /dev/null +++ b/src/path/dedge.cpp @@ -0,0 +1,86 @@ +#include "path/dedge.h" +#include "geometry/point.h" +#include "path/edge.h" +#include "path/pathpoint.h" + +#include <2geom/bezier-curve.h> + +namespace omm +{ + +template DEdgeBase::DEdgeBase(EdgePtr const edge, const Direction direction) + : edge(edge), direction(direction) +{ +} + +template bool DEdgeBase::is_valid() const noexcept +{ + return edge != nullptr && edge->is_valid(); +} + +template DEdgeBase DEdgeBase::fwd(EdgePtr edge) +{ + return DEdgeBase{edge, Direction::Forward}; +} + +template DEdgeBase DEdgeBase::bwd(EdgePtr edge) +{ + return DEdgeBase{edge, Direction::Backward}; +} + +template PathPoint& DEdgeBase::end_point() const +{ + return *edge->end_point(direction); +} + +template PathPoint& DEdgeBase::start_point() const +{ + return *edge->start_point(direction); +} + +template double DEdgeBase::start_angle() const +{ + return angle(direction, start_point(), end_point()); +} + +template double DEdgeBase::end_angle() const +{ + return angle(other(direction), end_point(), start_point()); +} + +template std::unique_ptr DEdgeBase::to_geom_curve() const +{ + const std::vector pts{ + start_point().geometry().position(), start_point().geometry().tangent_position({edge->path(), direction}), + end_point().geometry().tangent_position({edge->path(), other(direction)}), end_point().geometry().position()}; + return std::make_unique>(util::transform(std::move(pts), &Vec2f::to_geom_point)); +} + +template double DEdgeBase::angle(const Direction key_direction, const PathPoint& hinge, + const PathPoint& other_point) const +{ + const auto key = Point::TangentKey{edge->path(), key_direction}; + const auto tangent = hinge.geometry().tangent(key); + static constexpr double eps = 0.001; + if (tangent.magnitude > eps) { + return tangent.argument; + } else { + const auto other_key = Point::TangentKey{edge->path(), other(key_direction)}; + const auto t_pos = other_point.geometry().tangent_position(other_key); + const auto o_pos = hinge.geometry().position(); + return PolarCoordinates(t_pos - o_pos).argument; + } +} + +template QString DEdgeBase::to_string() const +{ + if (edge == nullptr) { + return "null"; + } + return (direction == Direction::Forward ? "" : "r") + edge->to_string(); +} + +template struct DEdgeBase; +template struct DEdgeBase; + +} // namespace omm diff --git a/src/path/dedge.h b/src/path/dedge.h new file mode 100644 index 000000000..86497714f --- /dev/null +++ b/src/path/dedge.h @@ -0,0 +1,53 @@ +#pragma once + +#include "geometry/direction.h" +#include <2geom/curve.h> +#include +#include + +namespace omm +{ + +class Edge; +class PathPoint; + +template struct DEdgeBase +{ + explicit DEdgeBase(EdgePtr edge, Direction direction); + static DEdgeBase fwd(EdgePtr edge); + static DEdgeBase bwd(EdgePtr edge); + DEdgeBase() = default; + EdgePtr edge = nullptr; + [[nodiscard]] bool is_valid() const noexcept; + Direction direction = Direction::Forward; + template [[nodiscard]] bool operator<(const DEdgeBase& other) const + { + static constexpr auto to_tuple = [](const auto& d) { return std::tuple{d.edge, d.direction}; }; + return to_tuple(*this) < to_tuple(other); + } + + template [[nodiscard]] bool operator>(const DEdgeBase& other) const + { + return other < *this; + } + + template [[nodiscard]] bool operator==(const DEdgeBase& other) const + { + return edge == other.edge && direction == other.direction; + } + + [[nodiscard]] PathPoint& end_point() const; + [[nodiscard]] PathPoint& start_point() const; + [[nodiscard]] double start_angle() const; + [[nodiscard]] double end_angle() const; + [[nodiscard]] std::unique_ptr to_geom_curve() const; + QString to_string() const; + +private: + [[nodiscard]] double angle(const Direction key_direction, const PathPoint& hinge, const PathPoint& other) const; +}; + +using DEdge = DEdgeBase; +using DEdgeConst = DEdgeBase; + +} // namespace omm diff --git a/src/path/edge.cpp b/src/path/edge.cpp index 9ecc39d54..4fd077dc6 100644 --- a/src/path/edge.cpp +++ b/src/path/edge.cpp @@ -5,34 +5,85 @@ namespace omm { -QString Edge::label() const +Edge::Edge(std::shared_ptr a, std::shared_ptr b, const Path* const path) + : m_path(path), m_a(a), m_b(b) { - const auto* separator = flipped ? "++" : "--"; - return QString{"%1%2%3"}.arg(start_point()->index()).arg(separator).arg(end_point()->index()); } -Point Edge::start_geometry() const +QString Edge::to_string() const { - const auto g = start_point()->geometry(); -// return flipped ? g.flipped() : g; - return flipped ? g : g.flipped(); + static constexpr bool print_pointer = true; + if constexpr (print_pointer) { + return QString{"%1--%2 (%3)"}.arg(m_a->debug_id(), m_b->debug_id(), + QString::asprintf("%p", static_cast(this))); + } else { + return QString{"%1--%2"}.arg(m_a->debug_id(), m_b->debug_id()); + } } -Point Edge::end_geometry() const +void Edge::flip() noexcept { - const auto g = end_point()->geometry(); -// return flipped ? g.flipped() : g; - return flipped ? g : g.flipped(); + std::swap(m_a, m_b); } -PathPoint* Edge::start_point() const +bool Edge::has_point(const PathPoint* p) noexcept { - return flipped ? b : a; + return p == m_a.get() || p == m_b.get(); } -PathPoint* Edge::end_point() const +const std::shared_ptr& Edge::a() const noexcept { - return flipped ? a : b; + return m_a; +} + +const std::shared_ptr& Edge::b() const noexcept +{ + return m_b; +} + +std::shared_ptr& Edge::a() noexcept +{ + return m_a; +} + +std::shared_ptr& Edge::b() noexcept +{ + return m_b; +} + +const Path* Edge::path() const +{ + return m_path; +} + +bool Edge::is_valid() const noexcept +{ + return m_a && m_b && m_a->path_vector() == m_b->path_vector(); +} + +bool Edge::contains(const PathPoint* p) const noexcept +{ + return m_a.get() == p || m_b.get() == p; +} + +std::shared_ptr Edge::start_point(const Direction& direction) const noexcept +{ + return direction == Direction::Backward ? b() : a(); +} + +std::shared_ptr Edge::end_point(const Direction& direction) const noexcept +{ + return direction == Direction::Forward ? b() : a(); +} + +bool Edge::is_loop() const noexcept +{ + return m_a.get() == m_b.get(); +} + +std::array Edge::points() const +{ + return {m_a.get(), m_b.get()}; } } // namespace omm diff --git a/src/path/edge.h b/src/path/edge.h index 00a5d4d13..a01090704 100644 --- a/src/path/edge.h +++ b/src/path/edge.h @@ -1,11 +1,13 @@ #pragma once +#include "geometry/direction.h" #include -#include +#include namespace omm { +class Path; class PathPoint; class Point; @@ -13,29 +15,31 @@ class Edge { public: Edge() = default; - [[nodiscard]] QString label() const; - - [[nodiscard]] Point start_geometry() const; - [[nodiscard]] Point end_geometry() const; - [[nodiscard]] PathPoint* start_point() const; - [[nodiscard]] PathPoint* end_point() const; - - bool flipped = false; - PathPoint* a = nullptr; - PathPoint* b = nullptr; - - // Edge equality is not unabiguously implementable. - // It's clear that numerical coincidence should not matter (1). - // Also, direction should not matter, because we're dealing with undirected graphs (2). - // It'd be also a good idea to distinguish joined points (two edges between A and B are not equal) - // because tangents can make these edges appear very different (3). - // Usually, multiple edges only occur between joined points and can be distinguished well. - // However, consider the loop (A) --e1-- (B) --e2-- (A): - // e1 and e2 are not distinguishable when ignoring direction, no joined points are involved to - // distinguish. - // That is, requirement (2) and (3) conflict. - // In practice that is no problem because the equality operator is not required. - friend bool operator==(const Edge&, const Edge&) = delete; + explicit Edge(std::shared_ptr a, std::shared_ptr b, const Path* path); + Edge(const Edge&) = delete; + Edge(Edge&&) = default; + Edge& operator=(const Edge&) = delete; + Edge& operator=(Edge&&) = default; + ~Edge() = default; + [[nodiscard]] QString to_string() const; + void flip() noexcept; + [[nodiscard]] bool has_point(const PathPoint* p) noexcept; + [[nodiscard]] const std::shared_ptr& a() const noexcept; + [[nodiscard]] const std::shared_ptr& b() const noexcept; + [[nodiscard]] std::shared_ptr& a() noexcept; + [[nodiscard]] std::shared_ptr& b() noexcept; + [[nodiscard]] const Path* path() const; + [[nodiscard]] bool is_valid() const noexcept; + [[nodiscard]] bool contains(const PathPoint* p) const noexcept; + [[nodiscard]] std::shared_ptr start_point(const Direction& direction) const noexcept; + [[nodiscard]] std::shared_ptr end_point(const Direction& direction) const noexcept; + [[nodiscard]] bool is_loop() const noexcept; + [[nodiscard]] std::array points() const; + +private: + const Path* m_path; + std::shared_ptr m_a = nullptr; + std::shared_ptr m_b = nullptr; }; } // namespace omm diff --git a/src/path/face.cpp b/src/path/face.cpp index 32969beb3..7e9c6bd8e 100644 --- a/src/path/face.cpp +++ b/src/path/face.cpp @@ -1,165 +1,157 @@ #include "path/face.h" -#include "common.h" -#include "geometry/point.h" +#include "geometry/line.h" +#include "path/dedge.h" #include "path/pathpoint.h" -#include "path/edge.h" +#include "path/pathvectorview.h" +#include #include -namespace +namespace omm { -using namespace omm; - -bool same_point(const PathPoint* p1, const PathPoint* p2) +Face::Face() + : m_path_vector_view(std::make_unique()) { - return p1 == p2 || (p1 != nullptr && p1->joined_points().contains(p2)); } -bool align_last_edge(const Edge& second_last, Edge& last) +Face::Face(PathVectorView pvv) + : m_path_vector_view(std::make_unique(std::move(pvv))) { - assert(!last.flipped); - if (same_point(second_last.end_point(), last.b)) { - last.flipped = true; - return true; - } else { - return same_point(second_last.end_point(), last.a); - } } -bool align_two_edges(Edge& second_last, Edge& last) -{ - assert(!last.flipped); - assert(!second_last.flipped); - if (same_point(second_last.b, last.b)) { - last.flipped = true; - return true; - } else if (same_point(second_last.a, last.a)) { - second_last.flipped = true; - return true; - } else if (same_point(second_last.a, last.b)) { - second_last.flipped = true; - last.flipped = true; - return true; - } else { - return same_point(second_last.b, last.a); - } -} - -template -bool equal_at_offset(const Ts& ts, const Rs& rs, const std::size_t offset) +Face::Face(const Face& other) + : m_path_vector_view(std::make_unique(other.path_vector_view())) { - if (ts.size() != rs.size()) { - return false; - } - - for (std::size_t i = 0; i < ts.size(); ++i) { - const auto j = (i + offset) % ts.size(); - if (!same_point(ts.at(i), rs.at(j))) { - return false; - } - } - return true; } -} // namespace - -namespace omm +Face::Face(Face&& other) noexcept + : Face() { + swap(*this, other); +} -std::list Face::points() const +Face& Face::operator=(Face other) { - std::list points; - for (const auto& edge : edges()) { - if (points.empty()) { - points.emplace_back(edge.start_geometry()); - } else { - points.back().set_right_position(edge.start_geometry().right_position()); - } - points.emplace_back(edge.end_geometry()); - } - return points; + swap(*this, other); + return *this; } -std::deque Face::path_points() const +Face& Face::operator=(Face&& other) noexcept { - std::deque points; - for (const auto& edge : edges()) { - points.emplace_back(edge.start_point()); - } - return points; + swap(*this, other); + return *this; } Face::~Face() = default; -bool Face::add_edge(const Edge& edge) -{ - assert(!edge.flipped); - m_edges.emplace_back(edge); - if (m_edges.size() == 2) { - return align_two_edges(m_edges[0], m_edges[1]); - } else if (m_edges.size() > 2) { - return align_last_edge(m_edges[m_edges.size() - 2], m_edges.back()); - } - return true; -} - -const std::deque& Face::edges() const +void swap(Face& a, Face& b) noexcept { - return m_edges; + swap(a.m_path_vector_view, b.m_path_vector_view); } double Face::compute_aabb_area() const { + if (path_vector_view().edges().size()) { + return 0.0; + } + double left = std::numeric_limits::infinity(); double right = -std::numeric_limits::infinity(); double top = -std::numeric_limits::infinity(); double bottom = std::numeric_limits::infinity(); - const auto points = this->points(); - for (const auto& p : points) { + for (const auto* pp : path_vector_view().path_points()) { + const auto& p = pp->geometry(); left = std::min(left, p.position().x); right = std::max(right, p.position().x); top = std::max(top, p.position().y); bottom = std::min(bottom, p.position().y); } - if (points.empty()) { - return 0.0; - } else { - return (right - left) * (top - bottom); - } + return (right - left) * (top - bottom); } QString Face::to_string() const { - const auto edges = util::transform(m_edges, std::mem_fn(&Edge::label)); - return static_cast(edges).join(", "); + return m_path_vector_view->to_string(); } -bool operator==(const Face& a, const Face& b) +bool Face::is_valid() const noexcept { - const auto points_a = a.path_points(); - const auto points_b = b.path_points(); - if (points_a.size() != points_b.size()) { - return false; - } - const auto points_b_reversed = std::deque(points_b.rbegin(), points_b.rend()); - QStringList pa; - QStringList pb; - for (std::size_t i = 0; i < points_a.size(); ++i) { - pa.append(QString{"%1"}.arg(points_a.at(i)->index())); - pb.append(QString{"%1"}.arg(points_b.at(i)->index())); + return m_path_vector_view->is_simply_closed(); +} + +PathVectorView& Face::path_vector_view() +{ + return *m_path_vector_view; +} + +const PathVectorView& Face::path_vector_view() const +{ + return *m_path_vector_view; +} + +double Face::area() const +{ + // See Green's Theorem and Leibniz' Sektorformel + const auto g_path = m_path_vector_view->to_geom(); + double sum = 0.0; + for (std::size_t i = 0; i < g_path.size(); ++i) { + const auto& curve = static_cast&>(g_path.at(i)); + const auto& derivative = static_cast&>(*curve.derivative()); + const auto x = curve.fragment()[0]; + const auto y = curve.fragment()[1]; + const auto dx = derivative.fragment()[0]; + const auto dy = derivative.fragment()[1]; + const auto f = y * dx - x * dy; + const auto integral = Geom::integral(f); + sum += integral.valueAt(1.0) - integral.valueAt(0.0); } + return sum / 2.0; +} + +bool Face::operator==(const Face& other) const +{ + return *m_path_vector_view == other.path_vector_view(); +} + +bool Face::operator!=(const Face& other) const +{ + return !(*this == other); +} + +bool Face::operator<(const Face& other) const +{ + return *m_path_vector_view < other.path_vector_view(); +} - for (std::size_t offset = 0; offset < points_a.size(); ++offset) { - if (equal_at_offset(points_a, points_b, offset)) { - return true; +PolygonLocation polygon_contains(const std::vector& polygon, const Vec2f& p) +{ + // Ray casting + bool inside = false; + const Line ray{p, p + Vec2(1.0, 0.0)}; // Ray with arbitary direction from p + for (std::size_t i = 0; i < polygon.size(); ++i) { + const Line line{i == 0 ? polygon.back() : polygon.at(i - 1), polygon.at(i)}; + static constexpr auto eps = 0.0001; + static constexpr auto in01 = [](const double d) { return d >= 0.0 && d < 1.0; }; + if (std::abs(line.distance(p)) < eps && in01(line.project(p))) { + return PolygonLocation::Edge; } - if (equal_at_offset(points_a, points_b_reversed, offset)) { - return true; + const auto t = line.intersect(ray); + const auto u = ray.intersect(line); + if (!std::isfinite(t) || !std::isfinite(u)) { + // line is parallel to ray and p is not on line: no intersection. + } else if (u >= 0.0 && in01(t)) { + // intersection + inside = !inside; } } - return false; + return inside ? PolygonLocation::Inside : PolygonLocation::Outside; +} + +std::ostream& operator<<(std::ostream& os, const Face& face) +{ + return os << face.to_string().toStdString(); } } // namespace omm diff --git a/src/path/face.h b/src/path/face.h index d276ae9b3..00b146c7f 100644 --- a/src/path/face.h +++ b/src/path/face.h @@ -1,53 +1,61 @@ #pragma once +#include "geometry/vec2.h" #include -#include #include +#include +#include +#include + +class QPainterPath; namespace omm { +namespace serialization +{ +class SerializerWorker; +class DeserializerWorker; +} // namespace serialization + class Point; class PathPoint; class Edge; +class PathVectorView; class Face { public: - Face() = default; + Face(); + Face(PathVectorView view); + Face(const Face& other); + Face(Face&& other) noexcept; + Face& operator=(Face other); + Face& operator=(Face&& other) noexcept; ~Face(); - Face(const Face&) = default; - Face(Face&&) = default; - Face& operator=(const Face&) = default; - Face& operator=(Face&&) = default; - - bool add_edge(const Edge& edge); - - /** - * @brief points returns the geometry of each point around the face with proper tangents. - * @note a face with `n` edges yields `n+1` points, because start and end point are listed - * separately. - * that's quite convenient for drawing paths. - * @see path_points - */ - [[nodiscard]] std::list points() const; - - /** - * @brief path_points returns the points around the face. - * @note a face with `n` edges yields `n` points, because start and end point are not listed - * separately. - * That's quite convenient for checking face equality. - * @see points - */ - [[nodiscard]] std::deque path_points() const; - [[nodiscard]] const std::deque& edges() const; + friend void swap(Face& a, Face& b) noexcept; + [[nodiscard]] double compute_aabb_area() const; [[nodiscard]] QString to_string() const; + friend std::ostream& operator<<(std::ostream& os, const Face& face); + [[nodiscard]] bool is_valid() const noexcept; + [[nodiscard]] PathVectorView& path_vector_view(); + [[nodiscard]] const PathVectorView& path_vector_view() const; + [[nodiscard]] double area() const; - friend bool operator==(const Face& a, const Face& b); + [[nodiscard]] bool operator==(const Face& other) const; + [[nodiscard]] bool operator!=(const Face& other) const; + [[nodiscard]] bool operator<(const Face& other) const; + + class ReferencePolisher; + void serialize(serialization::SerializerWorker& worker) const; + void deserialize(serialization::DeserializerWorker& worker); private: - std::deque m_edges; + std::unique_ptr m_path_vector_view; }; +enum class PolygonLocation { Inside, Outside, Edge }; +[[nodiscard]] PolygonLocation polygon_contains(const std::vector& polygon, const Vec2f& p); + } // namespace omm diff --git a/src/path/facedetector.cpp b/src/path/facedetector.cpp new file mode 100644 index 000000000..b556b1c9b --- /dev/null +++ b/src/path/facedetector.cpp @@ -0,0 +1,139 @@ +#include "path/facedetector.h" +#include "common.h" +#include "path/ccwcomparator.h" +#include "path/dedge.h" +#include "path/edge.h" +#include "path/face.h" +#include "path/pathpoint.h" +#include "path/pathvectorview.h" +#include +#include + +namespace +{ + +template std::set max_elements(const std::set& vs, const Key& key) +{ + if (vs.empty()) { + return {}; + } + std::set out{*vs.begin()}; + auto max_value = key(*vs.begin()); + for (auto it = std::next(vs.begin()); it != vs.end(); ++it) { + const auto& v = *it; + if (const auto value = key(v); value == max_value) { + out.insert(v); + } else if (value > max_value) { + out = {v}; + max_value = value; + } + } + return out; +} + +omm::DEdge find_next_edge(const omm::DEdge& current, const omm::Graph& graph, std::set& allowed_edges) +{ + const auto& hinge = current.end_point(); + const auto edges = graph.out_edges(hinge); + + const omm::CCWComparator compare_with_current(current); + std::set candidates; + for (const auto& de : edges) { + const auto is_current_reversed = current.edge == de.edge && current.direction != de.direction; + if (!is_current_reversed && allowed_edges.contains(de)) { + candidates.emplace(de); + } + } + + const auto min_it = std::min_element(candidates.begin(), candidates.end(), compare_with_current); + if (min_it == candidates.end()) { + return {}; + } else { + auto next_edge = *min_it; + allowed_edges.erase(next_edge); + return next_edge; + } +} + +std::deque follow_edge(const omm::DEdge& seed, const omm::Graph& graph, std::set& allowed_edges) +{ + std::deque sequence; + auto next = seed; + + do { + next = find_next_edge(next, graph, allowed_edges); + if (next.edge == nullptr) { + if (sequence.empty() && seed.edge->a() == seed.edge->b()) { + allowed_edges.erase(seed); + return {seed}; + } + return {}; + } + sequence.emplace_back(next); + } while (next != seed); + return sequence; +} + +} // namespace + +namespace omm::face_detector +{ + +std::set compute_faces_on_connected_graph_without_dead_ends(const Graph& graph) +{ + std::set allowed_edges; + std::set faces; + for (auto* e : graph.edges()) { + allowed_edges.emplace(e, Direction::Backward); + allowed_edges.emplace(e, Direction::Forward); + } + + while (!allowed_edges.empty()) { + + const auto current = *allowed_edges.begin(); + const auto sequence = follow_edge(current, graph, allowed_edges); + + if (!sequence.empty()) { + faces.emplace(omm::PathVectorView(sequence)); + } + } + return faces; +} + +auto argmax(const auto& values, const auto& key) +{ + using value_type = decltype(key(*values.begin())); + auto max = -std::numeric_limits::infinity(); + auto max_it = values.end(); + for (auto it = values.begin(); it != values.end(); ++it) { + if (const auto v = key(*it); v >= max) { + max_it = it; + max = v; + } + } + return max_it; +} + +Face find_outer_face(const std::set& faces) +{ + assert(!faces.empty()); + const std::vector faces_v(faces.begin(), faces.end()); + return *argmax(faces, [](const auto& face) { return std::abs(face.area()); }); +} + +std::set compute_faces_without_outer(Graph graph) +{ + graph.remove_dead_ends(); + + std::set faces; + for (const auto& connected_component : graph.connected_components()) { + auto c_faces = compute_faces_on_connected_graph_without_dead_ends(connected_component); + if (c_faces.size() > 1) { + c_faces.erase(find_outer_face(c_faces)); + } + faces.insert(c_faces.begin(), c_faces.end()); + } + return faces; +} + +} // namespace omm::face_detector diff --git a/src/path/facedetector.h b/src/path/facedetector.h new file mode 100644 index 000000000..112ed30e2 --- /dev/null +++ b/src/path/facedetector.h @@ -0,0 +1,21 @@ +#pragma once + +#include "graph.h" +#include +#include + +namespace omm +{ + +class Face; + +namespace face_detector +{ + +[[nodiscard]] std::set compute_faces_without_outer(Graph graph); +[[nodiscard]] std::set compute_faces_on_connected_graph_without_dead_ends(const Graph& graph); +[[nodiscard]] Face find_outer_face(const std::set& faces); + +} // namespace face_detector + +} // namespace omm diff --git a/src/path/graph.cpp b/src/path/graph.cpp index 382327995..ba3db5f68 100644 --- a/src/path/graph.cpp +++ b/src/path/graph.cpp @@ -1,283 +1,165 @@ #include "path/graph.h" -#include "path/pathpoint.h" -#include "path/face.h" #include "path/edge.h" #include "path/pathvector.h" -#include "path/path.h" -#include -#include -#include -#include +#include "removeif.h" +#include "transform.h" +#include -namespace -{ - -struct Vertex +namespace omm { - ::transparent_set points; - [[nodiscard]] QString debug_id() const { return (*points.begin())->debug_id(); } -}; -template void identify_edges(Graph& graph) +Graph::Graph(const PathVector& path_vector) : m_edges(util::transform(path_vector.edges())) { - auto e_index = get(boost::edge_index, graph); - typename boost::graph_traits::edges_size_type edge_count = 0; - for(auto [it, end] = edges(graph); it != end; ++it) { - put(e_index, *it, edge_count++); + for (auto* edge : m_edges) { + m_adjacent_edges[edge->a().get()].insert(edge); + m_adjacent_edges[edge->b().get()].insert(edge); + m_out_edges[edge->a().get()].insert(DEdge::fwd(edge)); + m_out_edges[edge->b().get()].insert(DEdge::bwd(edge)); } } -} // namespace - -namespace omm -{ - -struct edge_data_t +void Graph::remove_edge(Edge* edge) { - using kind = boost::edge_property_tag; -}; + m_edges.erase(edge); -struct vertex_data_t -{ - using kind = boost::vertex_property_tag; -}; - -using namespace boost; -class Graph::Impl : public adjacency_list>, - property> - > -{ -public: - using Joint = ::transparent_set; - using VertexIndexMap = std::map; - using JointMap = std::deque; - using EdgeDescriptor = boost::detail::edge_desc_impl; - using VertexDescriptor = std::size_t; - using Embedding = std::vector>; - using adjacency_list::adjacency_list; - [[nodiscard]] const Vertex& data(VertexDescriptor vertex) const; - [[nodiscard]] Vertex& data(VertexDescriptor vertex); - [[nodiscard]] const Edge& data(EdgeDescriptor edge_descriptor) const; - [[nodiscard]] Edge& data(EdgeDescriptor edge_descriptor); - void add_vertex(PathPoint* path_point); - bool add_edge(PathPoint* a, PathPoint* b); - [[nodiscard]] VertexDescriptor lookup_vertex(const PathPoint* p) const; - [[nodiscard]] Embedding compute_embedding() const; - [[nodiscard]] PolarCoordinates get_direction_at(const Edge& edge, VertexDescriptor vertex) const; - -private: - VertexIndexMap m_vertex_index_map; - JointMap m_joint_map; -}; - -Graph::Graph(const PathVector& path_vector) - : m_impl(std::make_unique()) -{ - for (const auto* path : path_vector.paths()) { - PathPoint* last_path_point = nullptr; - for (auto* point : path->points()) { - m_impl->add_vertex(point); - if (last_path_point != nullptr) { - m_impl->add_edge(point, last_path_point); + for (auto* const p : edge->points()) { + if (const auto it = m_out_edges.find(p); it != m_out_edges.end()) { + it->second.erase(DEdge::fwd(edge)); + it->second.erase(DEdge::bwd(edge)); + if (it->second.empty()) { + m_out_edges.erase(it); + } + } + if (const auto it = m_adjacent_edges.find(p); it != m_adjacent_edges.end()) { + it->second.erase(edge); + if (it->second.empty()) { + m_adjacent_edges.erase(it); } - last_path_point = point; } } } -std::vector Graph::compute_faces() const +const std::set& Graph::edges() const { - using Faces = std::list; - Faces faces; - struct Visitor : boost::planar_face_traversal_visitor - { - Visitor(const Impl& impl, Faces& faces) : faces(faces), m_impl(impl) {} - Faces& faces; - std::optional current_face; - - void begin_face() - { - current_face = Face{}; - } - - void next_edge(const Impl::EdgeDescriptor edge) - { - [[maybe_unused]] bool success = current_face->add_edge(m_impl.data(edge)); - assert(success); - } - - void end_face() - { - faces.emplace_back(*current_face); - current_face = std::nullopt; - } - - private: - const Impl& m_impl; - }; - - identify_edges(*m_impl); - const auto embedding = m_impl->compute_embedding(); - Visitor visitor{*m_impl, faces}; - boost::planar_face_traversal(*m_impl, &embedding[0], visitor); - - if (faces.empty()) { - return {}; - } - - // we don't want to include the largest face, which is contains the whole universe expect the path. - const auto areas = util::transform(faces, std::mem_fn(&Face::compute_aabb_area)); - const auto largest_face_i = std::distance(areas.begin(), std::max_element(areas.begin(), areas.end())); - faces.erase(std::next(faces.begin(), largest_face_i)); - - // NOLINTNEXTLINE(modernize-return-braced-init-list) - std::vector vfaces(faces.begin(), faces.end()); - vfaces.erase(std::unique(vfaces.begin(), vfaces.end()), vfaces.end()); - return vfaces; + return m_edges; } -void Graph::Impl::add_vertex(PathPoint* path_point) +const std::set& Graph::out_edges(const PathPoint& from) const { - // if a point is not joined, we need a set containing only that lonely point. - const auto vertex_points = [path_point]() -> ::transparent_set { - if (auto set = path_point->joined_points(); set.empty()) { - return {path_point}; - } else { - return set; - } - }(); - - // if a vertex was already assigned to a joined point, re-use that vertex. - for (auto* jp : vertex_points) { - if (const auto it = m_vertex_index_map.find(jp); it != m_vertex_index_map.end()) { - m_vertex_index_map.emplace(path_point, it->second); - return; - } + static std::set empty; + if (const auto it = m_out_edges.find(&from); it == m_out_edges.end()) { + return empty; + } else { + return it->second; } - - // if none of this point's joints has a vertex assigned, create a new one. - const auto n = boost::add_vertex(*this); - m_vertex_index_map.emplace(path_point, n); - m_joint_map.emplace_back(vertex_points); - data(lookup_vertex(path_point)).points = vertex_points; } -bool Graph::Impl::add_edge(PathPoint* a, PathPoint* b) +void Graph::remove_dead_ends() { - assert(&a->path() == &b->path()); - const auto ai = m_vertex_index_map.at(a); - const auto bi = m_vertex_index_map.at(b); - const auto [edge, was_inserted] = boost::add_edge(ai, bi, *this); - auto& edge_data = data(edge); - edge_data.a = a; - edge_data.b = b; - return was_inserted; -} + const auto is_not_on_fringe = [this](const auto& edge) { return degree(*edge->a()) != 1 && degree(*edge->b()) != 1; }; + std::set fringe = util::remove_if(m_edges, is_not_on_fringe); -Graph::Impl::VertexDescriptor Graph::Impl::lookup_vertex(const PathPoint* p) const -{ - return m_vertex_index_map.at(p); -} + const auto add_to_fringe = [this, &fringe, &is_not_on_fringe](const PathPoint& p) { + const auto edges = util::remove_if(m_adjacent_edges.at(&p), is_not_on_fringe); + fringe.insert(edges.begin(), edges.end()); + }; -Graph::Impl::Embedding Graph::Impl::compute_embedding() const -{ - const auto n = boost::num_vertices(*this); - Graph::Impl::Embedding embedding(n); - for (auto [v, vend] = boost::vertices(*this); v != vend; ++v) { - std::deque edges; - for (auto [e, eend] = out_edges(*v, *this); e != eend; ++e) { - edges.emplace_back(*e); + while (!fringe.empty()) { + auto* const current = fringe.extract(fringe.begin()).value(); + remove_edge(*current); + const auto i1 = m_adjacent_edges.find(current->a().get()); + const auto i2 = m_adjacent_edges.find(current->b().get()); + if (i1 != m_adjacent_edges.end() && i2 != m_adjacent_edges.end()) { + // current was not on fringe, impossible. + } else if (i1 != m_adjacent_edges.end()) { + add_to_fringe(*current->a()); + } else if (i2 != m_adjacent_edges.end()) { + add_to_fringe(*current->b()); + } else { + // may happen if current edge is lonely } - std::sort(edges.begin(), edges.end(), [this, v=*v](const auto e1, const auto e2) { - const auto a1 = python_like_mod(get_direction_at(data(e1), v).argument, 2 * M_PI); - const auto a2 = python_like_mod(get_direction_at(data(e2), v).argument, 2 * M_PI); - return a1 < a2; - }); - embedding[*v] = std::move(edges); } - return embedding; } -PolarCoordinates Graph::Impl::get_direction_at(const Edge& edge, VertexDescriptor vertex) const +std::list Graph::connected_components() const { - const auto compute_edge_direction = [](const Point& major, const Point& minor, const auto& get_tangent) { - static constexpr double eps = 0.00001; - if (const auto tangent = get_tangent(major); tangent.magnitude > eps) { - return tangent; - } else { - return PolarCoordinates{-major.position() + minor.position()}; + auto unvisited_vs = vertices(); + std::list> components; + while (!unvisited_vs.empty()) { + const auto& current_component = components.emplace_back(connected_component(**unvisited_vs.begin())); + for (auto* const v : current_component) { + unvisited_vs.erase(v); } - }; - const auto& joint = m_joint_map.at(vertex); - if (joint.contains(edge.a)) { - return compute_edge_direction(edge.a->geometry(), edge.b->geometry(), std::mem_fn(&Point::left_tangent)); - } else { - assert(joint.contains(edge.b)); - return compute_edge_direction(edge.b->geometry(), edge.a->geometry(), std::mem_fn(&Point::right_tangent)); } -} - -Graph::~Graph() = default; -const Vertex& Graph::Impl::data(const VertexDescriptor vertex) const -{ - return get(vertex_data_t{}, *this)[vertex]; + return util::transform(components, [this](const auto& component) { return subgraph(component); }); } -Vertex& Graph::Impl::data(const VertexDescriptor vertex) +Graph Graph::subgraph(const std::set& vertices) const { - return get(vertex_data_t{}, *this)[vertex]; + Graph g; + for (auto* const v : vertices) { + const auto edges = m_adjacent_edges.at(v); + g.m_adjacent_edges.emplace(v, edges); + g.m_edges.insert(edges.begin(), edges.end()); + g.m_out_edges.emplace(v, m_out_edges.at(v)); + } + return g; } -const Edge& Graph::Impl::data(EdgeDescriptor edge) const -{ - return get(edge_data_t{}, *this)[edge]; +std::set Graph::connected_component(PathPoint& seed) const +{ + std::set dfs{&seed}; + std::set component; + while (!dfs.empty()) { + auto* const v = dfs.extract(dfs.begin()).value(); + component.insert(v); + for (auto* const e : m_adjacent_edges.at(v)) { + for (auto* const p : e->points()) { + if (!component.contains(p)) { + dfs.insert(p); + } + } + } + } + return component; } -Edge& Graph::Impl::data(EdgeDescriptor edge) +std::set Graph::vertices() const { - return get(edge_data_t{}, *this)[edge]; + std::set vs; + for (const auto* const edge : m_edges) { + vs.insert(edge->a().get()); + vs.insert(edge->b().get()); + } + return vs; } -QString Graph::to_dot() const +std::size_t Graph::degree(const PathPoint& p) const { - const auto& g = *m_impl; - QString dot = "graph x {\n"; - for (auto ep = boost::edges(g); ep.first != ep.second; ++ep.first) { - const auto& edge = *ep.first; - auto v1 = boost::source(edge, g); - auto v2 = boost::target(edge, g); - dot += "\""; - dot += g.data(v1).debug_id(); - dot += "\" -- \""; - dot += g.data(v2).debug_id(); - dot += "\" [label=\""; - dot += g.data(edge).label(); - dot += "\"];\n"; + const auto it = m_adjacent_edges.find(&p); + if (it == m_adjacent_edges.end()) { + return 0; + } else { + const auto& edges = it->second; + const auto accumulate_degrees = [p = &p](const auto accu, const Edge* const edge) { + return accu + (edge->a().get() == p ? 1 : 0) + (edge->b().get() == p ? 1 : 0); + }; + return std::accumulate(edges.begin(), edges.end(), 0, accumulate_degrees); } - dot += "}"; - return dot;\ } -void Graph::remove_articulation_edges() const +void Graph::remove_edge(Edge& edge) { -// auto components = get(boost::edge_index, *m_impl); -// const auto n = boost::biconnected_components(*m_impl, components); -// LINFO << "Found " << n << " biconnected components."; - std::set art_points; - boost::articulation_points(*m_impl, std::inserter(art_points, art_points.end())); - const auto edge_between_articulation_points = [&art_points, this](const Impl::edge_descriptor& e) { - const auto o = [&art_points, this](const Impl::vertex_descriptor& v) { - return degree(v, *m_impl) <= 1 || art_points.contains(v); - }; - return o(e.m_source) && o(e.m_target); - }; - boost::remove_edge_if(edge_between_articulation_points, *m_impl); + m_edges.erase(&edge); + for (const auto& p : {edge.a(), edge.b()}) { + if (const auto it = m_adjacent_edges.find(p.get()); it != m_adjacent_edges.end()) { + it->second.erase(&edge); + if (it->second.empty()) { + m_adjacent_edges.erase(it); + } + } + } } } // namespace omm diff --git a/src/path/graph.h b/src/path/graph.h index 90eefbeca..ac1dd6024 100644 --- a/src/path/graph.h +++ b/src/path/graph.h @@ -1,43 +1,37 @@ #pragma once -#include "geometry/point.h" -#include -#include -#include -#include +#include "path/dedge.h" +#include +#include +#include namespace omm { class PathPoint; -class Edge; -class Face; -class PathVector; // NOLINT(bugprone-forward-declaration-namespace) +class PathVector; class Graph { public: - class Impl; - Graph(const PathVector& path_vector); - Graph(const Graph& other) = delete; - Graph(Graph&& other) = default; - Graph& operator=(const Graph& other) = delete; - Graph& operator=(Graph&& other) = default; - ~Graph(); - - [[nodiscard]] std::vector compute_faces() const; - [[nodiscard]] QString to_dot() const; - - /** - * @brief remove_articulation_edges an edge is articulated if it connects two articulated points, - * two points of degree one or less or an articulated point with a point of degree one or less. - * Removing articulated edges from a graph increase the number of components by one. - * Articulated edges can never be part of a face. - */ - void remove_articulation_edges() const; + explicit Graph(const PathVector& path_vector); + explicit Graph() = default; + void remove_edge(Edge* edge); + [[nodiscard]] const std::set& edges() const; + [[nodiscard]] const std::set& out_edges(const PathPoint& from) const; + + void remove_dead_ends(); + [[nodiscard]] std::list connected_components() const; + void remove_edge(Edge& edge); + [[nodiscard]] std::set vertices() const; + [[nodiscard]] std::set connected_component(PathPoint& seed) const; + [[nodiscard]] Graph subgraph(const std::set& vertices) const; + [[nodiscard]] std::size_t degree(const PathPoint& p) const; private: - std::unique_ptr m_impl; + std::set m_edges; + std::map> m_out_edges; + std::map> m_adjacent_edges; }; } // namespace omm diff --git a/src/path/lib2geomadapter.cpp b/src/path/lib2geomadapter.cpp index 43cf14358..ec7dbf842 100644 --- a/src/path/lib2geomadapter.cpp +++ b/src/path/lib2geomadapter.cpp @@ -1,48 +1,55 @@ #include "path/lib2geomadapter.h" +#include "path/edge.h" #include "path/path.h" -#include "path/pathvector.h" #include "path/pathpoint.h" -#include <2geom/pathvector.h> +#include "path/pathvector.h" namespace omm { -Geom::PathVector omm_to_geom(const PathVector& path_vector, InterpolationMode interpolation) +Geom::PathVector omm_to_geom(const PathVector& path_vector, const InterpolationMode interpolation) { Geom::PathVector paths; for (auto&& path : path_vector.paths()) { paths.push_back(omm_to_geom(*path, interpolation)); } + assert(path_vector.paths().size() == paths.size()); return paths; } -Geom::Path omm_to_geom(const Path& path, InterpolationMode interpolation) +template GeomEdgeType omm_to_geom(const Edge& edge) { - std::vector bzs; - const auto points = path.points(); - const std::size_t n = points.size(); - if (n == 0) { - return Geom::Path{}; - } + const auto& a = edge.a()->geometry(); + const auto& b = edge.b()->geometry(); + const auto a_pos = a.position().to_geom_point(); + const auto b_pos = b.position().to_geom_point(); - bzs.reserve(n - 1); - - std::unique_ptr smoothened; - const Path* self = &path; - if (interpolation == InterpolationMode::Smooth) { - smoothened = std::make_unique(path); - smoothened->smoothen(); - self = smoothened.get(); + if constexpr (interp == InterpolationMode::Linear) { + return Geom::LineSegment(std::vector{a_pos, b_pos}); + } else { + if (interp != InterpolationMode::Bezier) { + LWARNING << "Smooth mode is not yet implemented."; + } + const auto a_t = a.tangent_position({edge.path(), Direction::Forward}).to_geom_point(); + const auto b_t = b.tangent_position({edge.path(), Direction::Backward}).to_geom_point(); + return Geom::BezierCurveN<3>(std::vector{a_pos, a_t, b_t, b_pos}); } +} - for (std::size_t i = 0; i < n - 1; ++i) { - const auto cps = Path::compute_control_points(self->at(i).geometry(), - self->at(i + 1).geometry(), - interpolation); - bzs.emplace_back(util::transform(cps, std::mem_fn(&Vec2f::to_geom_point))); +Geom::Path omm_to_geom(const Path& path, const InterpolationMode interp) +{ + const auto make_path = [&path](const auto& edge_to_curve) { + const auto curves = util::transform(path.edges(), edge_to_curve); + return Geom::Path(curves.begin(), curves.end()); + }; + switch (interp) { + case InterpolationMode::Bezier: + return make_path([](const Edge* const edge) { return omm_to_geom(*edge); }); + case InterpolationMode::Linear: + return make_path([](const Edge* const edge) { return omm_to_geom(*edge); }); + case InterpolationMode::Smooth: + return make_path([](const Edge* const edge) { return omm_to_geom(*edge); }); } - - return {bzs.begin(), bzs.end()}; } std::unique_ptr geom_to_omm(const Geom::PathVector& geom_path_vector) @@ -56,19 +63,8 @@ std::unique_ptr geom_to_omm(const Geom::PathVector& geom_path_vector void add_cubic_bezier_to_path(Path& omm_path, const Geom::CubicBezier& c) { - const auto p0 = Vec2f(c[0]); - if (omm_path.size() == 0) { - omm_path.add_point(Point{p0}); - } - auto& last_point = *omm_path.points().back(); - auto geometry = last_point.geometry(); - geometry.set_right_tangent(PolarCoordinates(Vec2f(c[1]) - p0)); - last_point.set_geometry(geometry); - const auto p1 = Vec2f(c[3]); - auto& pref = omm_path.add_point(Point{p1}); - geometry = pref.geometry(); - geometry.set_left_tangent(PolarCoordinates(Vec2f(c[2]) - p1)); - pref.set_geometry(geometry); + (void) omm_path; + (void) c; } std::unique_ptr geom_to_omm(const Geom::Path& geom_path, PathVector* parent) @@ -85,4 +81,8 @@ std::unique_ptr geom_to_omm(const Geom::Path& geom_path, PathVector* paren return omm_path; } +template GeomEdgeType omm_to_geom(const Edge&); +template GeomEdgeType omm_to_geom(const Edge&); +template GeomEdgeType omm_to_geom(const Edge&); + } // namespace omm diff --git a/src/path/lib2geomadapter.h b/src/path/lib2geomadapter.h index 2cf633bfa..1bd8ca6f5 100644 --- a/src/path/lib2geomadapter.h +++ b/src/path/lib2geomadapter.h @@ -1,18 +1,14 @@ #pragma once #include "common.h" - -namespace Geom -{ -class PathVector; -class Path; -} // namespace Geom +#include <2geom/pathvector.h> namespace omm { class PathVector; class Path; +class Edge; [[nodiscard]] Geom::PathVector omm_to_geom(const PathVector& path_vector, InterpolationMode interpolation = InterpolationMode::Bezier); @@ -21,4 +17,8 @@ class Path; [[nodiscard]] std::unique_ptr geom_to_omm(const Geom::PathVector& path_vector); [[nodiscard]] std::unique_ptr geom_to_omm(const Geom::Path& geom_path, PathVector* parent); +template using GeomEdgeType = + std::conditional_t>; +template [[nodiscard]] GeomEdgeType omm_to_geom(const omm::Edge& edge); + } // namespace omm diff --git a/src/path/path.cpp b/src/path/path.cpp index 96c24017d..5f24c7ea9 100644 --- a/src/path/path.cpp +++ b/src/path/path.cpp @@ -1,37 +1,38 @@ #include "path/path.h" +#include "config.h" #include "geometry/point.h" +#include "path/dedge.h" +#include "path/edge.h" #include "path/pathpoint.h" -#include "serializers/abstractserializer.h" -#include "serializers/serializerworker.h" -#include "serializers/deserializerworker.h" -#include <2geom/pathvector.h> +#include "path/pathview.h" -namespace -{ +#include -using namespace omm; -auto copy(const std::deque>& vs, Path& path) +namespace { - std::decay_t copy; - for (auto&& v : vs) { - copy.emplace_back(std::make_unique(v->geometry(), path)); - } - return copy; -} -auto to_path_points(std::vector&& points, Path& path) +void replace_tangents_key(const auto& edges, const std::map& map) { - return util::transform(std::move(points), [&path](auto&& point) { - return std::make_unique(point, path); - }); + for (auto& edge : edges) { + for (auto& p : {edge->a(), edge->b()}) { + p->geometry().replace_tangents_key(map); + } + } } -auto to_path_points(std::deque&& points, Path& path) +[[maybe_unused]] void draw_arrow(QPainterPath& painter_path, const omm::PolarCoordinates& v, const double offset) { - return util::transform(std::move(points), [&path](auto&& point) { - return std::make_unique(point, path); - }); + const auto pos_before = painter_path.currentPosition(); + const auto origin = omm::Vec2f(pos_before) + offset * v.to_cartesian(); + const auto move_to = [v, &painter_path, origin](const double angle_offset) { + const auto end_pos = (v + angle_offset).to_cartesian() + origin; + painter_path.moveTo(origin.to_pointf()); + painter_path.lineTo(end_pos.to_pointf()); + }; + move_to(M_PI / 6.0); + move_to(-M_PI / 6.0); + painter_path.moveTo(pos_before); } } // namespace @@ -40,189 +41,283 @@ namespace omm { Path::Path(PathVector* path_vector) - : m_path_vector(path_vector) + : m_path_vector(path_vector) { } -Path::Path(std::deque&& points, PathVector* path_vector) - : m_points(to_path_points(std::move(points), *this)) - , m_path_vector(path_vector) +Path::~Path() = default; + +bool Path::contains(const PathPoint& point) const { + const auto points = this->points(); + return std::find(points.begin(), points.end(), &point) != points.end(); } -Path::Path(std::vector&& points, PathVector* path_vector) - : m_points(to_path_points(std::move(points), *this)) - , m_path_vector(path_vector) +std::shared_ptr Path::share(const PathPoint& point) const { + if (m_edges.empty()) { + if (m_last_point.get() == &point) { + return m_last_point; + } else { + return {}; + } + } + + if (const auto& a = m_edges.front()->a(); a.get() == &point) { + return a; + } + + for (const auto& edge : m_edges) { + if (const auto& b = edge->b(); b.get() == &point) { + return b; + } + } + + return {}; } -Path::Path(const Path& other, PathVector* path_vector) - : m_points(copy(other.m_points, *this)) - , m_path_vector(path_vector) +PathVector* Path::path_vector() const { + return m_path_vector; } -Path::~Path() = default; +void Path::set_path_vector(PathVector* path_vector) +{ + m_path_vector = path_vector; +} -std::size_t Path::size() const +void Path::set_single_point(std::shared_ptr single_point) { - return m_points.size(); + assert(m_edges.empty()); + assert(!m_last_point); + m_last_point = single_point; + m_last_point->geometry().replace_tangents_key({{nullptr, this}}); } -PathPoint& Path::at(std::size_t i) const +std::shared_ptr Path::extract_single_point() { - return *m_points.at(i); + assert(m_edges.empty()); + assert(m_last_point); + auto last_point = m_last_point; + m_last_point.reset(); + return last_point; } -bool Path::contains(const PathPoint& point) const +QString Path::print_edge_info() const { - return std::any_of(m_points.begin(), m_points.end(), [&point](const auto& candidate) { - return &point == candidate.get(); - }); + auto lines = util::transform(this->edges(), &Edge::to_string); + lines.push_front(QString::asprintf("== Path 0x%p, Edges %zu", static_cast(this), edges().size())); + if (edges().empty()) { + lines.append(QString::asprintf("Single point: 0x%p", static_cast(m_last_point.get()))); + } + return lines.join("\n"); } -std::size_t Path::find(const PathPoint& point) const +void Path::set_last_point_from_edges() { - const auto it = std::find_if(m_points.begin(), m_points.end(), [&point](const auto& candidate) { - return &point == candidate.get(); - }); - if (it == m_points.end()) { - throw std::out_of_range("No such point in path."); + if (m_edges.empty()) { + m_last_point = nullptr; } else { - return std::distance(m_points.begin(), it); + m_last_point = m_edges.back()->b(); } } -PathPoint& Path::add_point(const Point& point) +Edge& Path::add_edge(std::shared_ptr a, std::shared_ptr b) { - return *m_points.emplace_back(std::make_unique(point, *this)); + return add_edge(std::make_unique(a, b, this)); } -void Path::make_linear() const +std::shared_ptr Path::last_point() const { - for (const auto& point : m_points) { - point->set_geometry(point->geometry().nibbed()); - } + return m_last_point; } -void Path::set_interpolation(InterpolationMode interpolation) const +std::shared_ptr Path::first_point() const { - switch (interpolation) { - case InterpolationMode::Bezier: - return; - case InterpolationMode::Smooth: - smoothen(); - return; - case InterpolationMode::Linear: - make_linear(); - return; + if (m_edges.empty()) { + return m_last_point; + } else { + return m_edges.front()->a(); } - Q_UNREACHABLE(); } -std::vector Path::compute_control_points(const Point& a, const Point& b, InterpolationMode interpolation) +Edge& Path::add_edge(std::unique_ptr edge) { - static constexpr double t = 1.0 / 3.0; - switch (interpolation) { - case InterpolationMode::Bezier: - [[fallthrough]]; - case InterpolationMode::Smooth: - return {a.position(), - a.right_position(), - b.left_position(), - b.position()}; - break; - case InterpolationMode::Linear: - return {a.position(), - ((1.0 - t) * a.position() + t * b.position()), - ((1.0 - t) * b.position() + t * a.position()), - b.position()}; - break; + assert(edge->a() && edge->b()); + ::replace_tangents_key(std::vector{edge.get()}, {{nullptr, this}}); + + const auto try_emplace = [this](std::unique_ptr& edge) { + if (m_last_point == nullptr || last_point().get() == edge->a().get()) { + m_edges.emplace_back(std::move(edge)); + } else if (first_point().get() == edge->b().get()) { + m_edges.emplace_front(std::move(edge)); + } else { + return false; + } + return true; + }; + + auto& ref = *edge; + + if (!try_emplace(edge)) { + edge->flip(); + if (!try_emplace(edge)) { + throw PathException{"Cannot add edge to path."}; + } } - Q_UNREACHABLE(); - return {}; -} -PathVector* Path::path_vector() const -{ - return m_path_vector; + set_last_point_from_edges(); + return ref; } -void Path::set_path_vector(PathVector* path_vector) + +std::deque> Path::replace(const PathView& path_view, std::deque> edges) { - m_path_vector = path_vector; + assert(is_valid(edges)); + assert(is_valid()); + + ::replace_tangents_key(edges, {{nullptr, this}}); + + const auto swap_edges = [this](const auto& begin, const auto& end, std::deque>&& edges) { + std::deque> removed_edges; + std::copy(std::move_iterator(begin), std::move_iterator(end), std::back_inserter(removed_edges)); + auto gap_begin = m_edges.erase(begin, end); + m_edges.insert(gap_begin, std::move_iterator(edges.begin()), std::move_iterator(edges.end())); + return removed_edges; + }; + + const bool set_last_point_from_edges = [this, &edges, path_view]() { + if (edges.empty() && path_view.point_count() == points().size() - 1) { + // There will be no edges left but m_last_point needs to be set. + + if (path_view.begin() > 0) { + // all edges will be removed and all points except the first one. + m_last_point = first_point(); + } // else all edges will be removed and all points except the last one. + + return false; // Don't update m_last_point later, + } else { + return true; // Do update m_last_point later + } + }(); + + std::deque> removed_edges; + if (path_view.begin() == 0 && path_view.end() == points().size()) { + // all points are replaced + removed_edges = swap_edges(m_edges.begin(), m_edges.end(), std::move(edges)); + } else if (path_view.begin() == 0) { + // append left + removed_edges = swap_edges(m_edges.begin(), m_edges.begin() + path_view.point_count(), std::move(edges)); + } else if (path_view.end() == points().size()) { + // append right + removed_edges = swap_edges(m_edges.end() - path_view.point_count(), m_edges.end(), std::move(edges)); + } else { + // append middle + const auto begin = m_edges.begin() + path_view.begin() - 1; + removed_edges = swap_edges(begin, + begin + path_view.point_count() + 1, + std::move(edges)); + } + + if (set_last_point_from_edges) { + this->set_last_point_from_edges(); + } + assert(is_valid()); + assert(is_valid(removed_edges)); + return removed_edges; } -void Path::smoothen() const +bool Path::is_valid() const { - for (std::size_t i = 0; i < m_points.size(); ++i) { - m_points[i]->set_geometry(smoothen_point(i)); + if (m_last_point == nullptr) { + if (m_edges.empty()) { + return true; + } else { + LERROR << "Last point may only be null if path is empty, but path has edges."; + return false; + } + } + + if (!m_edges.empty() && m_edges.back()->b() != m_last_point) { + LERROR << "Is not valid because last point is inconsistent."; + return false; } + + const auto all_points_have_tangents + = std::all_of(m_edges.begin(), m_edges.end(), [this](const auto& edge) { + return edge->a()->geometry().tangents().contains({this, Direction::Backward}) + && edge->a()->geometry().tangents().contains({this, Direction::Forward}) + && edge->b()->geometry().tangents().contains({this, Direction::Backward}) + && edge->b()->geometry().tangents().contains({this, Direction::Forward}); + }); + return is_valid(m_edges) && all_points_have_tangents; } -Point Path::smoothen_point(std::size_t i) const +std::vector Path::points() const { - const std::size_t n = m_points.size(); - PathPoint* left = nullptr; - PathPoint* right = nullptr; - Point copy = m_points[i]->geometry(); - if (m_points.size() < 2) { - return copy; + if (m_edges.empty()) { + if (m_last_point) { + return {m_last_point.get()}; + } + return {}; } - if (i == 0) { - left = m_points.at(0).get(); - right = m_points.at(1).get(); - } else if (i == n - 1) { - left = m_points.at(n - 2).get(); - right = m_points.at(n - 1).get(); - } else { - left = m_points.at(i - 1).get(); - right = m_points.at(i + 1).get(); + + std::vector points; + points.reserve(m_edges.size() + 1); + points.emplace_back(m_edges.front()->a().get()); + for (const auto& edge : m_edges) { + points.emplace_back(edge->b().get()); } - const Vec2f d = (left->geometry().position() - right->geometry().position()) / 6.0; - copy.set_right_tangent(PolarCoordinates(-d)); - copy.set_left_tangent(PolarCoordinates(d)); - return copy; + return points; } -std::deque Path::points() const +std::vector Path::edges() const { - return util::transform(m_points, [](const auto& pt) { return pt.get(); }); + return util::transform(m_edges, &std::unique_ptr::get); } -void Path::insert_points(std::size_t i, std::deque>&& points) +Edge& Path::edge(std::size_t i) const { - m_points.insert(std::next(m_points.begin(), static_cast(i)), - std::make_move_iterator(points.begin()), - std::make_move_iterator(points.end())); + return *m_edges.at(i); } -std::deque > Path::extract(std::size_t start, std::size_t size) +void Path::draw_segment(QPainterPath& painter_path, const Edge& edge, const Path* const path) { - std::deque> points(size); - for (std::size_t i = 0; i < size; ++i) { - std::swap(points[i], m_points[i + start]); + + const auto g1 = edge.a()->geometry(); + const auto t1 = g1.tangent_position({path, Direction::Forward}); + const auto g2 = edge.b()->geometry(); + const auto t2 = g2.tangent_position({path, Direction::Backward}); + + const DEdgeConst dedge(&edge, Direction::Forward); + + static constexpr auto len = [](const auto& p1, const auto& p2) { return (p1 - p2).euclidean_norm(); }; + [[maybe_unused]] const auto arrow_size = (len(g1.position(), t1) + len(t1, t2) + len(t2, g2.position())) / 5.0; + if constexpr (PATH_DRAW_DIRECTION) { + const PolarCoordinates v(dedge.start_angle() + M_PI, arrow_size); + draw_arrow(painter_path, 0.5 * v, -0.1); } - const auto begin = std::next(m_points.begin(), static_cast(start)); - const auto end = std::next(begin, static_cast(size)); - m_points.erase(begin, end); - return points; -} -void Path::serialize(serialization::SerializerWorker& worker) const -{ - worker.sub(POINTS_POINTER)->set_value(m_points, [](const auto& point, auto& worker_i) { - point->geometry().serialize(worker_i); - }); + painter_path.cubicTo(t1.to_pointf(), t2.to_pointf(), g2.position().to_pointf()); + + if constexpr (PATH_DRAW_DIRECTION) { + const PolarCoordinates v(dedge.end_angle(), arrow_size); + draw_arrow(painter_path, v, 0.1); + } } -void Path::deserialize(serialization::DeserializerWorker& worker) +QPainterPath Path::to_painter_path() const { - worker.sub(POINTS_POINTER)->get_items([this](auto& worker_i) { - Point geometry; - geometry.deserialize(worker_i); - m_points.emplace_back(std::make_unique(geometry, *this)); - }); + if (m_edges.empty()) { + return {}; + } + QPainterPath painter_path; + painter_path.moveTo(m_edges.front()->a()->geometry().position().to_pointf()); + for (const auto& edge : m_edges) { + draw_segment(painter_path, *edge, this); + } + return painter_path; } + } // namespace omm diff --git a/src/path/path.h b/src/path/path.h index 03fdffeef..2fcdacd6e 100644 --- a/src/path/path.h +++ b/src/path/path.h @@ -1,22 +1,19 @@ #pragma once #include "common.h" -#include "geometry/vec2.h" +#include "logging.h" #include #include -#include -namespace omm -{ +class QPainterPath; -namespace serialization +namespace omm { -class SerializerWorker; -class DeserializerWorker; -} // namespace serialization class Point; class PathPoint; +class Edge; +class PathView; // NOLINTNEXTLINE(bugprone-forward-declaration-namespace) class Path; @@ -24,14 +21,17 @@ class Path; // NOLINTNEXTLINE(bugprone-forward-declaration-namespace) class PathVector; +class PathException : std::runtime_error +{ +public: + using std::runtime_error::runtime_error; +}; + // NOLINTNEXTLINE(bugprone-forward-declaration-namespace) class Path { public: explicit Path(PathVector* path_vector = nullptr); - explicit Path(const Path& other, PathVector* path_vector = nullptr); - explicit Path(std::deque&& points, PathVector* path_vector = nullptr); - explicit Path(std::vector&& points, PathVector* path_vector = nullptr); ~Path(); Path(Path&&) = delete; Path& operator=(const Path&) = delete; @@ -39,49 +39,66 @@ class Path static constexpr auto POINTS_POINTER = "points"; - void serialize(serialization::SerializerWorker& worker) const; - void deserialize(serialization::DeserializerWorker& worker); - [[nodiscard]] std::size_t size() const; - [[nodiscard]] PathPoint& at(std::size_t i) const; + Edge& add_edge(std::unique_ptr edge); + Edge& add_edge(std::shared_ptr a, std::shared_ptr b); + std::shared_ptr last_point() const; + std::shared_ptr first_point() const; + + static void draw_segment(QPainterPath& painter_path, const Edge& edge, const Path* path); + QPainterPath to_painter_path() const; + + + /** + * @brief Path::replace replaces the points selected by @param path_view with @param edges. + * @param path_view the point selection to be removed + * @param edges the edges that fill the gap. + * The first point of the first edge in this deque must match the last point left of the gap, + * unless there are no points left of the gap. + * The last point of the last edge in this deque must match the first point right of the gap, + * unless there are no points right of the gap. + * @return The edges that have been removed. + */ + std::deque> replace(const PathView& path_view, std::deque> edges); + + [[nodiscard]] bool is_valid() const; + [[nodiscard]] std::vector points() const; + [[nodiscard]] std::vector edges() const; + [[nodiscard]] Edge& edge(std::size_t i) const; + [[nodiscard]] bool contains(const PathPoint& point) const; - [[nodiscard]] std::size_t find(const PathPoint& point) const; - PathPoint& add_point(const Point& point); - void make_linear() const; - void smoothen() const; - [[nodiscard]] Point smoothen_point(std::size_t i) const; - [[nodiscard]] std::deque points() const; - void insert_points(std::size_t i, std::deque >&& points); - [[nodiscard]] std::deque> extract(std::size_t start, std::size_t size); - [[nodiscard]] static std::vector - compute_control_points(const Point& a, const Point& b, InterpolationMode interpolation = InterpolationMode::Bezier); + [[nodiscard]] std::shared_ptr share(const PathPoint& point) const; [[nodiscard]] PathVector* path_vector() const; void set_path_vector(PathVector* path_vector); void set_interpolation(InterpolationMode interpolation) const; + void set_single_point(std::shared_ptr single_point); + std::shared_ptr extract_single_point(); - template static QPainterPath to_painter_path(const Points& points, bool close = false) + template [[nodiscard]] static bool is_valid(const Edges& edges) { - if (points.empty()) { - return {}; + if (edges.empty()) { + return true; } - QPainterPath path; - path.moveTo(points.front().position().to_pointf()); - for (auto it = points.begin(); next(it) != points.end(); ++it) { - path.cubicTo(it->right_position().to_pointf(), - next(it)->left_position().to_pointf(), - next(it)->position().to_pointf()); + if (!std::all_of(edges.begin(), edges.end(), [](const auto& edge) { return edge->is_valid(); })) { + LERROR << "Is not valid because one or more edges contain invalid points."; + return false; } - if (close) { - path.cubicTo(points.back().right_position().to_pointf(), - points.front().left_position().to_pointf(), - points.front().position().to_pointf()); + for (auto it = begin(edges); next(it) != end(edges); advance(it, 1)) { + if ((*it)->b() != (*next(it))->a()) { + LERROR << "Is not valid because edges are not connected."; + return false; + } } - return path; + return true; } + QString print_edge_info() const; + private: - std::deque> m_points; + std::shared_ptr m_last_point; + std::deque> m_edges; PathVector* m_path_vector; + void set_last_point_from_edges(); }; } // namespace diff --git a/src/path/pathpoint.cpp b/src/path/pathpoint.cpp index 43fcba168..fb7466b9d 100644 --- a/src/path/pathpoint.cpp +++ b/src/path/pathpoint.cpp @@ -1,99 +1,110 @@ #include "path/pathpoint.h" +#include "path/edge.h" #include "path/path.h" #include "path/pathvector.h" -#include "objects/pathobject.h" -#include "scene/disjointpathpointsetforest.h" -#include "scene/scene.h" -namespace omm -{ -PathPoint::PathPoint(const Point& geometry, Path& path) - : m_geometry(geometry) - , m_path(path) +namespace { -} -PathPoint::PathPoint(Path& path) - : m_path(path) +std::pair +compute_smooth_tangents(const omm::PathPoint& point, const omm::Path& path) { -} + const auto points = path.points(); + const auto it = std::find_if(points.begin(), points.end(), [&point](const auto* candidate) { + return &point == candidate; + }); + assert(it != points.end()); + const auto* const bwd = it == points.begin() ? nullptr : *prev(it); + const auto* const fwd = next(it) == points.end() ? nullptr : *next(it); -::transparent_set PathPoint::joined_points() const -{ - return path_vector()->joined_points().get(this); -} -void PathPoint::join(::transparent_set buddies) -{ - buddies.insert(this); - path_vector()->joined_points().insert(buddies); + const auto pos = point.geometry().position(); + + static constexpr auto t = 1.0 / 3.0; + using PC = omm::PolarCoordinates; + static constexpr auto lerp = [](const double t, const auto& a, const auto& b) { + return (1.0 - t) * a + t * b; + }; + + auto bwd_pc = bwd == nullptr ? PC() : PC(lerp(t, pos, bwd->geometry().position()) - pos); + auto fwd_pc = fwd == nullptr ? PC() : PC(lerp(t, pos, fwd->geometry().position()) - pos); + + if (bwd != nullptr && fwd != nullptr) { + const auto p_bwd = bwd->geometry().position(); + const auto p_fwd = fwd->geometry().position(); + const auto p_bwd_reflected = (2.0 * pos - p_bwd); + fwd_pc.argument = omm::PolarCoordinates(lerp(0.5, p_bwd_reflected, p_fwd) - pos).argument; + bwd_pc.argument = (-fwd_pc).argument; + } + + return {bwd_pc, fwd_pc}; } -void PathPoint::disjoin() +} // namespace + +namespace omm { - path_vector()->joined_points().remove({this}); -} -PathVector* PathPoint::path_vector() const +PathPoint::PathPoint(const Point& geometry, const PathVector* path_vector) + : m_path_vector(path_vector), m_geometry(geometry) { - return m_path.path_vector(); } -Point PathPoint::compute_joined_point_geometry(PathPoint& joined) const +Point PathPoint::set_interpolation(InterpolationMode mode) const { - const auto controller_t = path_vector()->path_object()->global_transformation(Space::Scene); - const auto agent_t = joined.path_vector()->path_object()->global_transformation(Space::Scene); - const auto t = agent_t.inverted().apply(controller_t); - auto geometry = joined.geometry(); - geometry.set_position(t.apply(this->geometry()).position()); - - // TODO handle tangents - - return geometry; + auto copy = m_geometry; + auto& tangents = copy.tangents(); + for (auto& [key, tangent] : tangents) { + switch (mode) + { + case InterpolationMode::Linear: + tangent.magnitude = 0.0; + break; + case InterpolationMode::Bezier: + break; + case InterpolationMode::Smooth: + if (key.direction == Direction::Forward) { + const auto [bwd, fwd] = compute_smooth_tangents(*this, *key.path); + tangents[{key.path, Direction::Forward}] = fwd; + tangents[{key.path, Direction::Backward}] = bwd; + } + break; + } + } + return copy; } -bool PathPoint::is_dangling() const +const PathVector* PathPoint::path_vector() const { - if (path_vector() == nullptr || !path().contains(*this)) { - return true; - } - if (!::contains(path_vector()->paths(), &path())) { - return false; - } - const auto* const path_object = path_vector()->path_object(); - if (path_object == nullptr) { - return false; - } - const auto* const scene = path_object->scene(); - return scene == nullptr || !scene->contains(path_object); + return m_path_vector; } QString PathPoint::debug_id() const { - auto joins = util::transform(joined_points()); - if (joins.empty()) { - joins = {this}; + static constexpr bool print_pointer = false; + if constexpr (print_pointer) { + return QString{"%1 (%2)"}.arg(index()).arg(QString::asprintf("%p", static_cast(this))); + } else { + return QString{"%1"}.arg(index()); } - QStringList ids = util::transform(joins, [](const auto* p) { - return QString("%1").arg(p->index()); - }); - std::sort(ids.begin(), ids.end()); - return "(" + ids.join(" ") + ")"; } std::size_t PathPoint::index() const { assert(path_vector() != nullptr); - std::size_t offset = 0; - for (auto* path : path_vector()->paths()) { - if (path->contains(*this)) { - return offset + path->find(*this); - } else { - offset += path->size(); - } + const auto points = path_vector()->points(); + return std::distance(points.begin(), std::find(points.begin(), points.end(), this)); +} + +std::set PathPoint::edges() const +{ + if (m_path_vector == nullptr) { + return {}; } - throw std::runtime_error("Point is not part of a path."); + auto edges = util::transform(m_path_vector->edges()); + std::erase_if(edges, [this](const auto* edge) { return !edge->contains(this); }); + return edges; } void PathPoint::set_geometry(const Point& point) @@ -101,19 +112,14 @@ void PathPoint::set_geometry(const Point& point) m_geometry = point; } -const Point& PathPoint::geometry() const +Point& PathPoint::geometry() { return m_geometry; } -PathPoint PathPoint::copy(Path& path) const -{ - return PathPoint(m_geometry, path); -} - -Path& PathPoint::path() const +const Point& PathPoint::geometry() const { - return m_path; + return m_geometry; } bool PathPoint::is_selected() const @@ -121,14 +127,9 @@ bool PathPoint::is_selected() const return m_is_selected; } -void PathPoint::set_selected(bool selected, bool update_buddies) +void PathPoint::set_selected(bool selected) { m_is_selected = selected; - if (update_buddies) { - for (auto* buddy : joined_points()) { - buddy->set_selected(selected, false); - } - } } } // namespace omm diff --git a/src/path/pathpoint.h b/src/path/pathpoint.h index 702b35843..974df20a2 100644 --- a/src/path/pathpoint.h +++ b/src/path/pathpoint.h @@ -1,30 +1,26 @@ #pragma once +#include "common.h" #include "geometry/point.h" -#include "transparentset.h" #include namespace omm { -// NOLINTNEXTLINE(bugprone-forward-declaration-namespace) -class Path; - -// NOLINTNEXTLINE(bugprone-forward-declaration-namespace) -class PathVector; +class Path; // NOLINT(bugprone-forward-declaration-namespace) +class PathVector; // NOLINT(bugprone-forward-declaration-namespace) +class Edge; class PathPoint { public: - explicit PathPoint(const Point& geometry, Path& path); - explicit PathPoint(Path& path); + explicit PathPoint(const Point& geometry, const PathVector* path_vector); void set_geometry(const Point& point); [[nodiscard]] const Point& geometry() const; - PathPoint copy(Path& path) const; - [[nodiscard]] Path& path() const; + [[nodiscard]] Point& geometry(); static constexpr auto TYPE = QT_TRANSLATE_NOOP("PathPoint", "PathPoint"); - void set_selected(bool is_selected, bool update_buddies = true); + void set_selected(bool is_selected); [[nodiscard]] bool is_selected() const; // The PathPoint is identified by it's memory address, which hence must not change during its @@ -36,13 +32,9 @@ class PathPoint PathPoint& operator=(const PathPoint& other) = delete; PathPoint& operator=(PathPoint&& other) = delete; ~PathPoint() = default; + Point set_interpolation(InterpolationMode mode) const; - [[nodiscard]] ::transparent_set joined_points() const; - void join(::transparent_set buddies); - void disjoin(); - [[nodiscard]] PathVector* path_vector() const; - [[nodiscard]] Point compute_joined_point_geometry(PathPoint& joined) const; - [[nodiscard]] bool is_dangling() const; + [[nodiscard]] const PathVector* path_vector() const; /** * @brief debug_id returns an string to identify the point uniquely at this point in time @@ -58,9 +50,14 @@ class PathPoint */ [[nodiscard]] std::size_t index() const; + /** + * @brief edges enumerates all edges that touch this point. + */ + std::set edges() const; + private: + const PathVector* m_path_vector; Point m_geometry; - Path& m_path; bool m_is_selected = false; }; diff --git a/src/path/pathvector.cpp b/src/path/pathvector.cpp index 20280ebea..4f4d2579f 100644 --- a/src/path/pathvector.cpp +++ b/src/path/pathvector.cpp @@ -1,48 +1,20 @@ #include "path/pathvector.h" -#include "commands/modifypointscommand.h" #include "common.h" #include "geometry/point.h" -#include "properties/boolproperty.h" -#include "properties/optionproperty.h" -#include "renderers/style.h" -#include "scene/scene.h" -#include "scene/disjointpathpointsetforest.h" #include "objects/pathobject.h" -#include "path/pathpoint.h" -#include "path/path.h" -#include "path/graph.h" +#include "path/edge.h" #include "path/face.h" -#include "scene/mailbox.h" +#include "path/facedetector.h" +#include "path/graph.h" +#include "path/lib2geomadapter.h" +#include "path/path.h" +#include "path/pathpoint.h" +#include "path/pathvectorisomorphism.h" #include "removeif.h" #include - -namespace -{ - -constexpr auto JOINED_POINTES_SHARED = "joined_points_shared"; -constexpr auto OWNED_JOINED_POINTS = "owned_joined_points"; - -using namespace omm; - -std::map map_points(const PathVector& from, const PathVector& to) -{ - const auto from_paths = from.paths(); - const auto to_paths = to.paths(); - assert(from_paths.size() == to_paths.size()); - std::map map; - for (std::size_t i = 0; i < from_paths.size(); ++i) { - const auto& from_path = from_paths.at(i); - const auto& to_path = to_paths.at(i); - assert(from_path->size() == to_path->size()); - for (std::size_t j = 0; j < from_path->size(); ++j) { - map.insert({&from_path->at(j), &to_path->at(j)}); - } - } - return map; -} - -} // namespace +#include +#include namespace omm { @@ -51,166 +23,108 @@ class Style; PathVector::PathVector(PathObject* path_object) : m_path_object(path_object) - , m_owned_joined_points(std::make_unique()) -{ -} - -bool PathVector::joined_points_shared() const { - assert((m_shared_joined_points == nullptr) != (m_owned_joined_points.get() == nullptr)); - return m_owned_joined_points == nullptr; } PathVector::PathVector(const PathVector& other, PathObject* path_object) : m_path_object(path_object) - , m_owned_joined_points(std::make_unique(other.joined_points())) { - for (const auto* path : other.paths()) { - add_path(std::make_unique(*path, this)); - } - m_owned_joined_points->replace(map_points(other, *this)); + copy_from(other); } -PathVector::PathVector(PathVector&& other) noexcept -{ - swap(*this, other); -} - -PathVector& PathVector::operator=(const PathVector& other) -{ - *this = PathVector{other}; - return *this; -} +PathVector::~PathVector() = default; -std::unique_ptr PathVector::share_joined_points(DisjointPathPointSetForest& joined_points) +void PathVector::serialize(serialization::SerializerWorker& worker) const { - assert(!joined_points_shared()); - m_shared_joined_points = &joined_points; - for (const auto& set : m_owned_joined_points->sets()) { - m_shared_joined_points->insert(set); + using PointIndices = std::map; + PointIndices point_indices; + std::vector> iss; + iss.reserve(m_paths.size()); + std::map path_indices; + for (const auto& path : m_paths) { + std::list is; + for (const auto* const point : path->points()) { + const auto [it, was_inserted] = point_indices.try_emplace(point, point_indices.size()); + is.emplace_back(it->second); + } + iss.emplace_back(is.begin(), is.end()); + path_indices.emplace(path.get(), path_indices.size()); } - return std::move(m_owned_joined_points); -} -void PathVector::unshare_joined_points(std::unique_ptr joined_points) -{ - assert(joined_points_shared()); - m_shared_joined_points = nullptr; - m_owned_joined_points = std::move(joined_points); -} - -PathVector& PathVector::operator=(PathVector&& other) noexcept -{ - swap(*this, other); - return *this; -} - -void swap(PathVector& a, PathVector& b) noexcept -{ - swap(a.m_owned_joined_points, b.m_owned_joined_points); - std::swap(a.m_path_object, b.m_path_object); - swap(a.m_paths, b.m_paths); - for (auto& path : a.m_paths) { - path->set_path_vector(&a); - } - for (auto& path : b.m_paths) { - path->set_path_vector(&b); + std::vector point_indices_vec(point_indices.size()); + for (const auto& [point, index] : point_indices) { + point_indices_vec.at(index) = point; } - std::swap(a.m_shared_joined_points, b.m_shared_joined_points); -} - -PathVector::~PathVector() = default; -void PathVector::serialize(serialization::SerializerWorker& worker) const -{ - worker.sub(SEGMENTS_POINTER)->set_value(m_paths, [](const auto& path, auto& worker_i) { - if (path->size() == 0) { - LWARNING << "Ignoring empty sub-path."; - } else { - path->serialize(worker_i); - } + worker.sub("geometries")->set_value(point_indices_vec, [&path_indices](const PathPoint* const point, auto& worker) { + point->geometry().serialize(worker, path_indices); }); - const bool shared = joined_points_shared(); - worker.sub(JOINED_POINTES_SHARED)->set_value(shared); - if (!shared) { - worker.sub(OWNED_JOINED_POINTS)->set_value(*m_owned_joined_points); - } + + worker.sub("paths")->set_value(iss); } void PathVector::deserialize(serialization::DeserializerWorker& worker) { - m_paths.clear(); - worker.sub(SEGMENTS_POINTER)->get_items([this](auto& worker_i) { - Path& path = *m_paths.emplace_back(std::make_unique(this)); - path.deserialize(worker_i); - }); - const bool shared = worker.sub(JOINED_POINTES_SHARED)->get_bool(); - if (!shared) { - m_owned_joined_points = std::make_unique(); - worker.sub(OWNED_JOINED_POINTS)->get(*m_owned_joined_points); + std::vector> iss; + worker.sub("paths")->get(iss); + + std::map> point_indices_per_path; + std::vector paths; + paths.reserve(iss.size()); + for (const auto& path_point_indices : iss) { + auto& path = add_path(); + point_indices_per_path.emplace(&path, path_point_indices); + paths.emplace_back(&path); } -} -PathPoint& PathVector::point_at_index(std::size_t index) const -{ - for (Path* path : paths()) { - if (index < path->size()) { - return path->at(index); + std::deque> points; + worker.sub("geometries")->get_items([this, &paths, &points](auto& worker) { + Point geometry; + geometry.deserialize(worker, paths); + points.emplace_back(std::make_shared(geometry, this)); + }); + + for (const auto& [path, point_indices] : point_indices_per_path) { + if (point_indices.size() == 1) { + path->set_single_point(points.at(point_indices.front())); } else { - index -= path->size(); + for (std::size_t i = 1; i < point_indices.size(); ++i) { + const auto& a = points.at(point_indices.at(i - 1)); + const auto& b = points.at(point_indices.at(i)); + path->add_edge(std::make_unique(a, b, path)); + } } } - throw std::runtime_error{"Index out of bounds."}; } -QPainterPath PathVector::outline() const +QPainterPath PathVector::to_painter_path() const { QPainterPath outline; - for (const Path* path : paths()) { - const auto points = path->points(); - if (!points.empty()) { - outline.addPath(Path::to_painter_path(util::transform(points, [](const PathPoint* p) { - return p->geometry(); - }))); - } + for (const auto* path : paths()) { + outline.addPath(path->to_painter_path()); } return outline; } -std::vector PathVector::faces() const +std::set PathVector::faces() const { - Graph graph{*this}; - graph.remove_articulation_edges(); - const auto faces = graph.compute_faces(); - std::vector qpps; - qpps.reserve(faces.size()); - for (const auto& face : faces) { - qpps.emplace_back(Path::to_painter_path(face.points())); - } + return face_detector::compute_faces_without_outer(Graph(*this)); +} - for (bool path_changed = true; path_changed;) - { - path_changed = false; - for (auto& q1 : qpps) { - for (auto& q2 : qpps) { - if (&q1 == &q2) { - continue; - } - if (q1.contains(q2)) { - path_changed = true; - q1 -= q2; - } - } - } +std::vector PathVector::edges() const +{ + std::list edges; + for (const auto& path : m_paths) { + const auto pedges = path->edges(); + edges.insert(edges.end(), pedges.begin(), pedges.end()); } - - return qpps; + return std::vector(edges.begin(), edges.end()); } std::size_t PathVector::point_count() const { - return std::accumulate(cbegin(m_paths), cend(m_paths), 0, [](std::size_t n, auto&& path) { - return n + path->size(); + return std::accumulate(cbegin(m_paths), cend(m_paths), 0, [](std::size_t n, const auto& path) { + return n + path->points().size(); }); } @@ -219,6 +133,11 @@ std::deque PathVector::paths() const return util::transform(m_paths, std::mem_fn(&std::unique_ptr::get)); } +Path& PathVector::path(std::size_t i) const +{ + return *m_paths.at(i); +} + Path* PathVector::find_path(const PathPoint& point) const { for (auto&& path : m_paths) { @@ -229,12 +148,17 @@ Path* PathVector::find_path(const PathPoint& point) const return nullptr; } -Path& PathVector::add_path(std::unique_ptr&& path) +Path& PathVector::add_path(std::unique_ptr path) { path->set_path_vector(this); return *m_paths.emplace_back(std::move(path)); } +Path& PathVector::add_path() +{ + return add_path(std::make_unique(this)); +} + std::unique_ptr PathVector::remove_path(const Path &path) { const auto it = std::find_if(m_paths.begin(), m_paths.end(), [&path](const auto& s_ptr) { @@ -246,17 +170,27 @@ std::unique_ptr PathVector::remove_path(const Path &path) return extracted_path; } -std::deque PathVector::points() const +std::shared_ptr PathVector::share(const PathPoint& path_point) const +{ + for (const auto& path : m_paths) { + if (const auto& a = path->share(path_point); a != nullptr) { + return a; + } + } + return {}; +} + +std::set PathVector::points() const { - std::deque points; + std::set points; for (const auto& path : m_paths) { const auto& ps = path->points(); - points.insert(points.end(), ps.begin(), ps.end()); + points.insert(ps.begin(), ps.end()); } return points; } -std::deque PathVector::selected_points() const +std::set PathVector::selected_points() const { return util::remove_if(points(), [](const auto& p) { return !p->is_selected(); }); } @@ -268,68 +202,296 @@ void PathVector::deselect_all_points() const } } -void PathVector::update_joined_points_geometry() const +void PathVector::draw_point_ids(QPainter& painter) const { - std::set updated_path_vectors; - for (auto* point : points()) { - for (auto* buddy : point->joined_points()) { - if (buddy != point && buddy->path_vector() != this) { - updated_path_vectors.insert(buddy->path_vector()); - buddy->set_geometry(point->compute_joined_point_geometry(*buddy)); + for (const auto* point : points()) { + static constexpr QPointF offset{10.0, 10.0}; + painter.drawText(point->geometry().position().to_pointf() + offset, point->debug_id()); + } +} + +void PathVector::draw_path_ids(QPainter& painter) const +{ + std::size_t path_index = 0; + for (const auto* const path : paths()) { + std::size_t edge_index = 0; + for (const auto* edge : path->edges()) { + const auto geom_edge = omm_to_geom(*edge); + const auto label = QString("%1.%2").arg(path_index).arg(edge_index); + const auto pos = Vec2f(geom_edge.pointAt(0.5)).to_pointf(); + painter.drawText(pos, label); + edge_index += 1; + } + path_index += 1; + } +} + +PathObject* PathVector::path_object() const +{ + return m_path_object; +} + +std::unique_ptr PathVector::join(const std::deque& pvs, double eps) +{ + auto joined = std::make_unique(); + std::map point_mapping; + for (const auto& pv : pvs) { + auto pv_mapping = joined->copy_from(*pv).points; + point_mapping.merge(pv_mapping); + } + + for (const std::vector& correspondences : PathVectorIsomorphism(pvs).correspondences()) { + std::set close_points; + for (std::size_t i = 0; i < correspondences.size(); ++i) { + for (std::size_t j = 0; j < i; ++j) { + + } + } + } + + std::deque> close_points; + const auto eps2 = eps * eps; + for (std::size_t i1 = 0; i1 < pvs.size(); ++i1) { + for (std::size_t i2 = 0; i2 < i1; ++i2) { + for (const auto* const p1 : pvs.at(i1)->points()) { + for (const auto* const p2 : pvs.at(i2)->points()) { + if ((p1->geometry().position() - p2->geometry().position()).euclidean_norm2() < eps2) { + close_points.emplace_back(p1, p2); + } + } } } } - for (auto* path_vector : updated_path_vectors) { - path_vector->path_object()->update(); + + for (std::size_t i = 0; i < close_points.size(); ++i) { + const auto& [p1, p2] = close_points.at(i); + PathPoint*& j1 = point_mapping.at(p1); + PathPoint*& j2 = point_mapping.at(p2); + auto* joined_point = joined->join({j1, j2}); + j1 = joined_point; + j2 = joined_point; } + + return joined; } -void PathVector::join_points_by_position(const std::vector& positions) const +PathPoint* PathVector::join(std::set ps) { - static constexpr auto eps = 0.1; - static constexpr auto eps2 = eps * eps; - const auto points = this->points(); - LINFO << "==="; - for (const auto pos : positions) { - ::transparent_set joint; - for (auto* point : points) { - const auto d2 = (point->geometry().position() - pos).euclidean_norm2(); - LINFO << "d2: " << d2 << " " << point->geometry().position().to_string() << " " << pos.to_string(); - if (d2 < eps2) { - joint.insert(point); + if (ps.empty()) { + return {}; + } + + std::shared_ptr special; + const auto replace_maybe = [&ps, &special](std::shared_ptr& candidate) { + if (ps.contains(candidate.get())) { + if (!special) { + special = candidate; + } else { + for (const auto& [key, tangent] : candidate->geometry().tangents()) { + special->geometry().set_tangent(key, tangent); + } + candidate = special; } } - joined_points().insert(joint); + }; + for (auto& path : m_paths) { + for (auto* edge : path->edges()) { + replace_maybe(edge->a()); + replace_maybe(edge->b()); + } } + + return special.get(); } -bool PathVector::is_valid() const +PathVector::Mapping PathVector::copy_from(const PathVector& other) { - if ((m_shared_joined_points == nullptr) != (!m_owned_joined_points)) { - return false; + std::map paths_map; + for (const auto* other_path : other.paths()) { + auto& path = add_path(); + paths_map.try_emplace(other_path, &path); } - for (const auto& path : m_paths) { - for (auto* point : path->points()) { - if (&point->path() != path.get() || point->path_vector() != this) { - return false;; + + std::map> points_map; + for (const auto* const point : other.points()) { + auto geometry = point->geometry(); + geometry.replace_tangents_key(paths_map); + points_map.try_emplace(point, std::make_shared(geometry, this)); + } + + for (const auto* other_path : other.paths()) { + const auto other_edges = other_path->edges(); + auto& path = *paths_map.at(other_path); + if (other_edges.empty()) { + if (other_path->last_point()) { + path.set_single_point(points_map.at(other_path->last_point().get())); + } + } else { + for (const auto* other_edge : other_edges) { + const auto& a = points_map.at(other_edge->a().get()); + const auto& b = points_map.at(other_edge->b().get()); + path.add_edge(std::make_unique(a, b, &path)); } } } - return true; + + std::map points_map_raw; + for (const auto& [key, shared_ptr] : points_map) { + points_map_raw.try_emplace(key, shared_ptr.get()); + } + + return {points_map_raw, paths_map}; } -PathObject* PathVector::path_object() const +QString PathVector::to_dot() const { - return m_path_object; + QString s; + QTextStream ts(&s, QIODevice::WriteOnly); + ts << "graph {\n"; + for (const auto* const edge : edges()) { + ts << "\""; + ts << edge->a()->debug_id(); + ts << "\" -- \""; + ts << edge->b()->debug_id(); + ts << "\" [label=\"" << edge->to_string() << "\"];\n"; + } + ts << "}"; + return s; } -DisjointPathPointSetForest& PathVector::joined_points() const +void PathVector::to_svg(const QString& filename) const { - if (joined_points_shared()) { - return *m_shared_joined_points; - } else { - return *m_owned_joined_points; + const auto bb = bounding_box(); + QSvgGenerator svg; + svg.setFileName(filename); + const double size = std::max(bb.height(), bb.width()); + const QPointF margin(size / 3.0, size / 3.0); + svg.setViewBox(QRectF(bb.topLeft() - margin, bb.bottomRight() + margin)); + static constexpr auto width = 100; + svg.setSize({width, static_cast(width / bb.width() * bb.height())}); + QPainter painter(&svg); + + static constexpr auto colors = std::array{ + Qt::red, Qt::green, Qt::blue, Qt::cyan, Qt::magenta, + Qt::yellow, Qt::gray, Qt::darkRed, Qt::darkGreen, Qt::darkBlue, + Qt::darkCyan, Qt::darkMagenta, Qt::darkYellow, Qt::darkGray, Qt::lightGray, + }; + + std::map path_colors; + std::size_t path_index = 0; + auto pen = painter.pen(); + auto font = painter.font(); + font.setPointSizeF(size / 10.0); + painter.setFont(font); + pen.setCosmetic(true); + for (const auto* const path : paths()) { + const auto color = colors.at(path_index % colors.size()); + pen.setColor(color); + painter.setPen(pen); + painter.drawPath(path->to_painter_path()); + path_colors.emplace(path, color); + path_index += 1; } + + pen.setStyle(Qt::DotLine); + for (const auto* const p : points()) { + const auto p0 = p->geometry().position().to_pointf(); + for (const auto& [key, pc] : p->geometry().tangents()) { + pen.setColor(path_colors.at(key.path)); + painter.setPen(pen); + const auto pt = p->geometry().tangent_position(key).to_pointf(); + const auto start = key.direction == Direction::Forward ? pt : p0; + const auto end = key.direction == Direction::Forward ? p0 : pt; + QPainterPath path; + path.moveTo(start); + path.lineTo(end); + if (!path.isEmpty()) { + painter.drawPath(path); + } + } + pen.setColor(Qt::black); + pen.setWidthF(size / 100.0); + painter.setPen(pen); + painter.drawPoint(p0); + + pen.setWidthF(1.0); + painter.setPen(pen); + painter.drawText(p0, QString("%1").arg(p->debug_id())); + } +} + +QRectF PathVector::bounding_box() const +{ + static constexpr auto get_geometry = [](const auto* const pp) { return pp->geometry(); }; + return Point::bounding_box(util::transform(points(), get_geometry)); +} + +void PathVector::set_path_object(PathObject* path_object) +{ + m_path_object = path_object; +} + +QString PathVector::to_string() const +{ + return QString("PathVector[%1 with %2 Points in %3 Paths]") + .arg(QString::asprintf("%p", static_cast(this))) + .arg(point_count()) + .arg(paths().size()); +} + +std::ostream& operator<<(std::ostream& os, const PathVector& path_vector) +{ + // TODO maybe we can implement some global mechanism that turns `T::to_string` into + // `std::ostream& operator<<(std::ostream&, const T&)` for any T. + return os << path_vector.to_string().toStdString(); +} + +bool operator==(const PathVector& a, const PathVector& b) +{ + const auto paths_a = a.paths(); + const auto paths_b = b.paths(); + + if (paths_a.size() != paths_b.size()) { + return false; + } + + std::map path_map_a_to_b; + for (std::size_t i = 0; i < paths_a.size(); ++i) { + [[maybe_unused]] const auto [it, success] = path_map_a_to_b.try_emplace(paths_a.at(i), paths_b.at(i)); + assert(success); + } + + // check if paths of a match paths of b topologically + std::map map_a_to_b; + std::map map_b_to_a; + const auto point_eq = [&map_a_to_b, &map_b_to_a, &path_map_a_to_b](const PathPoint* const a, + const PathPoint* const b) { + if (map_a_to_b.try_emplace(a, b).first->second != b || map_b_to_a.try_emplace(b, a).first->second != a) { + return false; // `a` maps to something other than `b` already or vice versa. + } + auto a_geometry = a->geometry(); + a_geometry.replace_tangents_key(path_map_a_to_b); + if (a_geometry != b->geometry()) { + return false; + } + return true; + }; + + const auto path_eq = [&point_eq](const Path* path_a, const Path* path_b) { + const auto points_a_i = path_a->points(); + const auto points_b_i = path_b->points(); + if (points_a_i.size() != points_b_i.size()) { + return false; + } + return std::equal(points_a_i.begin(), points_a_i.end(), points_b_i.begin(), point_eq); + }; + + return std::equal(paths_a.begin(), paths_a.end(), paths_b.begin(), path_eq); +} + +bool operator!=(const PathVector& a, const PathVector& b) +{ + return !(a == b); } } // namespace omm diff --git a/src/path/pathvector.h b/src/path/pathvector.h index 01cee0fc2..1b9f0b659 100644 --- a/src/path/pathvector.h +++ b/src/path/pathvector.h @@ -1,10 +1,15 @@ #pragma once -#include "geometry/vec2.h" +#include #include +#include #include +#include +#include class QPainterPath; +class QPainter; +class QRectF; namespace omm { @@ -21,8 +26,9 @@ class EnhancedPathVector; class Path; class PathPoint; class PathObject; -class DisjointPathPointSetForest; class Scene; +class Face; +class Edge; // NOLINTNEXTLINE(bugprone-forward-declaration-namespace) class PathVector @@ -30,43 +36,54 @@ class PathVector public: PathVector(PathObject* path_object = nullptr); PathVector(const PathVector& other, PathObject* path_object = nullptr); - PathVector(PathVector&& other) noexcept; - PathVector& operator=(const PathVector& other); - PathVector& operator=(PathVector&& other) noexcept; - ~PathVector(); - friend void swap(PathVector& a, PathVector& b) noexcept; + PathVector& operator=(PathVector other) = delete; - /** - * @brief share_joined_points use the `joined_points` forest to register joined points. - * The currently owned joined points will be added to the shared joined points. - */ - std::unique_ptr share_joined_points(DisjointPathPointSetForest& joined_points); - void unshare_joined_points(std::unique_ptr joined_points); + // Moving the path vector is not trivially possible because the owned PathPoint hold a pointer + // to the owned Paths and this. + // If you need to move a PathVector, encapsulated it in a unique_ptr. + PathVector(PathVector&& other) = delete; + PathVector& operator=(PathVector&& other) = delete; - /** - * @brief join_points_shared returns true if the joined points are shared or false otherwise. - */ - [[nodiscard]] bool joined_points_shared() const; + ~PathVector(); - static constexpr auto SEGMENTS_POINTER = "segments"; void serialize(serialization::SerializerWorker& worker) const; void deserialize(serialization::DeserializerWorker& worker); [[nodiscard]] PathPoint& point_at_index(std::size_t index) const; - [[nodiscard]] QPainterPath outline() const; - [[nodiscard]] std::vector faces() const; + [[nodiscard]] QPainterPath to_painter_path() const; + [[nodiscard]] std::set faces() const; + [[nodiscard]] std::vector edges() const; [[nodiscard]] std::size_t point_count() const; [[nodiscard]] std::deque paths() const; + [[nodiscard]] Path& path(std::size_t i) const; [[nodiscard]] Path* find_path(const PathPoint& point) const; - Path& add_path(std::unique_ptr&& path); + Path& add_path(std::unique_ptr path); + Path& add_path(); std::unique_ptr remove_path(const Path& path); - [[nodiscard]] std::deque points() const; - [[nodiscard]] std::deque selected_points() const; + [[nodiscard]] std::shared_ptr share(const PathPoint& path_point) const; + [[nodiscard]] std::set points() const; + [[nodiscard]] std::set selected_points() const; void deselect_all_points() const; [[nodiscard]] PathObject* path_object() const; - [[nodiscard]] DisjointPathPointSetForest& joined_points() const; - void update_joined_points_geometry() const; - void join_points_by_position(const std::vector& positions) const; + void draw_point_ids(QPainter& painter) const; + void draw_path_ids(QPainter& painter) const; + [[nodiscard]] QString to_dot() const; + void to_svg(const QString& filename) const; + [[nodiscard]] QRectF bounding_box() const; + void set_path_object(PathObject* path_object); + + static std::unique_ptr join(const std::deque& pvs, double eps); + + /** + * @brief join joins the points @code ps + * All points of @code ps must be part of this @code PathVector. + * One point of @code ps is kept, a pointer to this one is returned. + * All other points of @code are merged into that one and their ownership is returned. + * It is unspecified which point is kept. + * If @code ps is empty, nothing happens. + */ + PathPoint* join(std::set ps); + /** * @brief is_valid returns true if this path vector is valid. @@ -76,11 +93,33 @@ class PathVector */ [[nodiscard, maybe_unused]] bool is_valid() const; + [[nodiscard]] QString to_string() const; + friend std::ostream& operator<<(std::ostream& os, const PathVector& path_vector); + + struct Mapping + { + std::map points; + std::map paths; + }; + + /** + * @brief copy_from copies all paths and points of @code pv by copying. + * There will be no reference to @code pv when this function is done. + * @return a mapping from @code PathPoint in pv to @code PathPoint in `this`. + */ + Mapping copy_from(const PathVector& pv); + private: PathObject* m_path_object = nullptr; - DisjointPathPointSetForest* m_shared_joined_points = nullptr; - std::unique_ptr m_owned_joined_points; std::deque> m_paths; }; +/** + * @brief operator == returns true if a and b are equal. + * It is not necessary for a and b to own identical Paths or PathPoints, equalty may hold if a was + * an independent copy of b. + */ +[[nodiscard]] bool operator==(const PathVector& a, const PathVector& b); +[[nodiscard]] bool operator!=(const PathVector& a, const PathVector& b); + } // namespace omm diff --git a/src/path/pathvectorisomorphism.cpp b/src/path/pathvectorisomorphism.cpp new file mode 100644 index 000000000..d3d83c109 --- /dev/null +++ b/src/path/pathvectorisomorphism.cpp @@ -0,0 +1,72 @@ +#include "path/pathvectorisomorphism.h" +#include "path/path.h" +#include "path/pathvector.h" +#include "transform.h" + + +namespace omm +{ + +PathVectorIsomorphism::PathVectorIsomorphism(const std::deque& path_vectors) +{ + m_points = util::transform(path_vectors, [](const auto* const pv) { + return util::transform(pv->paths(), [](const auto* const path) { return path->points(); }); + }); + + const auto set = util::transform(m_points, [](const auto& pv) { return pv.size(); }); + if (set.size() != 1) { + // Not all PathVectors have the same number of paths + return; + } + m_n_paths = *set.begin(); + + m_n_paths = m_points.front().size(); + for (std::size_t i = 0; i < m_n_paths; ++i) { + const auto set = util::transform(m_points, [i](const auto& pv) { return pv.at(i).size(); }); + if (set.size() != 1) { + // Not all ith paths of each PathVector have same number of points + return; + } + m_n_points.push_back(*set.begin()); + } + m_is_valid = true; +} + +const PathVectorIsomorphism::Points& PathVectorIsomorphism::points() const +{ + return m_points; +} + +std::size_t PathVectorIsomorphism::n_paths() const +{ + return m_n_paths; +} + +std::size_t PathVectorIsomorphism::n_points(std::size_t path_index) const +{ + return m_n_points.at(path_index); +} + +bool PathVectorIsomorphism::is_valid() const +{ + return m_is_valid; +} + +std::set> PathVectorIsomorphism::correspondences() const +{ + std::set> set; + const auto n_path_vectors = m_points.size(); + for (std::size_t i = 0; i < m_n_paths; ++i) { + for (std::size_t j = 0; j < m_n_points.at(i); ++j) { + std::vector ps; + ps.reserve(n_path_vectors); + for (std::size_t k = 0; k < n_path_vectors; ++k) { + ps.push_back(m_points.at(k).at(i).at(j)); + } + set.insert(ps); + } + } + return set; +} + +} // namespace omm diff --git a/src/path/pathvectorisomorphism.h b/src/path/pathvectorisomorphism.h new file mode 100644 index 000000000..38abb7ec1 --- /dev/null +++ b/src/path/pathvectorisomorphism.h @@ -0,0 +1,49 @@ +#pragma once + +#include +#include +#include + + +namespace omm +{ + +class PathVector; +class PathPoint; + + +/** + * @brief The PathVectorIsomorphism struct holds pointers to the points of a number of PathVectors + * if they are isomorph, i.e., have the same structure. + * That is, all PathVectors must have the same number of path and each `ith` path from any PathVector + * has the same number of points. + * The number of paths in each path vector is stored in `n_paths`. + * The number of points in each `ith` path from any PathVector is stored in `n_points[i]`. + */ +class PathVectorIsomorphism +{ +public: + explicit PathVectorIsomorphism(const std::deque& path_vectors); + using Points = std::deque>>; + const Points& points() const; + std::size_t n_paths() const; + std::size_t n_points(std::size_t path_index) const; + bool is_valid() const; + std::set> correspondences() const; + +private: + Points m_points; + + /** + * @brief n_paths number of paths in each path vector + */ + std::size_t m_n_paths; + + /** + * @brief n_points number of points in each ith path + */ + std::deque m_n_points; + bool m_is_valid = false; +}; + +} // namespace omm diff --git a/src/path/pathvectorview.cpp b/src/path/pathvectorview.cpp new file mode 100644 index 000000000..8d4e727f2 --- /dev/null +++ b/src/path/pathvectorview.cpp @@ -0,0 +1,178 @@ +#include "path/pathvectorview.h" +#include "path/dedge.h" +#include "path/edge.h" +#include "path/path.h" +#include "path/pathpoint.h" +#include + + +namespace +{ + +std::size_t count_distinct_points(const omm::Edge& first, const omm::Edge& second) +{ + const auto* const a1 = first.a().get(); + const auto* const b1 = first.b().get(); + const auto* const a2 = second.a().get(); + const auto* const b2 = second.b().get(); + + const auto n = std::set{a1, b1, a2, b2}.size(); + return n; +} + +} // namespace + +namespace omm +{ + +PathVectorView::PathVectorView(std::deque edges) : m_edges(std::move(edges)) +{ +} + +bool PathVectorView::is_simply_closed() const +{ + switch (m_edges.size()) { + case 0: + return false; + case 1: + return m_edges.front().edge->a() == m_edges.front().edge->b(); // edge loops from point to itself + case 2: + // Both edges have the same points. + // They can be part of different paths, hence any direction is possible. + return count_distinct_points(*m_edges.front().edge, *m_edges.back().edge) == 2; + default: + // Assuming there are no intersections, + // there must be only one common point between first and last edge to be closed. + return count_distinct_points(*m_edges.front().edge, *m_edges.back().edge) == 3; + } + + return !m_edges.empty() && m_edges.front().edge->a().get() == m_edges.back().edge->b().get(); +} + +const std::deque& PathVectorView::edges() const +{ + return m_edges; +} + +QPainterPath PathVectorView::to_painter_path() const +{ + if (m_edges.empty()) { + return {}; + } + QPainterPath p; + p.moveTo(m_edges.front().start_point().geometry().position().to_pointf()); + for (const auto& edge : m_edges) { + auto* const path = edge.edge->path(); + const auto g1 = edge.start_point().geometry(); + const auto g2 = edge.end_point().geometry(); + auto bwd = Direction::Backward; + auto fwd = Direction::Forward; + if (edge.direction == Direction::Forward) { + std::swap(bwd, fwd); + } + p.cubicTo(g1.tangent_position({path, bwd}).to_pointf(), g2.tangent_position({path, fwd}).to_pointf(), + g2.position().to_pointf()); + } + + return p; +} + +Geom::Path PathVectorView::to_geom() const +{ + Geom::Path path; + for (const auto& dedge : m_edges) { + path.append(dedge.to_geom_curve().release()); + } + return path; +} + +bool PathVectorView::contains(const Vec2f& pos) const +{ + return to_painter_path().contains(pos.to_pointf()); +} + +QString PathVectorView::to_string() const +{ + static constexpr auto max_edges = static_cast(10); + const auto edges = this->edges(); + QStringList es; + for (std::size_t i = 0; i < std::min(max_edges, edges.size()); ++i) { + es.append(edges.at(i).to_string()); + } + return QString("[%1] %2").arg(edges.size()).arg(es.join(", ")); +} + +std::vector PathVectorView::path_points() const +{ + if (m_edges.empty()) { + return {}; + } + + std::vector ps; + ps.reserve(m_edges.size() + 1); + ps.emplace_back(&m_edges.front().start_point()); + for (std::size_t i = 0; i < m_edges.size(); ++i) { + ps.emplace_back(&m_edges.at(i).end_point()); + } + return ps; +} + +QRectF PathVectorView::bounding_box() const +{ + static constexpr auto get_geometry = [](const auto* const pp) { return pp->geometry(); }; + return Point::bounding_box(util::transform(path_points(), get_geometry)); +} + +std::vector PathVectorView::bounding_polygon() const +{ + std::vector poly; + + // each edge can add at most three points since the end point of one edge is + // the start point of the next one and the sequence of edges is closed. + poly.reserve(m_edges.size() * 3); + + for (const auto& edge : m_edges) { + poly.emplace_back(edge.start_point().geometry().position()); + const auto add_tangent_maybe = [&poly, path = edge.edge->path()](const Point& p, const auto& direction) { + static constexpr auto eps = 0.001; + const Point::TangentKey key{path, direction}; + if (p.tangent(key).magnitude > eps) { + poly.emplace_back(p.tangent_position(key)); + } + }; + add_tangent_maybe(edge.start_point().geometry(), Direction::Forward); + add_tangent_maybe(edge.end_point().geometry(), Direction::Backward); + } + poly.shrink_to_fit(); + return poly; +} + +std::vector PathVectorView::normalized() const +{ + auto edges = util::transform(m_edges, [](const auto& dedge) { return dedge.edge; }); + if (is_simply_closed()) { + const auto min_it = std::min_element(edges.begin(), edges.end()); + std::rotate(edges.begin(), min_it, edges.end()); + if (edges.size() >= 3 && edges.at(1) > edges.back()) { + std::reverse(edges.begin(), edges.end()); + // Reversing has moved the smallest edge to the end, but it must be at front after + // normalization . + std::rotate(edges.begin(), std::prev(edges.end()), edges.end()); + } + } else if (edges.size() >= 2 && edges.at(0) > edges.at(1)) { + std::reverse(edges.begin(), edges.end()); + } + return edges; +} + +bool operator==(const PathVectorView& a, const PathVectorView& b) +{ + return a.normalized() == b.normalized(); +} + +bool operator<(const PathVectorView& a, const PathVectorView& b) +{ + return a.normalized() < b.normalized(); +} + +} // namespace omm diff --git a/src/path/pathvectorview.h b/src/path/pathvectorview.h new file mode 100644 index 000000000..0a8a5a84a --- /dev/null +++ b/src/path/pathvectorview.h @@ -0,0 +1,58 @@ +#pragma once + +#include "geometry/vec2.h" +#include "path/dedge.h" +#include <2geom/path.h> +#include +#include + +class QPainterPath; +class QRectF; + +namespace omm +{ + +class Edge; +class PathPoint; + +class PathVectorView +{ +public: + PathVectorView() = default; + explicit PathVectorView(std::deque edges); + + /** + * @brief is_simply_closed returns true if every two consecutive edge pairs (including last and + * first) have exactly one point in common. + * cases: + * - zero edges: return false + * - one edge: return true iff the edge is a loop, i.e., if both of its points are the same. + * - two edges: return true iff the two edges have two points in common. + */ + [[nodiscard]] bool is_simply_closed() const; + [[nodiscard]] const std::deque& edges() const; + [[nodiscard]] QPainterPath to_painter_path() const; + [[nodiscard]] Geom::Path to_geom() const; + [[nodiscard]] bool contains(const Vec2f& pos) const; + [[nodiscard]] QString to_string() const; + [[nodiscard]] std::vector path_points() const; + [[nodiscard]] QRectF bounding_box() const; + [[nodiscard]] std::vector bounding_polygon() const; + + /** + * @brief normalize PathVectorViews are defined up to + * - the direction (m_edges can be reveresed and it still describes the same view + * because the actual direction is given by the paths the edges belong to). + * - the first edge if it is closed (m_edges can be rotated without chaning the view) + * This function returns the edges of the PathVectorView in normalized order. + */ + std::vector normalized() const; + +private: + std::deque m_edges; +}; + +[[nodiscard]] bool operator==(const PathVectorView& a, const PathVectorView& b); +[[nodiscard]] bool operator<(const PathVectorView& a, const PathVectorView& b); + +} // namespace omm diff --git a/src/path/pathview.cpp b/src/path/pathview.cpp index 6722916f4..123d1f96f 100644 --- a/src/path/pathview.cpp +++ b/src/path/pathview.cpp @@ -6,23 +6,35 @@ namespace omm { -PathView::PathView(Path& path, std::size_t index, std::size_t size) - : path(&path), index(index), size(size) +PathView::PathView(Path& path, std::size_t begin, std::size_t point_count) + : m_path(&path), m_begin(begin), m_point_count(point_count) { } -std::deque PathView::points() const +Path& PathView::path() const { - auto points = path->points(); - points.erase(points.begin(), std::next(points.begin(), index)); - points.erase(std::next(points.begin(), size), points.end()); - return points; + return *m_path; +} + +std::size_t PathView::begin() const +{ + return m_begin; +} + +std::size_t PathView::end() const +{ + return m_begin + m_point_count; +} + +std::size_t PathView::point_count() const +{ + return m_point_count; } bool operator<(const PathView& a, const PathView& b) { static constexpr auto as_tuple = [](const PathView& a) { - return std::tuple{a.path, a.index}; + return std::tuple{&a.path(), a.begin(), a.point_count()}; }; // NOLINTNEXTLINE(modernize-use-nullptr) return as_tuple(a) < as_tuple(b); @@ -30,7 +42,7 @@ bool operator<(const PathView& a, const PathView& b) std::ostream& operator<<(std::ostream& ostream, const PathView& path_view) { - ostream << "Path[" << path_view.path << " " << path_view.index << " " << path_view.size << "]"; + ostream << "Path[" << &path_view.path() << " " << path_view.begin() << " " << path_view.point_count() << "]"; return ostream; } diff --git a/src/path/pathview.h b/src/path/pathview.h index 684e91f5f..25c371c6d 100644 --- a/src/path/pathview.h +++ b/src/path/pathview.h @@ -10,16 +10,22 @@ namespace omm class Path; class PathPoint; -struct PathView +class PathView { public: - explicit PathView(Path& path, std::size_t index, std::size_t size); + explicit PathView(Path& path, std::size_t begin, std::size_t point_count); friend bool operator<(const PathView& a, const PathView& b); friend std::ostream& operator<<(std::ostream& ostream, const PathView& path_view); [[nodiscard]] std::deque points() const; - Path* path; - std::size_t index; - std::size_t size; + [[nodiscard]] Path& path() const; + [[nodiscard]] std::size_t begin() const; + [[nodiscard]] std::size_t end() const; + [[nodiscard]] std::size_t point_count() const ; + +private: + Path* m_path; + std::size_t m_begin; + std::size_t m_point_count; }; } // namepsace diff --git a/src/properties/CMakeLists.txt b/src/properties/CMakeLists.txt index 1a432508d..00917ad9d 100644 --- a/src/properties/CMakeLists.txt +++ b/src/properties/CMakeLists.txt @@ -3,6 +3,8 @@ target_sources(libommpfritt PRIVATE boolproperty.h colorproperty.cpp colorproperty.h + facelistproperty.cpp + facelistproperty.h floatproperty.cpp floatproperty.h integerproperty.cpp diff --git a/src/properties/facelistproperty.cpp b/src/properties/facelistproperty.cpp new file mode 100644 index 000000000..c0b4f4ecd --- /dev/null +++ b/src/properties/facelistproperty.cpp @@ -0,0 +1,20 @@ +#include "properties/facelistproperty.h" + +namespace omm +{ + +const Property::PropertyDetail FaceListProperty::detail{nullptr}; + +void FaceListProperty::deserialize(serialization::DeserializerWorker &worker) +{ + TypedProperty::deserialize(worker); + set(worker.sub(TypedPropertyDetail::VALUE_POINTER)->get()); +} + +void FaceListProperty::serialize(serialization::SerializerWorker &worker) const +{ + TypedProperty::serialize(worker); + worker.sub(TypedPropertyDetail::VALUE_POINTER)->set_value(value()); +} + +} // namespace omm diff --git a/src/properties/facelistproperty.h b/src/properties/facelistproperty.h new file mode 100644 index 000000000..c556a08a2 --- /dev/null +++ b/src/properties/facelistproperty.h @@ -0,0 +1,19 @@ +#pragma once + +#include "properties/typedproperty.h" +#include "facelist.h" + +namespace omm +{ + +class FaceListProperty : public TypedProperty +{ +public: + using TypedProperty::TypedProperty; + void deserialize(serialization::DeserializerWorker& worker) override; + void serialize(serialization::SerializerWorker& worker) const override; + + static const PropertyDetail detail; +}; + +} // namespace omm diff --git a/src/properties/facesproperty.cpp b/src/properties/facesproperty.cpp new file mode 100644 index 000000000..e20d1fe49 --- /dev/null +++ b/src/properties/facesproperty.cpp @@ -0,0 +1,21 @@ +//#include "properties/facesproperty.h" + + +//namespace omm +//{ + +//const Property::PropertyDetail FacesProperty::detail{nullptr}; + +//void FacesProperty::deserialize(serialization::DeserializerWorker& worker) +//{ +// TypedProperty::deserialize(worker); +// set(worker.sub(TypedPropertyDetail::VALUE_POINTER)->get()); +//} + +//void FacesProperty::serialize(serialization::SerializerWorker& worker) const +//{ +// TypedProperty::serialize(worker); +// worker.sub(TypedPropertyDetail::VALUE_POINTER)->set_value(value()); +//} + +//} // namespace omm diff --git a/src/properties/facesproperty.h b/src/properties/facesproperty.h new file mode 100644 index 000000000..6968abac3 --- /dev/null +++ b/src/properties/facesproperty.h @@ -0,0 +1,19 @@ +//#pragma once + +//#include "path/face.h" +//#include "properties/typedproperty.h" + + +//namespace omm +//{ + +//class FacesProperty : public TypedProperty +//{ +//public: +// using TypedProperty::TypedProperty; +// void deserialize(serialization::DeserializerWorker& worker) override; +// void serialize(serialization::SerializerWorker& worker) const override; +// static const PropertyDetail detail; +//}; + +//} // namespace omm diff --git a/src/properties/propertygroups/markerproperties.cpp b/src/properties/propertygroups/markerproperties.cpp index 8d1fb12c2..93afce912 100644 --- a/src/properties/propertygroups/markerproperties.cpp +++ b/src/properties/propertygroups/markerproperties.cpp @@ -58,8 +58,7 @@ void MarkerProperties ::draw_marker(Painter& painter, } p.setPen(Qt::NoPen); p.setBrush(color.to_qcolor()); - QPainterPath path = Path::to_painter_path(shape(width), true); - p.drawPath(path); + p.drawPath(shape(width)); p.restore(); } @@ -72,7 +71,7 @@ std::deque MarkerProperties::shapes() QObject::tr("Diamond")}; } -std::vector MarkerProperties::shape(const double width) const +QPainterPath MarkerProperties::shape(const double width) const { const double base = width * property_value(SIZE_PROPERTY_KEY); const auto shape = property_value(SHAPE_PROPERTY_KEY); @@ -94,42 +93,42 @@ std::vector MarkerProperties::shape(const double width) const Q_UNREACHABLE(); } -std::vector MarkerProperties::arrow(const Vec2f& size) +QPainterPath MarkerProperties::arrow(const Vec2f& size) { - return { - Point(Vec2f(size.x, 0.0)), - Point(Vec2f(0.0, size.y)), - Point(Vec2f(0.0, -size.y)), - Point(Vec2f(size.x, 0.0)), - }; + QPainterPath p; + p.moveTo(size.x, 0.0); + p.lineTo(0.0, size.y); + p.lineTo(0.0, -size.y); + p.lineTo(size.x, 0.0); + return p; } -std::vector MarkerProperties::bar(const Vec2f& size) +QPainterPath MarkerProperties::bar(const Vec2f& size) { - return { - Point(Vec2f(-size.x, size.y)), - Point(Vec2f(-size.x, -size.y)), - Point(Vec2f(size.x, -size.y)), - Point(Vec2f(size.x, size.y)), - Point(Vec2f(-size.x, size.y)), - }; + QPainterPath p; + p.moveTo(-size.x, size.y); + p.lineTo(-size.x, -size.y); + p.lineTo(size.x, -size.y); + p.lineTo(size.x, size.y); + p.lineTo(-size.x, size.y); + return p; } -std::vector MarkerProperties::circle(const Vec2f& size) +QPainterPath MarkerProperties::circle(const Vec2f& size) { Q_UNUSED(size); return {}; } -std::vector MarkerProperties::diamond(const Vec2f& size) +QPainterPath MarkerProperties::diamond(const Vec2f& size) { - return { - Point(Vec2f(-size.x, 0.0)), - Point(Vec2f(0.0, -size.y)), - Point(Vec2f(size.x, 0.0)), - Point(Vec2f(0.0, size.y)), - Point(Vec2f(-size.x, 0.0)), - }; + QPainterPath p; + p.moveTo(-size.x, 0.0); + p.lineTo(0.0, -size.y); + p.lineTo(size.x, 0.0); + p.lineTo(0.0, size.y); + p.lineTo(-size.x, 0.0); + return p; } } // namespace omm diff --git a/src/properties/propertygroups/markerproperties.h b/src/properties/propertygroups/markerproperties.h index 6cadec151..b5d89269d 100644 --- a/src/properties/propertygroups/markerproperties.h +++ b/src/properties/propertygroups/markerproperties.h @@ -27,11 +27,11 @@ class MarkerProperties : public PropertyGroup static constexpr auto REVERSE_PROPERTY_KEY = "reverse"; static std::deque shapes(); - std::vector shape(double width) const; - static std::vector arrow(const Vec2f& size); - static std::vector bar(const Vec2f& size); - static std::vector circle(const Vec2f& size); - static std::vector diamond(const Vec2f& size); + QPainterPath shape(double width) const; + static QPainterPath arrow(const Vec2f& size); + static QPainterPath bar(const Vec2f& size); + static QPainterPath circle(const Vec2f& size); + static QPainterPath diamond(const Vec2f& size); private: const Shape m_default_shape; diff --git a/src/properties/propertygroups/pathproperties.cpp b/src/properties/propertygroups/pathproperties.cpp index 2995f858f..645fd3007 100644 --- a/src/properties/propertygroups/pathproperties.cpp +++ b/src/properties/propertygroups/pathproperties.cpp @@ -1,4 +1,5 @@ #include "properties/propertygroups/pathproperties.h" +#include "geometry/orientedposition.h" #include "objects/object.h" #include "properties/boolproperty.h" #include "properties/floatproperty.h" diff --git a/src/properties/splineproperty.cpp b/src/properties/splineproperty.cpp index a3ea47364..d4a8242d8 100644 --- a/src/properties/splineproperty.cpp +++ b/src/properties/splineproperty.cpp @@ -2,12 +2,8 @@ namespace omm { -const Property::PropertyDetail SplineProperty::detail{nullptr}; -SplineProperty::SplineProperty(const omm::SplineType& default_value) - : TypedProperty(default_value) -{ -} +const Property::PropertyDetail SplineProperty::detail{nullptr}; void SplineProperty::deserialize(serialization::DeserializerWorker& worker) { diff --git a/src/properties/splineproperty.h b/src/properties/splineproperty.h index 45eccbf69..0165781c5 100644 --- a/src/properties/splineproperty.h +++ b/src/properties/splineproperty.h @@ -2,14 +2,14 @@ #include "properties/typedproperty.h" #include "splinetype.h" -#include namespace omm { + class SplineProperty : public TypedProperty { public: - explicit SplineProperty(const SplineType& default_value = SplineType()); + using TypedProperty::TypedProperty; void deserialize(serialization::DeserializerWorker& worker) override; void serialize(serialization::SerializerWorker& worker) const override; static constexpr auto MODE_PROPERTY_KEY = "mode"; diff --git a/src/properties/typedproperty.h b/src/properties/typedproperty.h index 14e382202..0e9710362 100644 --- a/src/properties/typedproperty.h +++ b/src/properties/typedproperty.h @@ -68,10 +68,12 @@ template class TypedProperty : public Property { return m_default_value; } + virtual void set_default_value(const ValueT& value) { m_default_value = value; } + virtual void reset() { m_value = m_default_value; diff --git a/src/propertytypeenum.h b/src/propertytypeenum.h index a883e1c48..5f7191d63 100644 --- a/src/propertytypeenum.h +++ b/src/propertytypeenum.h @@ -12,10 +12,11 @@ class AbstractPropertyOwner; class Color; class TriggerPropertyDummyValueType; class SplineType; +class FaceList; enum class Type{ Invalid, Float, Integer, Option, FloatVector, - IntegerVector, String, Color, Reference, Bool, Spline, Trigger + IntegerVector, String, Color, Reference, Bool, Spline, Trigger, Faces, }; constexpr bool is_integral(const Type type) @@ -46,7 +47,7 @@ constexpr bool is_color(const Type type) constexpr auto variant_types = std::array{ Type::Bool, Type::Float, Type::Color, Type::Integer, Type::IntegerVector, Type::FloatVector, Type::Reference, Type::String, Type::Option, Type::Trigger, - Type::FloatVector, Type::IntegerVector, Type::Spline, Type::Invalid + Type::FloatVector, Type::IntegerVector, Type::Spline, Type::Faces, Type::Invalid }; constexpr std::string_view variant_type_name(const Type type) noexcept @@ -74,6 +75,8 @@ constexpr std::string_view variant_type_name(const Type type) noexcept return QT_TRANSLATE_NOOP("DataType", "IntegerVector"); case Type::Spline: return QT_TRANSLATE_NOOP("DataType", "Spline"); + case Type::Faces: + return QT_TRANSLATE_NOOP("DataType", "FaceList"); case Type:: Invalid: return QT_TRANSLATE_NOOP("DataType", "Invalid"); } @@ -107,6 +110,8 @@ template constexpr Type get_variant_type() noexcept return Type::IntegerVector; } else if constexpr (std::is_same_v) { return Type::Spline; + } else if constexpr (std::is_same_v) { + return Type::Faces; } else { return Type::Invalid; } diff --git a/src/propertywidgets/CMakeLists.txt b/src/propertywidgets/CMakeLists.txt index b206d7253..5fe4bf85f 100644 --- a/src/propertywidgets/CMakeLists.txt +++ b/src/propertywidgets/CMakeLists.txt @@ -10,6 +10,7 @@ target_sources(libommpfritt PRIVATE add_subdirectory(boolpropertywidget) add_subdirectory(colorpropertywidget) add_subdirectory(numericpropertywidget) +add_subdirectory(facelistpropertywidget) add_subdirectory(floatpropertywidget) add_subdirectory(floatvectorpropertywidget) add_subdirectory(integerpropertywidget) diff --git a/src/propertywidgets/colorpropertywidget/colorpropertyconfigwidget.h b/src/propertywidgets/colorpropertywidget/colorpropertyconfigwidget.h index 32545e35e..2560290de 100644 --- a/src/propertywidgets/colorpropertywidget/colorpropertyconfigwidget.h +++ b/src/propertywidgets/colorpropertywidget/colorpropertyconfigwidget.h @@ -1,22 +1,14 @@ #pragma once #include "propertywidgets/propertyconfigwidget.h" +#include "properties/colorproperty.h" namespace omm { -class ColorProperty; - class ColorPropertyConfigWidget : public PropertyConfigWidget { -public: - using PropertyConfigWidget::PropertyConfigWidget; - void init(const PropertyConfiguration&) override - { - } - void update(PropertyConfiguration&) const override - { - } + Q_OBJECT }; } // namespace omm diff --git a/src/propertywidgets/facelistpropertywidget/CMakeLists.txt b/src/propertywidgets/facelistpropertywidget/CMakeLists.txt new file mode 100644 index 000000000..298ac15b4 --- /dev/null +++ b/src/propertywidgets/facelistpropertywidget/CMakeLists.txt @@ -0,0 +1,7 @@ +target_sources(libommpfritt PRIVATE + facelistwidget.cpp + facelistwidget.h + facelistpropertyconfigwidget.h + facelistpropertywidget.cpp + facelistpropertywidget.h +) diff --git a/src/propertywidgets/facelistpropertywidget/facelistpropertyconfigwidget.h b/src/propertywidgets/facelistpropertywidget/facelistpropertyconfigwidget.h new file mode 100644 index 000000000..320acfdd1 --- /dev/null +++ b/src/propertywidgets/facelistpropertywidget/facelistpropertyconfigwidget.h @@ -0,0 +1,16 @@ +#pragma once + +#include "properties/facelistproperty.h" +#include "propertywidgets/propertyconfigwidget.h" + +class QListWidget; + +namespace omm +{ + +class FaceListPropertyConfigWidget : public PropertyConfigWidget +{ + Q_OBJECT +}; + +} // namespace omm diff --git a/src/propertywidgets/facelistpropertywidget/facelistpropertywidget.cpp b/src/propertywidgets/facelistpropertywidget/facelistpropertywidget.cpp new file mode 100644 index 000000000..7542f0707 --- /dev/null +++ b/src/propertywidgets/facelistpropertywidget/facelistpropertywidget.cpp @@ -0,0 +1,25 @@ +#include "propertywidgets/facelistpropertywidget/facelistpropertywidget.h" +#include "properties/typedproperty.h" +#include "propertywidgets/facelistpropertywidget/facelistwidget.h" + +#include +#include + +namespace omm +{ +FaceListPropertyWidget::FaceListPropertyWidget(Scene& scene, const std::set& properties) + : PropertyWidget(scene, properties) +{ + auto face_list_widget = std::make_unique(); + m_face_list_widget = face_list_widget.get(); + set_widget(std::move(face_list_widget)); + FaceListPropertyWidget::update_edit(); +} + +void FaceListPropertyWidget::update_edit() +{ + QSignalBlocker blocker(m_face_list_widget); + m_face_list_widget->set_values(get_properties_values()); +} + +} // namespace omm diff --git a/src/propertywidgets/facelistpropertywidget/facelistpropertywidget.h b/src/propertywidgets/facelistpropertywidget/facelistpropertywidget.h new file mode 100644 index 000000000..7f4a34bfa --- /dev/null +++ b/src/propertywidgets/facelistpropertywidget/facelistpropertywidget.h @@ -0,0 +1,23 @@ +#pragma once + +#include "properties/facelistproperty.h" +#include "propertywidgets/propertywidget.h" + +namespace omm +{ + +class FaceListWidget; + +class FaceListPropertyWidget : public PropertyWidget +{ +public: + explicit FaceListPropertyWidget(Scene& scene, const std::set& properties); + +protected: + void update_edit() override; + +private: + FaceListWidget* m_face_list_widget; +}; + +} // namespace omm diff --git a/src/propertywidgets/facelistpropertywidget/facelistwidget.cpp b/src/propertywidgets/facelistpropertywidget/facelistwidget.cpp new file mode 100644 index 000000000..e57157f33 --- /dev/null +++ b/src/propertywidgets/facelistpropertywidget/facelistwidget.cpp @@ -0,0 +1,52 @@ +#include "propertywidgets/facelistpropertywidget/facelistwidget.h" + +#include "objects/pathobject.h" +#include "path/pathvector.h" +#include "removeif.h" +#include "path/face.h" +#include "path/edge.h" + +#include +#include +#include + + +namespace omm +{ + +void FaceListWidget::set_value(const value_type& value) +{ + m_lw->clear(); + for (const auto& face : value.faces()) { + m_lw->addItem(face.to_string()); + } +} + +void FaceListWidget::set_inconsistent_value() +{ + set_value(FaceList{}); +} + +FaceListWidget::value_type FaceListWidget::value() const +{ + return FaceList{}; +} + +FaceListWidget::FaceListWidget(QWidget* parent) + : QWidget(parent) +{ + setFocusPolicy(Qt::StrongFocus); + auto layout = std::make_unique(); + + auto insert = [layout=layout.get()](auto&& widget) -> auto& { + auto& ref = *widget; + layout->addWidget(widget.release()); + return ref; + }; + + m_lw = &insert(std::make_unique()); + m_pb_clear = &insert(std::make_unique(tr("Clear"))); + setLayout(layout.release()); +} + +} // namespace omm diff --git a/src/propertywidgets/facelistpropertywidget/facelistwidget.h b/src/propertywidgets/facelistpropertywidget/facelistwidget.h new file mode 100644 index 000000000..64bfd47c5 --- /dev/null +++ b/src/propertywidgets/facelistpropertywidget/facelistwidget.h @@ -0,0 +1,31 @@ +#pragma once + +#include "propertywidgets/multivalueedit.h" +#include "facelist.h" +#include + +class QCheckBox; +class QListWidget; +class QPushButton; + +namespace omm +{ + +class FaceListWidget : public QWidget, public MultiValueEdit +{ + Q_OBJECT +public: + explicit FaceListWidget(QWidget* parent = nullptr); + void set_value(const value_type& value) override; + [[nodiscard]] value_type value() const override; + +protected: + void set_inconsistent_value() override; + +private: + QCheckBox* m_cb_invert; + QListWidget* m_lw; + QPushButton* m_pb_clear; +}; + +} // namespace omm diff --git a/src/propertywidgets/floatpropertywidget/floatpropertyconfigwidget.h b/src/propertywidgets/floatpropertywidget/floatpropertyconfigwidget.h index 321d0bc1b..88256dd99 100644 --- a/src/propertywidgets/floatpropertywidget/floatpropertyconfigwidget.h +++ b/src/propertywidgets/floatpropertywidget/floatpropertyconfigwidget.h @@ -9,8 +9,7 @@ class FloatProperty; class FloatPropertyConfigWidget : public NumericPropertyConfigWidget { -public: - using NumericPropertyConfigWidget::NumericPropertyConfigWidget; + Q_OBJECT }; } // namespace omm diff --git a/src/propertywidgets/floatvectorpropertywidget/floatvectorpropertyconfigwidget.h b/src/propertywidgets/floatvectorpropertywidget/floatvectorpropertyconfigwidget.h index 34f25cc96..be2d42597 100644 --- a/src/propertywidgets/floatvectorpropertywidget/floatvectorpropertyconfigwidget.h +++ b/src/propertywidgets/floatvectorpropertywidget/floatvectorpropertyconfigwidget.h @@ -5,11 +5,10 @@ namespace omm { + class FloatVectorPropertyConfigWidget : public VectorPropertyConfigWidget { Q_OBJECT -public: - using VectorPropertyConfigWidget::VectorPropertyConfigWidget; }; } // namespace omm diff --git a/src/propertywidgets/integerpropertywidget/integerpropertyconfigwidget.h b/src/propertywidgets/integerpropertywidget/integerpropertyconfigwidget.h index 1a7387796..fc176aa6b 100644 --- a/src/propertywidgets/integerpropertywidget/integerpropertyconfigwidget.h +++ b/src/propertywidgets/integerpropertywidget/integerpropertyconfigwidget.h @@ -9,8 +9,7 @@ class IntegerProperty; class IntegerPropertyConfigWidget : public NumericPropertyConfigWidget { -public: - using NumericPropertyConfigWidget::NumericPropertyConfigWidget; + Q_OBJECT }; } // namespace omm diff --git a/src/propertywidgets/integerpropertywidget/integerpropertywidget.cpp b/src/propertywidgets/integerpropertywidget/integerpropertywidget.cpp index 338c693ec..d06bff3e9 100644 --- a/src/propertywidgets/integerpropertywidget/integerpropertywidget.cpp +++ b/src/propertywidgets/integerpropertywidget/integerpropertywidget.cpp @@ -4,7 +4,7 @@ namespace omm { IntegerPropertyWidget::IntegerPropertyWidget(Scene& scene, const std::set& properties) - : NumericPropertyWidget(scene, properties) + : NumericPropertyWidget(scene, properties) { const auto get_special_value = [](const IntegerProperty& ip) { return ip.special_value_label; }; const auto special_value_label diff --git a/src/propertywidgets/integervectorpropertywidget/integervectorpropertyconfigwidget.h b/src/propertywidgets/integervectorpropertywidget/integervectorpropertyconfigwidget.h index 846380eae..ddfde3de3 100644 --- a/src/propertywidgets/integervectorpropertywidget/integervectorpropertyconfigwidget.h +++ b/src/propertywidgets/integervectorpropertywidget/integervectorpropertyconfigwidget.h @@ -8,8 +8,6 @@ namespace omm class IntegerVectorPropertyConfigWidget : public VectorPropertyConfigWidget { Q_OBJECT -public: - using VectorPropertyConfigWidget::VectorPropertyConfigWidget; }; } // namespace omm diff --git a/src/propertywidgets/integervectorpropertywidget/integervectorpropertywidget.h b/src/propertywidgets/integervectorpropertywidget/integervectorpropertywidget.h index c1be267c9..2beac57e0 100644 --- a/src/propertywidgets/integervectorpropertywidget/integervectorpropertywidget.h +++ b/src/propertywidgets/integervectorpropertywidget/integervectorpropertywidget.h @@ -5,6 +5,7 @@ namespace omm { + class IntegerVectorPropertyWidget : public VectorPropertyWidget { public: diff --git a/src/propertywidgets/numericpropertywidget/numericpropertywidget.cpp b/src/propertywidgets/numericpropertywidget/numericpropertywidget.cpp index ed23e226e..b4edbfd55 100644 --- a/src/propertywidgets/numericpropertywidget/numericpropertywidget.cpp +++ b/src/propertywidgets/numericpropertywidget/numericpropertywidget.cpp @@ -52,6 +52,13 @@ void NumericPropertyWidget::update_configuration() NumericPropertyWidget::update_edit(); } +template +NumericMultiValueEdit::value_type>* +NumericPropertyWidget::spinbox() const +{ + return m_spinbox; +} + template void NumericPropertyWidget::update_edit() { QSignalBlocker blocker(m_spinbox); diff --git a/src/propertywidgets/numericpropertywidget/numericpropertywidget.h b/src/propertywidgets/numericpropertywidget/numericpropertywidget.h index 47ed535e7..eb830e3ae 100644 --- a/src/propertywidgets/numericpropertywidget/numericpropertywidget.h +++ b/src/propertywidgets/numericpropertywidget/numericpropertywidget.h @@ -8,8 +8,9 @@ namespace omm { -template -class NumericPropertyWidget : public PropertyWidget + +template class NumericPropertyWidget + : public PropertyWidget { public: using value_type = typename NumericPropertyT::value_type; @@ -18,10 +19,7 @@ class NumericPropertyWidget : public PropertyWidget protected: void update_edit() override; void update_configuration() override; - auto spinbox() const - { - return m_spinbox; - } + NumericMultiValueEdit* spinbox() const; private: NumericMultiValueEdit* m_spinbox; diff --git a/src/propertywidgets/optionpropertywidget/optionpropertywidget.cpp b/src/propertywidgets/optionpropertywidget/optionpropertywidget.cpp index 98609bb02..cf9c3ec39 100644 --- a/src/propertywidgets/optionpropertywidget/optionpropertywidget.cpp +++ b/src/propertywidgets/optionpropertywidget/optionpropertywidget.cpp @@ -25,6 +25,11 @@ OptionPropertyWidget::OptionPropertyWidget(Scene& scene, const std::set { public: explicit OptionPropertyWidget(Scene& scene, const std::set& properties); - [[nodiscard]] OptionsEdit* combobox() const - { - return m_options_edit; - } + [[nodiscard]] OptionsEdit* combobox() const; protected: void update_edit() override; diff --git a/src/propertywidgets/propertyconfigwidget.cpp b/src/propertywidgets/propertyconfigwidget.cpp index c531afec8..058b19de7 100644 --- a/src/propertywidgets/propertyconfigwidget.cpp +++ b/src/propertywidgets/propertyconfigwidget.cpp @@ -9,6 +9,15 @@ namespace omm { + +void AbstractPropertyConfigWidget::init(const PropertyConfiguration&) +{ +} + +void AbstractPropertyConfigWidget::update(PropertyConfiguration&) const +{ +} + void AbstractPropertyConfigWidget::hideEvent(QHideEvent* event) { QWidget::hideEvent(event); diff --git a/src/propertywidgets/propertyconfigwidget.h b/src/propertywidgets/propertyconfigwidget.h index 4f9df9bb2..a07f37b4b 100644 --- a/src/propertywidgets/propertyconfigwidget.h +++ b/src/propertywidgets/propertyconfigwidget.h @@ -19,8 +19,8 @@ class AbstractPropertyConfigWidget public: explicit AbstractPropertyConfigWidget() = default; - virtual void init(const PropertyConfiguration& configuration) = 0; - virtual void update(PropertyConfiguration& configuration) const = 0; + virtual void init(const PropertyConfiguration&); + virtual void update(PropertyConfiguration&) const; protected: void hideEvent(QHideEvent* event) override; @@ -36,6 +36,7 @@ template class PropertyConfigWidget : public AbstractPropert { return QString(PropertyT::TYPE()) + "ConfigWidget"; } + [[nodiscard]] QString type() const override { return TYPE(); diff --git a/src/propertywidgets/splinepropertywidget/splinepropertywidget.cpp b/src/propertywidgets/splinepropertywidget/splinepropertywidget.cpp index 6b49597c8..dc35ff94d 100644 --- a/src/propertywidgets/splinepropertywidget/splinepropertywidget.cpp +++ b/src/propertywidgets/splinepropertywidget/splinepropertywidget.cpp @@ -4,7 +4,7 @@ namespace omm { SplinePropertyWidget::SplinePropertyWidget(Scene& scene, const std::set& properties) - : PropertyWidget(scene, properties) + : PropertyWidget(scene, properties) { auto spline_edit = std::make_unique(); m_spline_edit = spline_edit.get(); diff --git a/src/propertywidgets/triggerpropertywidget/triggerpropertyconfigwidget.h b/src/propertywidgets/triggerpropertywidget/triggerpropertyconfigwidget.h index bfd7bc2e9..4fdc88e7e 100644 --- a/src/propertywidgets/triggerpropertywidget/triggerpropertyconfigwidget.h +++ b/src/propertywidgets/triggerpropertywidget/triggerpropertyconfigwidget.h @@ -1,22 +1,14 @@ #pragma once #include "propertywidgets/propertyconfigwidget.h" +#include "properties/triggerproperty.h" namespace omm { -class TriggerProperty; - class TriggerPropertyConfigWidget : public PropertyConfigWidget { -public: - using PropertyConfigWidget::PropertyConfigWidget; - void init(const PropertyConfiguration&) override - { - } - void update(PropertyConfiguration&) const override - { - } + Q_OBJECT }; } // namespace omm diff --git a/src/python/pathwrapper.cpp b/src/python/pathwrapper.cpp index b391021bd..49e0bfaca 100644 --- a/src/python/pathwrapper.cpp +++ b/src/python/pathwrapper.cpp @@ -16,11 +16,8 @@ void PathWrapper::define_python_interface(py::object& module) py::object PathWrapper::points() { auto& path_object = dynamic_cast(wrapped); - std::vector point_wrappers; - point_wrappers.reserve(path_object.geometry().point_count()); - for (PathPoint* point : path_object.geometry().points()) { - point_wrappers.emplace_back(*point); - } + static constexpr auto wrap = [](auto* path_point) { return PathPointWrapper{*path_point}; }; + const auto point_wrappers = util::transform(path_object.path_vector().points(), wrap); return py::cast(point_wrappers); } diff --git a/src/python/pointwrapper.cpp b/src/python/pointwrapper.cpp index 4a4447a1b..4d222102c 100644 --- a/src/python/pointwrapper.cpp +++ b/src/python/pointwrapper.cpp @@ -15,37 +15,38 @@ void PointWrapper::define_python_interface(py::object& module) py::class_(module, Point::TYPE) .def(py::init<>()) .def("position", &PointWrapper::position) - .def("left_tangent", &PointWrapper::left_tangent) - .def("right_tangent", &PointWrapper::right_tangent) +// .def("left_tangent", &PointWrapper::left_tangent) +// .def("right_tangent", &PointWrapper::right_tangent) .def("set_position", &PointWrapper::set_position) - .def("set_left_tangent", &PointWrapper::set_left_tangent) - .def("set_right_tangent", &PointWrapper::set_right_tangent); +// .def("set_left_tangent", &PointWrapper::set_left_tangent) +// .def("set_right_tangent", &PointWrapper::set_right_tangent) + ; } -py::object PointWrapper::left_tangent() const -{ - return py::cast(m_point.left_tangent().to_cartesian().to_stdvec()); -} +//py::object PointWrapper::left_tangent() const +//{ +// return py::cast(m_point.left_tangent().to_cartesian().to_stdvec()); +//} -py::object PointWrapper::right_tangent() const -{ - return py::cast(m_point.right_tangent().to_cartesian().to_stdvec()); -} +//py::object PointWrapper::right_tangent() const +//{ +// return py::cast(m_point.right_tangent().to_cartesian().to_stdvec()); +//} py::object PointWrapper::position() const { return py::cast(m_point.position().to_stdvec()); } -void PointWrapper::set_left_tangent(const py::object& value) -{ - m_point.set_left_tangent(PolarCoordinates(Vec2f(value.cast>()))); -} +//void PointWrapper::set_left_tangent(const py::object& value) +//{ +// m_point.set_left_tangent(PolarCoordinates(Vec2f(value.cast>()))); +//} -void PointWrapper::set_right_tangent(const py::object& value) -{ - m_point.set_right_tangent(PolarCoordinates(Vec2f(value.cast>()))); -} +//void PointWrapper::set_right_tangent(const py::object& value) +//{ +// m_point.set_right_tangent(PolarCoordinates(Vec2f(value.cast>()))); +//} void PointWrapper::set_position(const py::object& value) { diff --git a/src/python/pointwrapper.h b/src/python/pointwrapper.h index 1bd0b2181..8af3a7d50 100644 --- a/src/python/pointwrapper.h +++ b/src/python/pointwrapper.h @@ -11,11 +11,11 @@ class PointWrapper : public AbstractPyWrapper explicit PointWrapper(const Point& point); explicit PointWrapper() = default; static void define_python_interface(py::object& module); - [[nodiscard]] py::object left_tangent() const; - [[nodiscard]] py::object right_tangent() const; +// [[nodiscard]] py::object left_tangent() const; +// [[nodiscard]] py::object right_tangent() const; [[nodiscard]] py::object position() const; - void set_left_tangent(const py::object& value); - void set_right_tangent(const py::object& value); +// void set_left_tangent(const py::object& value); +// void set_right_tangent(const py::object& value); void set_position(const py::object& value); [[nodiscard]] const Point& point() const; diff --git a/src/removeif.h b/src/removeif.h index 7b14be43b..92448e27f 100644 --- a/src/removeif.h +++ b/src/removeif.h @@ -1,7 +1,8 @@ #pragma once -#include +#include #include +#include template class QList; diff --git a/src/renderers/offscreenrenderer.cpp b/src/renderers/offscreenrenderer.cpp index f0b97dee5..9b48bd20e 100644 --- a/src/renderers/offscreenrenderer.cpp +++ b/src/renderers/offscreenrenderer.cpp @@ -189,6 +189,8 @@ void set_uniform(omm::OffscreenRenderer& self, const QString& name, const T& val } else if constexpr (std::is_same_v) { const auto mat = value.to_qmatrix3x3(); program->setUniformValue(location, mat); + } else if constexpr (std::is_same_v) { + // faces is not available in GLSL } else { // statically fail here. If you're data type is not supported, add it explicitely. static_assert(std::is_same_v && !std::is_same_v); diff --git a/src/scene/CMakeLists.txt b/src/scene/CMakeLists.txt index b916815a9..7a2365350 100644 --- a/src/scene/CMakeLists.txt +++ b/src/scene/CMakeLists.txt @@ -3,8 +3,8 @@ target_sources(libommpfritt PRIVATE contextes_fwd.h cycleguard.cpp cycleguard.h - disjointpathpointsetforest.cpp - disjointpathpointsetforest.h + faceselection.cpp + faceselection.h itemmodeladapter.cpp itemmodeladapter.h list.cpp diff --git a/src/scene/disjointpathpointsetforest.cpp b/src/scene/disjointpathpointsetforest.cpp deleted file mode 100644 index e91d4ff56..000000000 --- a/src/scene/disjointpathpointsetforest.cpp +++ /dev/null @@ -1,126 +0,0 @@ -#include "scene/disjointpathpointsetforest.h" -#include "serializers/abstractdeserializer.h" -#include "serializers/deserializerworker.h" -#include "serializers/serializerworker.h" -#include "path/pathpoint.h" -#include "path/pathvector.h" -#include "path/path.h" -#include "objects/pathobject.h" -#include "scene/scene.h" - -static constexpr auto FOREST_POINTER = "forest"; -static constexpr auto PATH_ID_POINTER = "path-id"; -static constexpr auto INDEX_POINTER = "index"; - -namespace -{ - -struct PathPointId -{ - constexpr explicit PathPointId(const std::size_t path_id, const std::size_t point_index) - : path_id(path_id) - , point_index(point_index) - { - } - std::size_t path_id; - std::size_t point_index; -}; - -} // namespace - -namespace omm -{ - -class DisjointPathPointSetForest::ReferencePolisher : public omm::serialization::ReferencePolisher -{ -public: - explicit ReferencePolisher(const std::deque>& joined_point_indices, - DisjointPathPointSetForest& ref) - : m_ref(ref) - , m_joined_point_indices(joined_point_indices) - { - } - -private: - omm::DisjointPathPointSetForest& m_ref; - std::deque> m_joined_point_indices; - - void update_references(const std::map& map) override - { - m_ref.m_forest.clear(); - for (const auto& set : m_joined_point_indices) { - auto& forest_set = m_ref.m_forest.emplace_back(); - for (const auto& [path_id, point_index] : set) { - auto* path_object = dynamic_cast(map.at(path_id)); - auto& path_point = path_object->geometry().point_at_index(point_index); - forest_set.insert(&path_point); - } - } - } -}; - -void DisjointPathPointSetForest::deserialize(serialization::DeserializerWorker& worker) -{ - std::deque> joined_point_indices; - worker.sub(FOREST_POINTER)->get_items([&joined_point_indices](auto& worker_i) { - std::list point_set; - worker_i.get_items([&point_set](auto& worker_ii) { - const auto point_index = worker_ii.sub(INDEX_POINTER)->get_size_t(); - const auto path_id = worker_ii.sub(PATH_ID_POINTER)->get_size_t(); - point_set.emplace_back(path_id, point_index); - }); - joined_point_indices.push_back(point_set); - }); - worker.deserializer().register_reference_polisher(std::make_unique(joined_point_indices, *this)); -} - -void DisjointPathPointSetForest::serialize(serialization::SerializerWorker& worker) const -{ - auto copy = *this; - copy.remove_dangling_points(); - copy.serialize_impl(worker); -} - -void DisjointPathPointSetForest::remove_dangling_points() -{ - remove_if(std::mem_fn(&PathPoint::is_dangling)); -} - -void DisjointPathPointSetForest::remove_if(const std::function& predicate) -{ - for (auto& set : m_forest) { - std::erase_if(set, predicate); - } - remove_empty_sets(); -} - -void DisjointPathPointSetForest::replace(const std::map& dict) -{ - for (auto& old_set : m_forest) { - Joint new_set; - for (auto* old_point : old_set) { - if (const auto it = dict.find(old_point); it != dict.end()) { - new_set.insert(it->second); - } - } - old_set = new_set; - } - remove_empty_sets(); -} - -void DisjointPathPointSetForest::serialize(serialization::SerializerWorker& worker, const Joint& joint) -{ - worker.set_value(joint, [](const auto* p, auto& worker_i) { - worker_i.sub(INDEX_POINTER)->set_value(p->index()); - worker_i.sub(PATH_ID_POINTER)->set_value(p->path_vector()->path_object()->id()); - }); -} - -void DisjointPathPointSetForest::serialize_impl(serialization::SerializerWorker& worker) const -{ - worker.sub(FOREST_POINTER)->set_value(m_forest, [](const auto& joint, auto& worker_i) { - serialize(worker_i, joint); - }); -} - -} // namespace omm diff --git a/src/scene/disjointpathpointsetforest.h b/src/scene/disjointpathpointsetforest.h deleted file mode 100644 index 8bd4aa4b3..000000000 --- a/src/scene/disjointpathpointsetforest.h +++ /dev/null @@ -1,32 +0,0 @@ -#pragma once - -#include "disjointset.h" - -namespace omm -{ - -class PathPoint; - -namespace serialization -{ -class SerializerWorker; -class DeserializerWorker; -} // namespace serialization - -class DisjointPathPointSetForest : public DisjointSetForest -{ -public: - using DisjointSetForest::DisjointSetForest; - void deserialize(serialization::DeserializerWorker& worker); - void serialize(serialization::SerializerWorker& worker) const; - void remove_dangling_points(); - void remove_if(const std::function& predicate); - void replace(const std::map& dict); - -private: - class ReferencePolisher; - void serialize_impl(serialization::SerializerWorker& worker) const; - static void serialize(serialization::SerializerWorker& worker, const Joint& joint); -}; - -} // namespace omm diff --git a/src/scene/faceselection.cpp b/src/scene/faceselection.cpp new file mode 100644 index 000000000..345b67192 --- /dev/null +++ b/src/scene/faceselection.cpp @@ -0,0 +1,41 @@ +#include "scene/faceselection.h" +#include "path/face.h" + +namespace omm +{ + +FaceSelection::FaceSelection(Scene&) +{ + +} + +void FaceSelection::set_selected(const Face& face, bool is_selected) +{ + if (is_selected) { + select(face); + } else { + deselect(face); + } +} + +void FaceSelection::select(const Face& face) +{ + m_selection.insert(face); +} + +void FaceSelection::deselect(const Face& face) +{ + m_selection.erase(face); +} + +bool FaceSelection::is_selected(const Face& face) +{ + return m_selection.contains(face); +} + +void FaceSelection::clear() +{ + m_selection.clear(); +} + +} // namespace omm diff --git a/src/scene/faceselection.h b/src/scene/faceselection.h new file mode 100644 index 000000000..49119f4a0 --- /dev/null +++ b/src/scene/faceselection.h @@ -0,0 +1,26 @@ +#pragma once + +#include "common.h" +#include + +namespace omm +{ + +class Face; +class Scene; + +class FaceSelection +{ +public: + FaceSelection(Scene& scene); + void set_selected(const Face& face, bool is_selected); + void select(const Face& face); + void deselect(const Face& face); + [[nodiscard]] bool is_selected(const Face& face); + void clear(); + +private: + ::transparent_set m_selection; +}; + +} // namespace omm diff --git a/src/scene/mailbox.h b/src/scene/mailbox.h index 697ba8418..e82236675 100644 --- a/src/scene/mailbox.h +++ b/src/scene/mailbox.h @@ -157,6 +157,8 @@ class MailBox : public QObject */ void point_selection_changed(); + void face_selection_changed(); + /** * @brief scene_reseted is emitted when the scene was reset. */ diff --git a/src/scene/pointselection.cpp b/src/scene/pointselection.cpp index 397d4f5a1..e51f071af 100644 --- a/src/scene/pointselection.cpp +++ b/src/scene/pointselection.cpp @@ -15,7 +15,7 @@ ::transparent_set PointSelection::points() const { ::transparent_set selected_points; for (auto* path_object : type_casts(m_scene.item_selection())) { - for (auto* point : path_object->geometry().selected_points()) { + for (auto* point : path_object->path_vector().selected_points()) { selected_points.insert(point); } } diff --git a/src/scene/scene.cpp b/src/scene/scene.cpp index 64a7b9274..69bc6f46d 100644 --- a/src/scene/scene.cpp +++ b/src/scene/scene.cpp @@ -33,13 +33,13 @@ #include "main/application.h" #include "nodesystem/node.h" #include "nodesystem/nodemodel.h" -#include "scene/disjointpathpointsetforest.h" #include "scene/history/historymodel.h" #include "scene/history/macro.h" #include "scene/mailbox.h" #include "scene/objecttree.h" #include "scene/stylelist.h" #include "scene/pointselection.h" +#include "scene/faceselection.h" #include "tools/toolbox.h" namespace @@ -50,7 +50,6 @@ constexpr auto STYLES_POINTER = "styles"; constexpr auto ANIMATOR_POINTER = "animation"; constexpr auto NAMED_COLORS_POINTER = "colors"; constexpr auto EXPORT_OPTIONS_POINTER = "export_options"; -constexpr auto JOINED_POINTS_POINTER = "joined_points"; template std::set @@ -113,6 +112,7 @@ namespace omm { Scene::Scene() : point_selection(std::make_unique(*this)) + , face_selection(std::make_unique(*this)) , m_mail_box(new MailBox()) , m_object_tree(new ObjectTree(make_root(), *this)) , m_styles(new StyleList(*this)) @@ -121,7 +121,6 @@ Scene::Scene() , m_animator(new Animator(*this)) , m_named_colors(new NamedColors()) , m_export_options(new ExportOptions()) - , m_joined_points(new DisjointPathPointSetForest()) { object_tree().root().set_object_tree(object_tree()); for (auto kind : {Object::KIND, Tag::KIND, Style::KIND, Tool::KIND, nodes::Node::KIND}) { @@ -225,11 +224,6 @@ void Scene::set_export_options(const ExportOptions& export_options) } } -DisjointPathPointSetForest& Scene::joined_points() const -{ - return *m_joined_points; -} - bool Scene::save_as(const QString& filename) { return SceneSerialization{*this}.save(filename); @@ -251,7 +245,6 @@ void Scene::serialize(serialization::SerializerWorker& serializer) m_animator->serialize(*serializer.sub(ANIMATOR_POINTER)); m_named_colors->serialize(*serializer.sub(NAMED_COLORS_POINTER)); export_options().serialize(*serializer.sub(EXPORT_OPTIONS_POINTER)); - m_joined_points->serialize(*serializer.sub(JOINED_POINTS_POINTER)); } void Scene::deserialize(serialization::DeserializerWorker& deserializer) @@ -277,7 +270,6 @@ void Scene::deserialize(serialization::DeserializerWorker& deserializer) animator().deserialize(*deserializer.sub(ANIMATOR_POINTER)); named_colors().deserialize(*deserializer.sub(NAMED_COLORS_POINTER)); m_export_options->deserialize(*deserializer.sub(EXPORT_OPTIONS_POINTER)); - m_joined_points->deserialize(*deserializer.sub(JOINED_POINTS_POINTER)); } void Scene::reset() @@ -532,19 +524,17 @@ void Scene::set_mode(SceneMode mode) bool Scene::contains(const AbstractPropertyOwner* apo) const { - switch (apo->kind) { - case Kind::Tag: { - const auto tags = this->tags(); - // the std::set::find does not allow keys of type `const Tag*`. - // NOLINTNEXTLINE(performance-inefficient-algorithm) - return tags.end() != std::find(tags.begin(), tags.end(), dynamic_cast(apo)); - } - case Kind::Node: { - const auto nodes = this->collect_nodes(); - // the std::set::find does not allow keys of type `const Tag*`. - // NOLINTNEXTLINE(performance-inefficient-algorithm) - return nodes.end() != std::find(nodes.begin(), nodes.end(), dynamic_cast(apo)); + if (apo == nullptr) { + return false; } + static constexpr auto contains = [](const auto& container, const auto& key) { + return end(container) != std::find(begin(container), end(container), key); + }; + switch (apo->kind) { + case Kind::Tag: + return contains(this->tags(), dynamic_cast(apo)); + case Kind::Node: + return contains(this->collect_nodes(), dynamic_cast(apo)); case Kind::Object: return object_tree().contains(dynamic_cast(*apo)); case Kind::Style: diff --git a/src/scene/scene.h b/src/scene/scene.h index cdbb4a1e0..0611a9122 100644 --- a/src/scene/scene.h +++ b/src/scene/scene.h @@ -20,6 +20,7 @@ namespace omm class Animator; class ColorProperty; class Command; +class FaceSelection; class HistoryModel; class MailBox; class NamedColors; @@ -30,7 +31,6 @@ class PythonEngine; class StyleList; class ToolBox; struct ExportOptions; -class DisjointPathPointSetForest; namespace nodes { @@ -125,6 +125,7 @@ class Scene : public QObject bool remove(QWidget* parent, const std::set& selection); bool contains(const AbstractPropertyOwner* apo) const; std::unique_ptr point_selection; + std::unique_ptr face_selection; private: std::map> m_item_selection; @@ -247,12 +248,6 @@ class Scene : public QObject private: std::unique_ptr m_export_options; - - // === Joined Points === -private: - std::unique_ptr m_joined_points; -public: - [[nodiscard]] DisjointPathPointSetForest& joined_points() const; }; } // namespace omm diff --git a/src/scene/sceneserializer.cpp b/src/scene/sceneserializer.cpp index effc21fc4..5ea844fa4 100644 --- a/src/scene/sceneserializer.cpp +++ b/src/scene/sceneserializer.cpp @@ -8,7 +8,6 @@ #include "scene/mailbox.h" #include "scene/scene.h" #include "scene/stylelist.h" -#include "scene/disjointpathpointsetforest.h" #include #include @@ -126,18 +125,18 @@ bool SceneSerialization::load_bin(const QString& filename) const bool SceneSerialization::save_json(const QString& filename) const { - std::ofstream ofstream(filename.toStdString()); - if (!ofstream) { - LERROR << "Failed to open ofstream at '" << filename << "'."; - return false; - } - nlohmann::json json; serialization::JSONSerializer serializer{json}; if (!save(serializer)) { return false; } + std::ofstream ofstream(filename.toStdString()); + if (!ofstream) { + LERROR << "Failed to open ofstream at '" << filename << "'."; + return false; + } + ofstream << json.dump(4) << "\n"; return true; } diff --git a/src/serializers/deserializerworker.cpp b/src/serializers/deserializerworker.cpp index 56c20fd27..b187ea265 100644 --- a/src/serializers/deserializerworker.cpp +++ b/src/serializers/deserializerworker.cpp @@ -87,6 +87,8 @@ variant_type DeserializerWorker::get(const QString& type) return get(); } else if (type == "IntegerVector") { return get(); + } else if (type == "FacesList") { + return get(); } else if (type == "SplineType") { return get(); } else if (type == "Reference") { diff --git a/src/tags/styletag.cpp b/src/tags/styletag.cpp index f65ba890b..d9f897ec2 100644 --- a/src/tags/styletag.cpp +++ b/src/tags/styletag.cpp @@ -5,6 +5,7 @@ #include "objects/object.h" #include "properties/referenceproperty.h" #include "properties/triggerproperty.h" +#include "properties/facelistproperty.h" #include "renderers/style.h" #include "scene/mailbox.h" #include "scene/scene.h" @@ -22,15 +23,21 @@ StyleTag::StyleTag(Object& owner) : Tag(owner) create_property(EDIT_STYLE_PROPERTY_KEY) .set_label(QObject::tr("Edit style ...")) .set_category(category); + + create_property(FACE_LIST_PROPERTY_KEY) + .set_label(QObject::tr("Faces")) + .set_category(category); } QString StyleTag::type() const { return TYPE; } + void StyleTag::evaluate() { } + Flag StyleTag::flags() const { return Tag::flags(); diff --git a/src/tags/styletag.h b/src/tags/styletag.h index 6eb71f3e4..d158bbd5d 100644 --- a/src/tags/styletag.h +++ b/src/tags/styletag.h @@ -14,6 +14,7 @@ class StyleTag : public Tag static constexpr auto STYLE_REFERENCE_PROPERTY_KEY = "style"; static constexpr auto TYPE = QT_TRANSLATE_NOOP("any-context", "StyleTag"); static constexpr auto EDIT_STYLE_PROPERTY_KEY = "edit-style"; + static constexpr auto FACE_LIST_PROPERTY_KEY = "facelist"; void evaluate() override; Flag flags() const override; diff --git a/src/tools/CMakeLists.txt b/src/tools/CMakeLists.txt index 6b1c17cdd..dcb4afcbd 100644 --- a/src/tools/CMakeLists.txt +++ b/src/tools/CMakeLists.txt @@ -5,6 +5,8 @@ target_sources(libommpfritt PRIVATE knifetool.h pathtool.cpp pathtool.h + selectfacestool.cpp + selectfacestool.h selectobjectstool.cpp selectobjectstool.h selectpointsbasetool.cpp diff --git a/src/tools/brushselecttool.cpp b/src/tools/brushselecttool.cpp index 3cc0d3f17..17b9e3bf6 100644 --- a/src/tools/brushselecttool.cpp +++ b/src/tools/brushselecttool.cpp @@ -46,7 +46,7 @@ bool BrushSelectTool::mouse_press(const Vec2f& pos, const QMouseEvent& event) } for (Object* object : scene()->item_selection()) { if (auto* path_object = type_cast(object); path_object != nullptr) { - for (auto* point : path_object->geometry().points()) { + for (auto* point : path_object->path_vector().points()) { point->set_selected(false); } } @@ -75,7 +75,7 @@ void BrushSelectTool ::modify_selection(const Vec2f& pos, const QMouseEvent& eve for (Object* object : scene()->item_selection()) { auto* path_object = type_cast(object); if (path_object != nullptr) { - for (auto* point : path_object->geometry().points()) { + for (auto* point : path_object->path_vector().points()) { // we can't transform `pos` with path's inverse transformation because if it scales, // `radius` will be wrong. const auto gt = path_object->global_transformation(Space::Viewport); diff --git a/src/tools/handles/CMakeLists.txt b/src/tools/handles/CMakeLists.txt index dfc328346..a73b3a42c 100644 --- a/src/tools/handles/CMakeLists.txt +++ b/src/tools/handles/CMakeLists.txt @@ -2,6 +2,8 @@ target_sources(libommpfritt PRIVATE abstractselecthandle.cpp abstractselecthandle.h boundingboxhandle.h + facehandle.cpp + facehandle.h handle.cpp handle.h moveaxishandle.h diff --git a/src/tools/handles/facehandle.cpp b/src/tools/handles/facehandle.cpp new file mode 100644 index 000000000..90cd426c0 --- /dev/null +++ b/src/tools/handles/facehandle.cpp @@ -0,0 +1,54 @@ +#include "tools/handles/facehandle.h" +#include +#include "objects/pathobject.h" +#include "path/pathvectorview.h" +#include "scene/faceselection.h" +#include "scene/scene.h" + +namespace omm +{ + +FaceHandle::FaceHandle(Tool& tool, PathObject& path_object, const Face& face) + : AbstractSelectHandle(tool) + , m_path_object(path_object) + , m_face(face) +{ +} + +bool FaceHandle::contains_global(const Vec2f& point) const +{ + const auto p = transformation().inverted().apply_to_position(point); + return m_face.path_vector_view().contains(p); +} + +void FaceHandle::draw(QPainter& painter) const +{ + painter.save(); + painter.setTransform(transformation().to_qtransform()); + const auto status = is_selected() ? HandleStatus::Active : this->status(); + painter.setBrush(ui_color(status, "face")); + painter.drawPath(m_path); + painter.restore(); +} + +ObjectTransformation FaceHandle::transformation() const +{ + return m_path_object.global_transformation(Space::Viewport); +} + +bool FaceHandle::is_selected() const +{ + return tool.scene()->face_selection->is_selected(m_face); +} + +void FaceHandle::set_selected(bool selected) +{ + tool.scene()->face_selection->set_selected(m_face, selected); +} + +void FaceHandle::clear() +{ + return tool.scene()->face_selection->clear(); +} + +} // namespace omm diff --git a/src/tools/handles/facehandle.h b/src/tools/handles/facehandle.h new file mode 100644 index 000000000..99288fd59 --- /dev/null +++ b/src/tools/handles/facehandle.h @@ -0,0 +1,33 @@ +#pragma once + +#include "geometry/vec2.h" +#include "tools/handles/handle.h" +#include "tools/handles/abstractselecthandle.h" +#include "tools/tool.h" +#include "path/face.h" +#include + +namespace omm +{ +class FaceHandle : public AbstractSelectHandle +{ +public: + explicit FaceHandle(Tool& tool, PathObject& path_object, const Face& face); + [[nodiscard]] bool contains_global(const Vec2f& point) const override; + void draw(QPainter& painter) const override; + Vec2f position = Vec2f::o(); + ObjectTransformation transformation() const; + +protected: + bool transform_in_tool_space{}; + bool is_selected() const override; + void set_selected(bool selected) override; + void clear() override; + +private: + PathObject& m_path_object; + const Face m_face; + const QPainterPath m_path; +}; + +} // namespace omm diff --git a/src/tools/handles/particlehandle.h b/src/tools/handles/particlehandle.h index c0c5d036e..2b75f2999 100644 --- a/src/tools/handles/particlehandle.h +++ b/src/tools/handles/particlehandle.h @@ -13,9 +13,6 @@ class ParticleHandle : public Handle [[nodiscard]] bool contains_global(const Vec2f& point) const override; void draw(QPainter& painter) const override; Vec2f position = Vec2f::o(); - -protected: - bool transform_in_tool_space{}; }; template class MoveParticleHandle : public ParticleHandle diff --git a/src/tools/handles/pointselecthandle.cpp b/src/tools/handles/pointselecthandle.cpp index ea65cf603..861830c88 100644 --- a/src/tools/handles/pointselecthandle.cpp +++ b/src/tools/handles/pointselecthandle.cpp @@ -13,6 +13,21 @@ #include #include + +namespace +{ + +auto make_tangent_handles_map(omm::Tool& tool, omm::PointSelectHandle& psh, const auto& tangent_keys) +{ + std::map> map; + for (const auto& key : tangent_keys) { + map.try_emplace(key, std::make_unique(tool, psh, key)); + } + return map; +} + +} // namespace + namespace omm { @@ -20,9 +35,12 @@ PointSelectHandle::PointSelectHandle(Tool& tool, PathObject& path_object, PathPo : AbstractSelectHandle(tool) , m_path_object(path_object) , m_point(point) - , m_left_tangent_handle(std::make_unique(tool, *this, TangentHandle::Tangent::Left)) - , m_right_tangent_handle(std::make_unique(tool, *this, TangentHandle::Tangent::Right)) + , m_tangent_handles(make_tangent_handles_map(tool, *this, ::get_keys(point.geometry().tangents()))) { + assert(m_point.path_vector()); + assert(&path_object == m_point.path_vector()->path_object()); + assert(path_object.scene() == tool.scene()); + assert(path_object.scene()->contains(&path_object)); } ObjectTransformation PointSelectHandle::transformation() const @@ -43,28 +61,24 @@ bool PointSelectHandle::mouse_press(const Vec2f& pos, const QMouseEvent& event) return true; } - const auto [left_tangent_active, right_tangent_active] = tangents_active(); - if (left_tangent_active && m_left_tangent_handle->mouse_press(pos, event)) { - return true; - } - if (right_tangent_active && m_right_tangent_handle->mouse_press(pos, event)) { - return true; + for (auto& [key, handle] : m_tangent_handles) { + if (is_active(key) && handle->mouse_press(pos, event)) { + return true; + } } return false; } -bool PointSelectHandle ::mouse_move(const Vec2f& delta, const Vec2f& pos, const QMouseEvent& event) +bool PointSelectHandle::mouse_move(const Vec2f& delta, const Vec2f& pos, const QMouseEvent& event) { if (AbstractSelectHandle::mouse_move(delta, pos, event)) { return true; } - const auto [left_tangent_active, right_tangent_active] = tangents_active(); - if (left_tangent_active && m_left_tangent_handle->mouse_move(delta, pos, event)) { - return true; - } - if (right_tangent_active && m_right_tangent_handle->mouse_move(delta, pos, event)) { - return true; + for (auto& [key, handle] : m_tangent_handles) { + if (is_active(key) && handle->mouse_move(delta, pos, event)) { + return true; + } } return false; @@ -73,8 +87,9 @@ bool PointSelectHandle ::mouse_move(const Vec2f& delta, const Vec2f& pos, const void PointSelectHandle::mouse_release(const Vec2f& pos, const QMouseEvent& event) { AbstractSelectHandle::mouse_release(pos, event); - m_left_tangent_handle->mouse_release(pos, event); - m_right_tangent_handle->mouse_release(pos, event); + for (auto& [key, handle] : m_tangent_handles) { + handle->mouse_release(pos, event); + } } PathPoint& PointSelectHandle::point() const @@ -85,23 +100,14 @@ PathPoint& PointSelectHandle::point() const void PointSelectHandle::draw(QPainter& painter) const { const auto pos = transformation().apply_to_position(m_point.geometry().position()); - - const auto treat_sub_handle = [&painter, pos, this](auto& sub_handle, const auto& other_pos) { - sub_handle.position = other_pos; - - painter.setPen(ui_color("tangent")); - painter.drawLine(pos.x, pos.y, other_pos.x, other_pos.y); - sub_handle.draw(painter); - }; - - const auto [left_tangent_active, right_tangent_active] = tangents_active(); - if (left_tangent_active) { - const auto left_pos = transformation().apply_to_position(m_point.geometry().left_position()); - treat_sub_handle(*m_left_tangent_handle, left_pos); - } - if (right_tangent_active) { - const auto right_pos = transformation().apply_to_position(m_point.geometry().right_position()); - treat_sub_handle(*m_right_tangent_handle, right_pos); + for (const auto& [key, tangent] : m_point.geometry().tangents()) { + if (is_active(key)) { + const auto other_pos = transformation().apply_to_position(m_point.geometry().tangent_position(key)); + m_tangent_handles.at(key)->position = other_pos; + painter.setPen(ui_color("tangent")); + painter.drawLine(pos.x, pos.y, other_pos.x, other_pos.y); + m_tangent_handles.at(key)->draw(painter); + } } painter.translate(pos.to_pointf()); @@ -110,74 +116,37 @@ void PointSelectHandle::draw(QPainter& painter) const const auto rect = Tool::centered_rectangle({}, r); painter.setPen(ui_color(status, "point")); painter.setBrush(ui_color(status, "point fill")); - if (m_point.joined_points().size() > 1) { - painter.drawRect(rect); - } else { - painter.drawEllipse(rect); - } + painter.drawEllipse(rect); } -void PointSelectHandle::transform_tangent(const Vec2f& delta, TangentHandle::Tangent tangent) +void PointSelectHandle::transform_tangent(const Vec2f& delta, const Point::TangentKey& tangent_key) { - transform_tangent(delta, dynamic_cast(tool).tangent_mode(), tangent); -} - -auto get_primary_secondary_tangent(const Point& point, const TangentHandle::Tangent tangent) -{ - switch (tangent) { - case TangentHandle::Tangent::Right: - return std::pair{point.right_tangent(), point.left_tangent()}; - case TangentHandle::Tangent::Left: - return std::pair{point.left_tangent(), point.right_tangent()}; - } - Q_UNREACHABLE(); -} - -void set_primary_secondary_tangent(Point& point, - const PolarCoordinates& primary, - const PolarCoordinates& secondary, - const TangentHandle::Tangent tangent) -{ - switch (tangent) { - case TangentHandle::Tangent::Left: - point.set_left_tangent(primary); - point.set_right_tangent(secondary); - return; - case TangentHandle::Tangent::Right: - point.set_left_tangent(secondary); - point.set_right_tangent(primary); - return; - } - Q_UNREACHABLE(); + transform_tangent(delta, dynamic_cast(tool).tangent_mode(), tangent_key); } void PointSelectHandle::transform_tangent(const Vec2f& delta, TangentMode mode, - TangentHandle::Tangent tangent) + const Point::TangentKey& primary_tangent_key) { + const auto old_primary_tangent = m_point.geometry().tangent(primary_tangent_key); auto new_point = m_point.geometry(); - const auto [primary, secondary] = get_primary_secondary_tangent(new_point, tangent); - const auto transformation = ObjectTransformation().translated(delta); - const auto new_primary = transformation.transformed(this->transformation()).apply_to_position(primary); - auto new_secondary = secondary; + auto& secondary_tangents = new_point.tangents(); + secondary_tangents.erase(primary_tangent_key); + + + const auto t_delta = ObjectTransformation().translated(delta); + const auto t_this = this->transformation(); + const auto new_primary_tangent = t_delta.transformed(t_this).apply_to_position(old_primary_tangent); std::map map; if (mode == TangentMode::Mirror && !(QGuiApplication::keyboardModifiers() & Qt::ShiftModifier)) { - new_secondary = Point::mirror_tangent(secondary, primary, new_primary); - for (auto* buddy : m_point.joined_points()) { - if (buddy != &m_point) { - auto geometry = buddy->geometry(); - const auto left = Point::mirror_tangent(geometry.left_tangent(), primary, new_primary); - const auto right = Point::mirror_tangent(geometry.right_tangent(), primary, new_primary); - geometry.set_left_tangent(left); - geometry.set_right_tangent(right); - map[buddy] = geometry; - } + for (auto& [key, secondary] : secondary_tangents) { + secondary = Point::mirror_tangent(secondary, old_primary_tangent, new_primary_tangent); } } - set_primary_secondary_tangent(new_point, new_primary, new_secondary, tangent); + new_point.set_tangent(primary_tangent_key, new_primary_tangent); map[&m_point] = new_point; tool.scene()->submit(map); @@ -188,7 +157,7 @@ std::pair PointSelectHandle::tangents_active() const const auto* interpolation_property = m_path_object.property(PathObject::INTERPOLATION_PROPERTY_KEY); const auto interpolation_mode = interpolation_property->value(); if ((interpolation_mode == InterpolationMode::Bezier && m_point.is_selected())) { - const auto points = m_path_object.geometry().find_path(m_point)->points(); + const auto points = m_path_object.path_vector().find_path(m_point)->points(); assert(!points.empty()); return {points.front() != &m_point, points.back() != &m_point}; } else { @@ -206,7 +175,7 @@ void PointSelectHandle::set_selected(bool selected) void PointSelectHandle::clear() { - for (auto* point : m_path_object.geometry().points()) { + for (auto* point : m_path_object.path_vector().points()) { point->set_selected(false); } } @@ -216,4 +185,29 @@ bool PointSelectHandle::is_selected() const return m_point.is_selected(); } +std::map +PointSelectHandle::other_tangents(const Point::TangentKey& tangent_key) const +{ + auto tangents = m_point.geometry().tangents(); + tangents.erase(tangent_key); + return tangents; +} + +bool PointSelectHandle::is_active(const Point::TangentKey& tangent_key) const +{ + if (tangent_key.path == nullptr) { + LWARNING << "Unexpected condition: " + "Tangent keys of points in PointSelectHandle should always have a path assigned."; + return true; + } + + if (tangent_key.direction == Direction::Backward) { + // backward tangent is active if the point is not the first point in the path. + return tangent_key.path->first_point().get() != &m_point; + } else { + // forward tangent is active if the point is not the last point in the path. + return tangent_key.path->last_point().get() != &m_point; + } +} + } // namespace omm diff --git a/src/tools/handles/pointselecthandle.h b/src/tools/handles/pointselecthandle.h index c5a298bb4..87b1e88e7 100644 --- a/src/tools/handles/pointselecthandle.h +++ b/src/tools/handles/pointselecthandle.h @@ -24,7 +24,7 @@ class PointSelectHandle : public AbstractSelectHandle bool mouse_move(const Vec2f& delta, const Vec2f& pos, const QMouseEvent& e) override; void mouse_release(const Vec2f& pos, const QMouseEvent& event) override; [[nodiscard]] PathPoint& point() const; - void transform_tangent(const Vec2f& delta, TangentHandle::Tangent tangent); + void transform_tangent(const Vec2f& delta, const Point::TangentKey& tangent_key); protected: [[nodiscard]] ObjectTransformation transformation() const; @@ -35,11 +35,12 @@ class PointSelectHandle : public AbstractSelectHandle private: PathObject& m_path_object; PathPoint& m_point; - std::unique_ptr m_left_tangent_handle; - std::unique_ptr m_right_tangent_handle; + std::map> m_tangent_handles; [[nodiscard]] std::pair tangents_active() const; + std::map other_tangents(const Point::TangentKey& tangent_key) const; + bool is_active(const Point::TangentKey& tangent_key) const; - void transform_tangent(const Vec2f& delta, TangentMode mode, TangentHandle::Tangent tangent); + void transform_tangent(const Vec2f& delta, TangentMode mode, const Point::TangentKey& tangent_key); }; } // namespace diff --git a/src/tools/handles/scalebandhandle.h b/src/tools/handles/scalebandhandle.h index 7118af0ad..7c5426943 100644 --- a/src/tools/handles/scalebandhandle.h +++ b/src/tools/handles/scalebandhandle.h @@ -3,6 +3,7 @@ #include "geometry/vec2.h" #include "tools/handles/handle.h" #include "tools/tool.h" +#include namespace omm { diff --git a/src/tools/handles/tangenthandle.cpp b/src/tools/handles/tangenthandle.cpp index e60f34b4c..dd95af0d9 100644 --- a/src/tools/handles/tangenthandle.cpp +++ b/src/tools/handles/tangenthandle.cpp @@ -6,8 +6,8 @@ namespace omm { -TangentHandle::TangentHandle(Tool& tool, PointSelectHandle& master_handle, Tangent tangent) - : Handle(tool), m_master_handle(master_handle), m_tangent(tangent) +TangentHandle::TangentHandle(Tool& tool, PointSelectHandle& master_handle, const Point::TangentKey& tangent_key) + : Handle(tool), m_master_handle(master_handle), m_tangent_key(tangent_key) { } @@ -35,7 +35,7 @@ bool TangentHandle::mouse_move(const Vec2f& delta, const Vec2f& pos, const QMous { Handle::mouse_move(delta, pos, e); if (status() == HandleStatus::Active) { - m_master_handle.transform_tangent(delta, m_tangent); + m_master_handle.transform_tangent(delta, m_tangent_key); return true; } else { return false; diff --git a/src/tools/handles/tangenthandle.h b/src/tools/handles/tangenthandle.h index 51f8d484a..9b402cdc6 100644 --- a/src/tools/handles/tangenthandle.h +++ b/src/tools/handles/tangenthandle.h @@ -1,6 +1,7 @@ #pragma once #include "tools/handles/handle.h" +#include "geometry/point.h" namespace omm { @@ -10,8 +11,7 @@ class PointSelectHandle; class TangentHandle : public Handle { public: - enum class Tangent { Left, Right }; - TangentHandle(Tool& tool, PointSelectHandle& master_handle, Tangent tangent); + TangentHandle(Tool& tool, PointSelectHandle& master_handle, const Point::TangentKey& tangent_key); [[nodiscard]] double draw_epsilon() const override; void draw(QPainter& painter) const override; [[nodiscard]] bool contains_global(const Vec2f& point) const override; @@ -22,7 +22,7 @@ class TangentHandle : public Handle private: PointSelectHandle& m_master_handle; - const Tangent m_tangent; + const Point::TangentKey m_tangent_key; }; } // namespace omm diff --git a/src/tools/pathtool.cpp b/src/tools/pathtool.cpp index 5e298d57d..06aa91b0c 100644 --- a/src/tools/pathtool.cpp +++ b/src/tools/pathtool.cpp @@ -1,134 +1,257 @@ #include "tools/pathtool.h" #include "commands/addcommand.h" -#include "commands/modifypointscommand.h" +#include "commands/addpointscommand.h" #include "commands/joinpointscommand.h" +#include "commands/modifypointscommand.h" +#include "commands/ownedlocatedpath.h" #include "main/application.h" #include "objects/pathobject.h" -#include "path/pathpoint.h" +#include "path/edge.h" #include "path/path.h" +#include "path/pathpoint.h" #include "path/pathvector.h" -#include "scene/scene.h" +#include "renderers/painter.h" #include "scene/history/historymodel.h" #include "scene/history/macro.h" -#include "tools/selecttool.h" +#include "scene/scene.h" #include "tools/handles/handle.h" +#include "tools/selecttool.h" #include -namespace omm +namespace { -struct PathTool::Current +template void emplace_back(auto& cs, T&& arg, Ts&&... args) { - PathObject* path_object = nullptr; - Path* path = nullptr; - PathPoint* point = nullptr; - PathPoint* last_point = nullptr; - - void find_tie(const Scene& scene) - { - path_object = nullptr; - path = nullptr; - last_point = nullptr; - auto path_objects = type_casts(scene.item_selection()); - if (path_objects.size() == 1) { - path_object = *path_objects.begin(); - if (auto sp = path_object->geometry().selected_points(); !sp.empty()) { - last_point = sp.front(); - path = path_object->geometry().find_path(*last_point); - } - } else { - path_object = nullptr; - } + cs.emplace_back(std::forward(arg)); + if constexpr (sizeof...(args) > 0) { + emplace_back(cs, std::forward(args)...); } -}; - -} // namespace omm +} -namespace +template typename Container, typename... Args> decltype(auto) make(Args&&... args) { + using T = std::tuple_element_t<0, std::tuple...>>; + Container cs; + if constexpr (requires(Container cs, std::size_t n) { cs.reserve(n); }) { + cs.reserve(sizeof...(args)); + } + emplace_back(cs, std::forward(args)...); + return cs; +} -using namespace omm; +} // namespace + +namespace omm +{ -class LeftButtonPressImpl +class PathTool::PathBuilder { public: - explicit LeftButtonPressImpl(Scene& scene, PathTool::Current& current) - : m_scene(scene) - , m_current(current) + explicit PathBuilder(Scene& scene) : m_scene(scene) {} + + /** + * @brief create_first_path_point a new path is created, not connected to any existing paths. + * @param pos the geometry of the first point + */ + void create_first_path_point(const Point& pos) + { + assert(is_valid()); + m_current_path = ¤t_path_vector().add_path(); + auto point = std::make_shared(pos, ¤t_path_vector()); + m_current_point = point.get(); + OwnedLocatedPath olp(m_current_path, 0, std::deque{std::move(point)}); + m_scene.submit(std::move(olp), current_path_vector().path_object()); + assert(is_valid()); + } + + void submit_add_points_command(omm::PathObject& path_object, + omm::Path& path, + const std::size_t point_offset, + std::deque>&& points) { + OwnedLocatedPath olp(&path, point_offset, std::move(points)); + auto command = std::make_unique(std::move(olp), &path_object); + const auto new_edges = command->new_edges(); + m_scene.submit(std::move(command)); + if (!new_edges.empty()) { + m_last_point = new_edges.back()->b().get(); + } } - void find_target_point(Tool& tool, const Vec2f& pos) + Path* find_most_sensible_path(const PathPoint& p) { - for (auto* handle : tool.handles()) { - const auto* point_select_handle = dynamic_cast(handle); - if (point_select_handle != nullptr && point_select_handle->contains_global(pos)) { - m_target_point = &point_select_handle->point(); - return; + const auto paths = p.path_vector()->paths(); + for (auto* const path : paths) { + if (path->last_point().get() == &p || path->first_point().get() == &p) { + return path; } } + for (auto* const path : paths) { + if (path->contains(p)) { + return path; + } + } + return nullptr; } - void insert_point_segment(const Point& point, const std::size_t index) + void add_point(std::shared_ptr&& b) { - std::deque> points; - m_current.point = points.emplace_back(std::make_unique(point, *m_current.path)).get(); - m_located_paths.emplace_back(m_current.path, index, std::move(points)); - if (m_target_point != nullptr) { - m_points_to_join.insert({m_current.point, m_target_point}); + assert(b); + m_current_point = b.get(); + if (m_last_point != nullptr && m_current_path == nullptr) { + m_current_path = find_most_sensible_path(*m_last_point); + } + assert(m_current_path != nullptr); + if (m_last_point == m_current_path->last_point().get()) { + // append + submit_add_points_command(*m_current_path_object, *m_current_path, m_current_path->points().size(), {b}); + } else if (auto root = current_path_vector().share(*m_last_point); root != nullptr) { + // branch + assert(m_last_point != nullptr); + m_current_path = ¤t_path_vector().add_path(); + submit_add_points_command(*m_current_path_object, *m_current_path, 0, {root, b}); + } else { + // m_last_point has been removed (e.g. by undo), also append. + submit_add_points_command(*m_current_path_object, *m_current_path, m_current_path->points().size(), {b}); } } - void add_point(const Point& point) + void add_point(Point point) { - if (m_current.path == nullptr) { - // no path is selected: add the point to a newly created segment - auto new_path = std::make_unique(std::deque{point}, &m_current.path_object->geometry()); - m_current.point = &new_path->at(0); - m_located_paths.emplace_back(std::move(new_path)); - } else if (m_current.path->size() == 0 || m_current.path->points().back() == m_current.last_point) { - // segment is empty or last point of the segmet is selected: append point at end - insert_point_segment(point, m_current.path->size()); - } else if (m_current.path->points().front() == m_current.last_point) { - // first point of segment is selected: append point at begin - insert_point_segment(point, 0); + ensure_active_path_object(); + assert(is_valid()); + point = m_current_path_object->global_transformation(Space::Viewport).inverted().apply(point); + if (m_last_point == nullptr) { + create_first_path_point(point); } else { - // other point of segment is selected: add point to a newly created segment and join points - auto new_path = std::make_unique(std::deque{m_current.last_point->geometry(), point}, - &m_current.path_object->geometry()); - m_current.point = &new_path->at(1); - m_points_to_join = {&new_path->at(0), m_current.last_point}; - m_located_paths.emplace_back(std::move(new_path)); + add_point(std::make_shared(point, ¤t_path_vector())); } + assert(is_valid()); } - void polish() + void find_tie() { - PathObject& current_path = *m_current.point->path_vector()->path_object(); - if (!m_points_to_join.empty()) { - start_macro(); + assert(is_valid()); + m_current_point = nullptr; + if (const auto selected_paths = m_scene.item_selection(); selected_paths.empty()) { + m_current_path_object = nullptr; + } else { + m_current_path_object = *selected_paths.begin(); + if (const auto ps = m_current_path_object->path_vector().selected_points(); !ps.empty()) { + m_current_point = *ps.begin(); + } } - m_scene.submit(current_path, std::move(m_located_paths)); - if (!m_points_to_join.empty()) { - m_scene.submit(m_scene, std::deque{m_points_to_join}); + assert(is_valid()); + } + + void close_path() + { + if (const auto selection = current_path_vector().selected_points(); m_last_point != nullptr && !selection.empty()) { + if (auto end = current_path_vector().share(**selection.begin()); end) { + // it may be that start and end have been removed from the path in the meanwhile (undo) + add_point(std::move(end)); + } } - current_path.geometry().deselect_all_points(); - m_current.point->set_selected(true); - current_path.update(); } - void ensure_active_path(Scene& scene, PathTool::Current& current) + PathObject* find_selected_path_object() const { - if (current.path_object == nullptr) { + const auto selection = m_scene.item_selection(); + static constexpr auto is_path_object = [](const auto* item) { return item->type() == PathObject::TYPE; }; + const auto it = std::find_if(selection.begin(), selection.end(), is_path_object); + if (it == selection.end()) { + return nullptr; + } else { + return dynamic_cast(*it); + } + } + + void ensure_active_path_object() + { + if (m_scene.contains(m_current_path_object) && m_scene.item_selection().contains(m_current_path_object)) { + // everything can stay as it is, we have an existing and selected m_current_path_object. + return; + } else if (auto* const selected_path_object = find_selected_path_object(); selected_path_object != nullptr) { + // There is a path object selected, but it's not m_current_path_object. + // Use the selected path object. + m_current_path_object = selected_path_object; + } else { + // There is no path object selected and m_current_path_object doesn't exist currently. + // Create a new one. start_macro(); static constexpr auto insert_mode = Application::InsertionMode::Default; auto& path_object = Application::instance().insert_object(PathObject::TYPE, insert_mode); - current.path_object = dynamic_cast(&path_object); - current.path_object->property(PathObject::INTERPOLATION_PROPERTY_KEY)->set(InterpolationMode::Bezier); - scene.set_selection({current.path_object}); + m_current_path_object = dynamic_cast(&path_object); + m_current_path_object->property(PathObject::INTERPOLATION_PROPERTY_KEY)->set(InterpolationMode::Bezier); + m_scene.set_selection({m_current_path_object}); } + m_current_point = nullptr; + m_last_point = nullptr; } + bool has_active_path_object() const + { + return m_current_path_object != nullptr; + } + + bool has_active_path() const + { + return m_current_path != nullptr; + } + + bool has_active_point() const + { + return m_current_point != nullptr; + } + + bool move_tangents(const Vec2f& delta) + { + if (m_current_path == nullptr || m_current_point == nullptr) { + return false; + } + + const auto bwd_key = Point::TangentKey(m_current_path, Direction::Backward); + const auto fwd_key = Point::TangentKey(m_current_path, Direction::Forward); + const auto lt = PolarCoordinates(m_current_point->geometry().tangent(bwd_key).to_cartesian() - delta); + auto& geometry = m_current_point->geometry(); + geometry.set_tangent(bwd_key, lt); + geometry.set_tangent(fwd_key, -lt); + m_current_point->set_geometry(geometry); + m_current_path_object->update(); + return true; + } + + void release() + { + m_last_point = m_current_point; + m_current_point = nullptr; + m_macro.reset(); + } + + void end() + { + if (m_current_path_object != nullptr) { + m_current_path_object->property(PathObject::INTERPOLATION_PROPERTY_KEY)->set(InterpolationMode::Bezier); + } + } + + PathVector& current_path_vector() const + { + assert(m_current_path_object != nullptr); + return m_current_path_object->path_vector(); + } + + bool is_valid() const + { + if (!m_current_path_object) { + return true; + } + const auto& paths = m_current_path_object->path_vector().paths(); + return std::all_of(paths.begin(), paths.end(), [](const Path* p) { return p->is_valid(); }); + } + +private: void start_macro() { if (m_macro == nullptr) { @@ -136,23 +259,17 @@ class LeftButtonPressImpl } } -private: + PathObject* m_current_path_object = nullptr; + Path* m_current_path = nullptr; + PathPoint* m_last_point = nullptr; + PathPoint* m_current_point = nullptr; Scene& m_scene; - PathTool::Current& m_current; std::unique_ptr m_macro; - PathPoint* m_target_point = nullptr; - std::deque m_located_paths; - ::transparent_set m_points_to_join; }; -} // namespace - -namespace omm -{ - PathTool::PathTool(Scene& scene) : SelectPointsBaseTool(scene) - , m_current(std::make_unique()) + , m_path_builder(std::make_unique(scene)) { } @@ -162,36 +279,25 @@ bool PathTool::mouse_move(const Vec2f& delta, const Vec2f& pos, const QMouseEven { if (SelectPointsBaseTool::mouse_move(delta, pos, event)) { return true; - } else if (m_current->path == nullptr || m_current->point == nullptr) { - return false; } else { - const auto lt = PolarCoordinates(m_current->point->geometry().left_tangent().to_cartesian() + delta); - auto geometry = m_current->point->geometry(); - geometry.set_left_tangent(lt); - geometry.set_right_tangent(-lt); - m_current->point->set_geometry(geometry); - m_current->path_object->update(); - return true; + return m_path_builder->move_tangents(delta); } } bool PathTool::mouse_press(const Vec2f& pos, const QMouseEvent& event) { const auto control_modifier = static_cast(event.modifiers() & Qt::ControlModifier); - if (!control_modifier && SelectPointsBaseTool::mouse_press(pos, event, false)) { + if (SelectPointsBaseTool::mouse_press(pos, event, false)) { + if (control_modifier) { + m_path_builder->close_path(); + } else { + m_path_builder->find_tie(); + } return true; } else { - m_current->find_tie(*scene()); if (event.button() == Qt::LeftButton) { - LeftButtonPressImpl impl{*scene(), *m_current}; - if (control_modifier) { - impl.find_target_point(*this, pos); - } - impl.ensure_active_path(*scene(), *m_current); - const auto transformation = m_current->path_object->global_transformation(Space::Viewport).inverted(); - const Point point{transformation.apply_to_position(pos)}; - impl.add_point(point); - impl.polish(); + m_path_builder->add_point(Point{pos}); + reset(); return true; } return false; @@ -201,7 +307,7 @@ bool PathTool::mouse_press(const Vec2f& pos, const QMouseEvent& event) void PathTool::mouse_release(const Vec2f& pos, const QMouseEvent& event) { SelectPointsBaseTool::mouse_release(pos, event); - m_current->point = nullptr; + m_path_builder->release(); } QString PathTool::type() const @@ -212,15 +318,18 @@ QString PathTool::type() const void PathTool::end() { SelectPointsBaseTool::end(); - if (m_current->path != nullptr) { - m_current->path_object->property(PathObject::INTERPOLATION_PROPERTY_KEY)->set(InterpolationMode::Bezier); - } + m_path_builder->end(); } void PathTool::reset() { - m_current->find_tie(*scene()); +// m_path_builder->find_tie(); SelectPointsBaseTool::reset(); } +void PathTool::draw(Painter& painter) const +{ + SelectPointsBaseTool::draw(painter); +} + } // namespace omm diff --git a/src/tools/pathtool.h b/src/tools/pathtool.h index d65083a3c..1e4e00fa0 100644 --- a/src/tools/pathtool.h +++ b/src/tools/pathtool.h @@ -22,10 +22,11 @@ class PathTool : public SelectPointsBaseTool [[nodiscard]] QString type() const override; void end() override; void reset() override; - struct Current; + void draw(Painter& painter) const override; + class PathBuilder; private: - std::unique_ptr m_current; + std::unique_ptr m_path_builder; }; } // namespace omm diff --git a/src/tools/selectfacestool.cpp b/src/tools/selectfacestool.cpp new file mode 100644 index 000000000..849e18e01 --- /dev/null +++ b/src/tools/selectfacestool.cpp @@ -0,0 +1,49 @@ +#include "tools/selectfacestool.h" +#include "handles/facehandle.h" +#include "objects/pathobject.h" +#include "path/face.h" +#include "path/pathvector.h" +#include "scene/faceselection.h" +#include "scene/scene.h" + +namespace omm +{ + +QString SelectFacesTool::type() const +{ + return TYPE; +} + +SceneMode SelectFacesTool::scene_mode() const +{ + return SceneMode::Face; +} + +Vec2f SelectFacesTool::selection_center() const +{ + return Vec2f{}; +} + +void SelectFacesTool::transform_objects(ObjectTransformation transformation) +{ + Q_UNUSED(transformation) +// return scene()->face_selection->center(Space::Viewport); +} + +void SelectFacesTool::reset() +{ + clear(); + make_handles(); +} + +void SelectFacesTool::make_handles() +{ + for (auto* path_object : scene()->item_selection()) { + for (const auto& face : path_object->faces()) { + auto handle = std::make_unique(*this, *path_object, face); + push_handle(std::move(handle)); + } + } +} + +} // namespace omm diff --git a/src/tools/selectfacestool.h b/src/tools/selectfacestool.h new file mode 100644 index 000000000..f42726ba6 --- /dev/null +++ b/src/tools/selectfacestool.h @@ -0,0 +1,24 @@ +#pragma once + +#include "tools/selecttool.h" + +namespace omm +{ + +class SelectFacesTool : public AbstractSelectTool +{ + Q_OBJECT +public: + using AbstractSelectTool::AbstractSelectTool; + QString type() const override; + SceneMode scene_mode() const override; + static constexpr auto TYPE = QT_TRANSLATE_NOOP("any-context", "SelectFacesTool"); + Vec2f selection_center() const override; + void transform_objects(omm::ObjectTransformation transformation) override; + void reset() override; + +private: + void make_handles(); +}; + +} // namespace diff --git a/src/tools/selectpointsbasetool.cpp b/src/tools/selectpointsbasetool.cpp index 619f9ff77..7f38e358d 100644 --- a/src/tools/selectpointsbasetool.cpp +++ b/src/tools/selectpointsbasetool.cpp @@ -76,7 +76,7 @@ bool SelectPointsBaseTool::mouse_press(const Vec2f& pos, const QMouseEvent& even return true; } else if (allow_clear && event.buttons() == Qt::LeftButton) { for (auto* path_object : path_objects) { - path_object->geometry().deselect_all_points(); + path_object->path_vector().deselect_all_points(); } Q_EMIT scene()->mail_box().point_selection_changed(); return false; @@ -88,7 +88,7 @@ bool SelectPointsBaseTool::mouse_press(const Vec2f& pos, const QMouseEvent& even void SelectPointsBaseTool::make_handles() { for (auto* path_object : scene()->item_selection()) { - const auto points = path_object->geometry().points(); + const auto points = path_object->path_vector().points(); for (auto* point : points) { auto handle = std::make_unique(*this, *path_object, *point); push_handle(std::move(handle)); diff --git a/src/tools/selectsimilartool.cpp b/src/tools/selectsimilartool.cpp index e985bb068..63ca78cc8 100644 --- a/src/tools/selectsimilartool.cpp +++ b/src/tools/selectsimilartool.cpp @@ -17,20 +17,10 @@ constexpr auto MODE_PROPERTY_KEY = "mode"; constexpr auto STRATEGY_PROPERTY_KEY = "strategy"; constexpr auto THRESHOLD_PROPERTY_KEY = "threshold"; constexpr auto APPLY_PROPERTY_KEY = "apply"; -enum class Mode { Normal, X, Y, Distance }; +enum class Mode { X, Y, Distance }; enum class MatchStrategy { Any, All }; using namespace omm; -double normal_distance(const Point& a, const Point& b) -{ - const auto normalize = [](double a, double b) { - return M_180_PI * PolarCoordinates::normalize_angle(std::abs(a - b)) - M_PI_2; - }; - - return std::min(normalize(a.left_tangent().argument, b.left_tangent().argument), - normalize(a.right_tangent().argument, b.right_tangent().argument)); -} - omm::Vec2f distance(const PathObject& path_object, const Point& a, const Point& b, @@ -105,7 +95,7 @@ void SelectSimilarTool::update_selection() { const auto strategy = property(STRATEGY_PROPERTY_KEY)->value(); for (const auto* path_object : scene()->item_selection()) { - for (auto* point : path_object->geometry().points()) { + for (auto* point : path_object->path_vector().points()) { if (m_base_selection.contains(point)) { const auto is_similar = [point, path_object, this](const PathPoint* b) { return this->is_similar(*path_object, point->geometry(), b->geometry()); @@ -132,7 +122,7 @@ void SelectSimilarTool::update_base_selection() { m_base_selection.clear(); for (const auto* path_object : scene()->item_selection()) { - for (auto* point : path_object->geometry().points()) { + for (auto* point : path_object->path_vector().points()) { if (point->is_selected()) { m_base_selection.insert(point); } @@ -146,8 +136,6 @@ bool SelectSimilarTool::is_similar(const PathObject& path_object, const Point& a const auto mode = property(MODE_PROPERTY_KEY)->value(); const auto distance = [mode, &path_object, alignment](const Point& a, const Point& b) { switch (mode) { - case Mode::Normal: - return normal_distance(a, b); case Mode::X: return std::abs(::distance(path_object, a, b, alignment).x); case Mode::Y: @@ -179,7 +167,6 @@ void SelectSimilarTool::update_property_appearance() const auto mode = property(MODE_PROPERTY_KEY)->value(); auto* const threshold_property = dynamic_cast(property(THRESHOLD_PROPERTY_KEY)); static const std::map> threshold_config{ - {Mode::Normal, {"°", 180.0}}, // NOLINT(cppcoreguidelines-avoid-magic-numbers) {Mode::X, {"", std::numeric_limits::max()}}, {Mode::Y, {"", std::numeric_limits::max()}}, {Mode::Distance, {"", std::numeric_limits::max()}}, diff --git a/src/tools/tool.cpp b/src/tools/tool.cpp index 9e3fca833..d3cfa9904 100644 --- a/src/tools/tool.cpp +++ b/src/tools/tool.cpp @@ -76,12 +76,8 @@ void Tool::draw(Painter& renderer) const } } - bool Tool::is_active() const { - for (const auto* handle : handles()) { - std::cout << "handle " << handle << ": " << int(handle->status()) << std::endl; - } return std::any_of(m_handles.begin(), m_handles.end(), [](const auto& handle) { return handle->status() == HandleStatus::Active; }); diff --git a/src/tools/toolbox.cpp b/src/tools/toolbox.cpp index 6c66e5a82..9f16efb9f 100644 --- a/src/tools/toolbox.cpp +++ b/src/tools/toolbox.cpp @@ -3,6 +3,7 @@ #include "scene/scene.h" #include "tools/selectobjectstool.h" #include "tools/selectpointstool.h" +#include "tools/selectfacestool.h" #include "tools/selecttool.h" namespace @@ -38,6 +39,7 @@ namespace omm const decltype(ToolBox::m_default_tools) ToolBox::m_default_tools { {omm::SceneMode::Object, omm::SelectObjectsTool::TYPE}, {omm::SceneMode::Vertex, omm::SelectPointsTool::TYPE}, + {omm::SceneMode::Face, omm::SelectFacesTool::TYPE}, }; ToolBox::ToolBox(Scene& scene) diff --git a/src/tools/transformpointshelper.cpp b/src/tools/transformpointshelper.cpp index 9825c2af3..8b3c4992e 100644 --- a/src/tools/transformpointshelper.cpp +++ b/src/tools/transformpointshelper.cpp @@ -71,11 +71,8 @@ void TransformPointsHelper::update() { m_initial_points.clear(); for (const auto* path_object : m_path_objects) { - for (PathPoint* point : path_object->geometry().selected_points()) { + for (PathPoint* point : path_object->path_vector().selected_points()) { m_initial_points[point] = point->geometry(); - for (PathPoint* buddy : point->joined_points()) { - m_initial_points[buddy] = point->compute_joined_point_geometry(*buddy); - } } } Q_EMIT initial_transformations_changed(); diff --git a/src/variant.h b/src/variant.h index ec107dd8e..02b7c0f0b 100644 --- a/src/variant.h +++ b/src/variant.h @@ -1,11 +1,14 @@ #pragma once #include "color/color.h" +#include "path/face.h" #include "geometry/vec2.h" #include "logging.h" #include "splinetype.h" +#include "facelist.h" #include #include +#include namespace omm { @@ -22,8 +25,8 @@ class TriggerPropertyDummyValueType { return false; } -} -; +}; + using variant_type = std::variant; + SplineType, + FaceList>; template T null_value(); diff --git a/src/widgets/CMakeLists.txt b/src/widgets/CMakeLists.txt index 0fc5dff7e..9d24ea32a 100644 --- a/src/widgets/CMakeLists.txt +++ b/src/widgets/CMakeLists.txt @@ -13,10 +13,6 @@ target_sources(libommpfritt PRIVATE multitabbar.h numericedit.cpp numericedit.h - pointdialog.cpp - pointdialog.h - pointedit.cpp - pointedit.h referencelineedit.cpp referencelineedit.h treeexpandmemory.cpp diff --git a/src/widgets/pointdialog.cpp b/src/widgets/pointdialog.cpp deleted file mode 100644 index c30a25a3f..000000000 --- a/src/widgets/pointdialog.cpp +++ /dev/null @@ -1,80 +0,0 @@ -#include "widgets/pointdialog.h" -#include "objects/pathobject.h" -#include "path/pathvector.h" -#include "widgets/coordinateedit.h" -#include "widgets/pointedit.h" -#include -#include -#include -#include -#include -#include -#include -#include - -namespace -{ -auto make_tab_widget_page(omm::PathObject& path_object, std::list& point_edits) -{ - auto scroll_area = std::make_unique(); - scroll_area->setWidgetResizable(true); - scroll_area->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - - auto widget = std::make_unique(); - auto layout = std::make_unique(); - - for (auto* point : path_object.geometry().points()) { - auto point_edit = std::make_unique(path_object, *point); - point_edits.push_back(point_edit.get()); - layout->addWidget(point_edit.release()); - } - layout->addStretch(); - - widget->setLayout(layout.release()); - scroll_area->setWidget(widget.release()); - return scroll_area; -} - -} // namespace - -namespace omm -{ -PointDialog::PointDialog(const std::set& paths, QWidget* parent) : QDialog(parent) -{ - assert(!paths.empty()); - auto tab_widget = std::make_unique(); - for (PathObject* path : paths) { - tab_widget->addTab(make_tab_widget_page(*path, m_point_edits).release(), path->name()); - } - auto button_box = std::make_unique(QDialogButtonBox::Ok); - connect(button_box.get(), &QDialogButtonBox::accepted, this, &PointDialog::accept); - - auto mode_combobox = std::make_unique(); - m_mode_combobox = mode_combobox.get(); - m_mode_combobox->addItems({tr("Polar"), tr("Cartesian"), tr("Both")}); - connect(m_mode_combobox, qOverload(&QComboBox::currentIndexChanged), [this](int i) { - for (auto&& point_edit : m_point_edits) { - point_edit->set_display_mode(static_cast(i + 1)); - } - }); - QSettings settings; - m_mode_combobox->setCurrentIndex(settings.value(DISPLAY_MODE_SETTINGS_KEY, 2).toInt()); - - auto layout = std::make_unique(); - layout->addWidget(tab_widget.release()); - - auto hlayout = std::make_unique(); - hlayout->addWidget(mode_combobox.release()); - hlayout->addWidget(button_box.release()); - - layout->addLayout(hlayout.release()); - setLayout(layout.release()); -} - -PointDialog::~PointDialog() -{ - QSettings s; - s.setValue(DISPLAY_MODE_SETTINGS_KEY, m_mode_combobox->currentIndex()); -} - -} // namespace omm diff --git a/src/widgets/pointdialog.h b/src/widgets/pointdialog.h deleted file mode 100644 index 8d0420980..000000000 --- a/src/widgets/pointdialog.h +++ /dev/null @@ -1,32 +0,0 @@ -#pragma once - -#include -#include -#include - -class QComboBox; - -namespace omm -{ - -class PathObject; -class PointEdit; - -class PointDialog : public QDialog -{ - Q_OBJECT -public: - explicit PointDialog(const std::set& paths, QWidget* parent = nullptr); - ~PointDialog() override; - PointDialog(PointDialog&&) = delete; - PointDialog(const PointDialog&) = delete; - PointDialog& operator=(PointDialog&&) = delete; - PointDialog& operator=(const PointDialog&) = delete; - -private: - std::list m_point_edits; - QComboBox* m_mode_combobox; - static constexpr auto DISPLAY_MODE_SETTINGS_KEY = "PointDialog/display_mode"; -}; - -} // namespace omm diff --git a/src/widgets/pointedit.cpp b/src/widgets/pointedit.cpp deleted file mode 100644 index 8c09f23be..000000000 --- a/src/widgets/pointedit.cpp +++ /dev/null @@ -1,165 +0,0 @@ -#include "widgets/pointedit.h" -#include "commands/modifypointscommand.h" -#include "scene/scene.h" -#include "objects/pathobject.h" -#include "path/pathpoint.h" -#include "widgets/coordinateedit.h" -#include -#include -#include -#include - -namespace -{ -auto make_tangent_layout(omm::CoordinateEdit*& coordinate_edit_ref, - QPushButton*& mirror_button_ref, - QPushButton*& vanish_button_ref) -{ - auto coordinate_edit = std::make_unique(); - coordinate_edit_ref = coordinate_edit.get(); - auto vanish_button = std::make_unique("V"); - vanish_button_ref = vanish_button.get(); - auto mirror_button = std::make_unique(">|<"); - mirror_button_ref = mirror_button.get(); - - auto hlayout = std::make_unique(); - hlayout->addWidget(vanish_button.release()); - hlayout->addWidget(mirror_button.release()); - - auto vlayout = std::make_unique(); - vlayout->addWidget(coordinate_edit.release()); - vlayout->addLayout(hlayout.release()); - - return vlayout; -} -} // namespace - -namespace omm -{ -PointEdit::PointEdit(PathObject& path_object, PathPoint& point, QWidget* parent) - : QWidget(parent), m_point(point), m_path(&path_object) -{ - if (m_point.is_selected()) { - QPalette palette = this->palette(); - palette.setColor(QPalette::Window, palette.color(QPalette::AlternateBase)); - setPalette(palette); - setAutoFillBackground(true); - } - - auto coupled = std::make_unique(); - auto left = make_tangent_layout(m_left_tangent_edit, m_mirror_from_right, m_vanish_left); - auto right = make_tangent_layout(m_right_tangent_edit, m_mirror_from_left, m_vanish_right); - - auto center = std::make_unique(); - - auto position_edit = std::make_unique(); - m_position_edit = position_edit.get(); - center->addWidget(position_edit.release()); - - auto couple_button = std::make_unique(tr("LNK")); - couple_button->setCheckable(true); - couple_button->setChecked(true); - m_coupled = couple_button.get(); - center->addWidget(couple_button.release()); - - auto main = std::make_unique(); - main->addLayout(left.release()); - main->addLayout(center.release()); - main->addLayout(right.release()); - - setLayout(main.release()); - - m_left_tangent_edit->set_coordinates(m_point.geometry().left_tangent().to_cartesian()); - m_right_tangent_edit->set_coordinates(m_point.geometry().right_tangent().to_cartesian()); - m_position_edit->set_coordinates(m_point.geometry().position()); - m_position_edit->set_display_mode(DisplayMode::Cartesian); - - connect(m_mirror_from_left, &QPushButton::clicked, this, &PointEdit::mirror_from_left); - connect(m_mirror_from_right, &QPushButton::clicked, this, &PointEdit::mirror_from_right); - connect(m_vanish_left, &QPushButton::clicked, [this]() { - QSignalBlocker m_left(m_left_tangent_edit); - m_left_tangent_edit->set_magnitude(0.0); - update_point(); - }); - connect(m_vanish_right, &QPushButton::clicked, [this]() { - QSignalBlocker m_right(m_right_tangent_edit); - m_right_tangent_edit->set_magnitude(0.0); - update_point(); - }); - - connect(m_left_tangent_edit, - &CoordinateEdit::value_changed_val, - this, - &PointEdit::set_right_maybe); - connect(m_right_tangent_edit, - &CoordinateEdit::value_changed_val, - this, - &PointEdit::set_left_maybe); - - { - connect(m_left_tangent_edit, &CoordinateEdit::value_changed, this, &PointEdit::update_point); - connect(m_right_tangent_edit, &CoordinateEdit::value_changed, this, &PointEdit::update_point); - connect(m_position_edit, &CoordinateEdit::value_changed, this, &PointEdit::update_point); - } -} - -void PointEdit::mirror_from_right() -{ - QSignalBlocker b_left(m_left_tangent_edit); - m_left_tangent_edit->set_coordinates(-m_right_tangent_edit->to_polar()); - update_point(); -} - -void PointEdit::mirror_from_left() -{ - QSignalBlocker b_right(m_right_tangent_edit); - m_right_tangent_edit->set_coordinates(-m_left_tangent_edit->to_polar()); - update_point(); -} - -void PointEdit::set_left_maybe(const PolarCoordinates& old_right, const PolarCoordinates& new_right) -{ - if (m_coupled->isChecked()) { - const auto old_pos = m_left_tangent_edit->to_polar(); - const auto new_pos = Point::mirror_tangent(old_pos, old_right, new_right); - QSignalBlocker b_left(m_left_tangent_edit); - m_left_tangent_edit->set_coordinates(new_pos); - } -} - -void PointEdit::set_right_maybe(const PolarCoordinates& old_left, const PolarCoordinates& new_left) -{ - if (m_coupled->isChecked()) { - const auto old_pos = m_right_tangent_edit->to_polar(); - const auto new_pos = Point::mirror_tangent(old_pos, old_left, new_left); - QSignalBlocker b_right(m_right_tangent_edit); - m_right_tangent_edit->set_coordinates(new_pos); - } -} - -void PointEdit::update_point() -{ - if (m_path == nullptr || m_path->scene() == nullptr) { - auto geometry = m_point.geometry(); - geometry.set_left_tangent(m_left_tangent_edit->to_polar()); - geometry.set_position(m_position_edit->to_cartesian()); - geometry.set_right_tangent(m_right_tangent_edit->to_polar()); - m_point.set_geometry(geometry); - } else { - std::map map; - Point new_point(m_position_edit->to_cartesian(), - m_left_tangent_edit->to_polar(), - m_right_tangent_edit->to_polar()); - map[&m_point] = new_point; - m_path->scene()->submit(map); - m_path->update(); - } -} - -void PointEdit::set_display_mode(const DisplayMode& display_mode) -{ - m_left_tangent_edit->set_display_mode(display_mode); - m_right_tangent_edit->set_display_mode(display_mode); -} - -} // namespace omm diff --git a/src/widgets/pointedit.h b/src/widgets/pointedit.h deleted file mode 100644 index 531988e89..000000000 --- a/src/widgets/pointedit.h +++ /dev/null @@ -1,45 +0,0 @@ -#pragma once - -#include - -class QPushButton; - -namespace omm -{ -class CoordinateEdit; -class PathObject; -class Point; -class PathPoint; -struct PolarCoordinates; -enum class DisplayMode; - -class PointEdit : public QWidget -{ - Q_OBJECT -public: - PointEdit(PathObject& path_object, PathPoint& point, QWidget* parent = nullptr); - void set_display_mode(const DisplayMode& display_mode); - -private: - QPushButton* m_mirror_from_left = nullptr; - QPushButton* m_mirror_from_right = nullptr; - QPushButton* m_coupled; - QPushButton* m_vanish_left = nullptr; - QPushButton* m_vanish_right = nullptr; - CoordinateEdit* m_left_tangent_edit = nullptr; - CoordinateEdit* m_right_tangent_edit = nullptr; - CoordinateEdit* m_position_edit; - PathPoint& m_point; - PathObject* m_path{}; - -private: - void mirror_from_right(); - void mirror_from_left(); - void set_left_maybe(const omm::PolarCoordinates& old_right, - const omm::PolarCoordinates& new_right); - void set_right_maybe(const omm::PolarCoordinates& old_left, - const omm::PolarCoordinates& new_left); - void update_point(); -}; - -} // namespace omm diff --git a/test/unit/CMakeLists.txt b/test/unit/CMakeLists.txt index ea9bca42a..9609d04d7 100644 --- a/test/unit/CMakeLists.txt +++ b/test/unit/CMakeLists.txt @@ -17,6 +17,7 @@ else() endif() macro(package_add_test SOURCE_FILE) + # TODO compile all tests into a single executable? string(REGEX REPLACE "\.[^.]*$" "" TESTNAME "${SOURCE_FILE}") add_executable(${TESTNAME} main.cpp testutil.cpp testutil.h ${SOURCE_FILE} ${compiled_resource_file}) add_dependencies(${TESTNAME} libommpfritt) @@ -32,14 +33,18 @@ macro(package_add_test SOURCE_FILE) set_target_properties(${TESTNAME} PROPERTIES FOLDER tests) endmacro() +package_add_test(ccwcomparatortest.cpp) package_add_test(color.cpp) -package_add_test(common.cpp) package_add_test(converttest.cpp) package_add_test(dnftest.cpp) +package_add_test(faceareatest.cpp) package_add_test(geometry.cpp) +package_add_test(graphtest.cpp) package_add_test(icon.cpp) +package_add_test(linetest.cpp) package_add_test(nodetest.cpp) package_add_test(pathtest.cpp) +package_add_test(polygontest.cpp) package_add_test(propertytest.cpp) package_add_test(serialization.cpp) package_add_test(splinetypetest.cpp) diff --git a/test/unit/ccwcomparatortest.cpp b/test/unit/ccwcomparatortest.cpp new file mode 100644 index 000000000..38404d153 --- /dev/null +++ b/test/unit/ccwcomparatortest.cpp @@ -0,0 +1,95 @@ +#include "path/ccwcomparator.h" +#include "geometry/point.h" +#include "path/edge.h" +#include "path/path.h" +#include "path/pathpoint.h" +#include "path/pathvector.h" +#include "testutil.h" +#include "gtest/gtest.h" + +namespace +{ + +class PathFactory +{ +public: + explicit PathFactory(omm::PathVector& path_vector) : m_path_vector(path_vector), m_hinge(make_path_point({0, 0})) + { + } + + omm::Edge* make_trunk() + { + auto& trunk = add_path(); + m_base = &trunk.add_edge(make_path_point({-1, 0}), m_hinge); + return &trunk.add_edge(m_hinge, make_path_point({1, 0})); + } + + omm::Edge* straight(const double angle) const + { + return &add_path().add_edge(m_hinge, make_path_point({std::cos(angle), std::sin(angle)})); + } + + omm::Edge* straight_out(const double first_tangent_length, const omm::Vec2f& second_tangent_pos, + const omm::Vec2f& second_point_pos) const + { + auto& path = add_path(); + auto second_point = make_path_point(second_point_pos); + second_point->geometry().set_tangent_position({&path, omm::Direction::Backward}, second_tangent_pos); + m_hinge->geometry().set_tangent({&path, omm::Direction::Forward}, omm::PolarCoordinates(0, first_tangent_length)); + return &path.add_edge(m_hinge, second_point); + } + + omm::Edge* base() const + { + return m_base; + } + +private: + omm::Path& add_path() const + { + return m_path_vector.add_path(); + } + + std::shared_ptr make_path_point(const omm::Vec2f& pos) const + { + return std::make_shared(omm::Point(pos), &m_path_vector); + } + + omm::Edge* m_base; + omm::PathVector& m_path_vector; + std::shared_ptr m_hinge; +}; + +} // namespace + +TEST(CCWComparatorTest, A) +{ + auto pv = omm::PathVector(); + std::deque edges; + + PathFactory path_factory(pv); + + edges.emplace_back(path_factory.make_trunk()); + edges.emplace_back(path_factory.straight_out(0.2, {0.5, 0.0}, {1.0, 1.0})); + edges.emplace_back(path_factory.straight_out(0.2, {0.3, 0.0}, {1.0, 1.0})); + edges.emplace_back(path_factory.straight_out(0.2, {0.5, 0.0}, {1.0, 2.0})); + edges.emplace_back(path_factory.straight_out(0.2, {0.3, 0.0}, {1.0, 2.0})); + edges.emplace_back(path_factory.straight(45 * M_PI / 180.0)); + + ommtest::Application app; + pv.to_svg("/tmp/" + ommtest::Application::test_id_for_filename() + ".svg"); + + // Check if edges are ascending. + // Only checking if edges is sorted is not sufficient, we want to compare each item with every item to + // make sure that the order incuded by CCWComparator is well-defined. + const omm::CCWComparator ccw_comparator(omm::DEdge::fwd(path_factory.base())); + for (std::size_t i = 0; i < edges.size(); ++i) { + for (std::size_t j = 0; j < edges.size(); ++j) { + const auto a = omm::DEdge::fwd(edges.at(i)); + const auto b = omm::DEdge::fwd(edges.at(j)); + const auto comp_result = ccw_comparator(a, b); + static constexpr auto to_string = [](const auto& edge) { return edge.to_string().toStdString(); }; + EXPECT_EQ(comp_result, i < j) << to_string(a) << " < " << to_string(b); + } + } +} diff --git a/test/unit/common.cpp b/test/unit/common.cpp deleted file mode 100644 index 4e380dd44..000000000 --- a/test/unit/common.cpp +++ /dev/null @@ -1,352 +0,0 @@ -#include "common.h" -#include "logging.h" -#include "disjointset.h" -#include "gtest/gtest.h" -#include -#include - -namespace -{ -struct Vertex { - explicit Vertex(int id) : id(id) - { - } - std::set successors; - operator int() const - { - return id; - } - bool operator==(const Vertex& other) const - { - return other.id == id; - } - -private: - int id; -}; - -using id_type = int; -using test_case_type = std::map>; - -test_case_type make_test(const std::set& ids) -{ - test_case_type vertices; - for (int id : ids) { - vertices.insert({id, std::make_unique(id)}); - } - return vertices; -} - -void connect(const test_case_type& vertices, id_type from, id_type to) -{ - vertices.at(from)->successors.insert(vertices.at(to).get()); -} - -test_case_type wiki_test_case() -{ - // Wikipedia has a drawing of test test graph: - // https://en.wikipedia.org/wiki/Topological_sorting#/media/File:Directed_acyclic_graph_2.svg - - test_case_type vertices = make_test({2, 3, 5, 7, 8, 9, 10, 11}); - connect(vertices, 5, 11); - connect(vertices, 11, 2); - connect(vertices, 11, 9); - connect(vertices, 11, 10); - connect(vertices, 7, 11); - connect(vertices, 7, 8); - connect(vertices, 3, 8); - connect(vertices, 3, 10); - connect(vertices, 8, 9); - - assert(vertices.size() == 8); - return vertices; -} - -test_case_type empty_test_case() -{ - return make_test({}); -} - -test_case_type single_vertex_test_case() -{ - return make_test({0}); -} - -test_case_type cycle_test_case() -{ - test_case_type vertices = make_test({0, 1}); - connect(vertices, 0, 1); - connect(vertices, 1, 0); - return vertices; -} - -template std::set get_vertices(const std::map>& map) -{ - return util::transform(map, [](auto&& p) { return p.second.get(); }); -} - -std::set get_successors(const Vertex* v) -{ - return v->successors; -} - -decltype(auto) topological_sort(const test_case_type& test_case) -{ - return omm::topological_sort(get_vertices(test_case), get_successors); -} - -} // namespace - -TEST(common, find_path) -{ - const auto test_case = wiki_test_case(); - -#define check_path(start_id, end_id, expect_exists) \ - do { \ - std::list path; \ - Vertex& start = *test_case.at(start_id); \ - Vertex& end = *test_case.at(end_id); \ - bool actual_exists = omm::find_path(&start, &end, path, get_successors); \ - EXPECT_EQ(actual_exists, expect_exists); \ - } while (false) - - check_path(5, 7, false); - check_path(5, 3, false); - check_path(5, 11, true); - check_path(5, 8, false); - check_path(5, 2, true); - check_path(5, 9, true); - check_path(5, 10, true); - - check_path(7, 5, false); - check_path(7, 3, false); - check_path(7, 11, true); - check_path(7, 8, true); - check_path(7, 2, true); - check_path(7, 9, true); - check_path(7, 10, true); - - check_path(3, 7, false); - check_path(3, 5, false); - check_path(3, 11, false); - check_path(3, 8, true); - check_path(3, 2, false); - check_path(3, 9, true); - check_path(3, 10, true); - - check_path(11, 7, false); - check_path(11, 3, false); - check_path(11, 5, false); - check_path(11, 8, false); - check_path(11, 2, true); - check_path(11, 9, true); - check_path(11, 10, true); - - check_path(8, 7, false); - check_path(8, 3, false); - check_path(8, 11, false); - check_path(8, 5, false); - check_path(8, 2, false); - check_path(8, 9, true); - check_path(8, 10, false); - - check_path(2, 7, false); - check_path(2, 3, false); - check_path(2, 11, false); - check_path(2, 8, false); - check_path(2, 5, false); - check_path(2, 9, false); - check_path(2, 10, false); - - check_path(9, 7, false); - check_path(9, 3, false); - check_path(9, 11, false); - check_path(9, 8, false); - check_path(9, 2, false); - check_path(9, 5, false); - check_path(9, 10, false); - - check_path(10, 7, false); - check_path(10, 3, false); - check_path(10, 11, false); - check_path(10, 8, false); - check_path(10, 2, false); - check_path(10, 9, false); - check_path(10, 5, false); -} - -TEST(common, tsort_simple) -{ - { - const auto [has_cycle, sequence] = ::topological_sort(empty_test_case()); - EXPECT_FALSE(has_cycle); - EXPECT_TRUE(sequence.empty()); - } - { - const auto [has_cycle, sequence] = ::topological_sort(single_vertex_test_case()); - EXPECT_FALSE(has_cycle); - } -} - -TEST(common, tsort_cycle) -{ - { - const auto [has_cycle, sequence] = topological_sort(cycle_test_case()); - EXPECT_TRUE(has_cycle); - } -} - -TEST(common, tsort) -{ - const auto test_case = wiki_test_case(); - const auto [has_cycle, sequence] = topological_sort(test_case); - const std::vector v_seq(sequence.begin(), sequence.end()); - for (std::size_t i = 0; i < v_seq.size(); ++i) { - for (std::size_t j = 0; j < i; ++j) { - std::list path_i_j; - // there must not be edges from back to front. - EXPECT_FALSE(omm::find_path(v_seq.at(i), v_seq.at(j), path_i_j, get_successors)); - } - } - EXPECT_FALSE(has_cycle); -} - -TEST(common, modern_cpp_for_loop_qt_cow_container) -{ - [[maybe_unused]] static bool begin_was_called = false; - [[maybe_unused]] static bool end_was_called = false; - [[maybe_unused]] static bool const_begin_was_called = false; - [[maybe_unused]] static bool const_end_was_called = false; - const auto reset = []() { - begin_was_called = true; - end_was_called = true; - const_begin_was_called = true; - const_end_was_called = true; - }; - - class TestContainer - { - private: - std::vector dummy; - - public: - decltype(auto) begin() - { - begin_was_called = true; - return dummy.begin(); - }; - - decltype(auto) end() - { - end_was_called = true; - return dummy.end(); - }; - - [[nodiscard]] decltype(auto) begin() const - { - const_begin_was_called = true; - return dummy.cbegin(); - }; - - [[nodiscard]] decltype(auto) end() const - { - const_end_was_called = true; - return dummy.cend(); - }; - }; - - auto f = []() { return TestContainer(); }; - - reset(); - for (auto&& i : f()) { - Q_UNUSED(i) - EXPECT_TRUE(begin_was_called); - EXPECT_TRUE(end_was_called); - EXPECT_FALSE(const_begin_was_called); - EXPECT_FALSE(const_end_was_called); - } - - reset(); - const auto fs = f(); - for (auto&& i : fs) { - Q_UNUSED(i) - EXPECT_FALSE(begin_was_called); - EXPECT_FALSE(end_was_called); - EXPECT_TRUE(const_begin_was_called); - EXPECT_TRUE(const_end_was_called); - } -} - -TEST(common, coherent_ranges) -{ - const std::vector values{0, 1, 2, 3, 4, 5, 6, 7, 8}; - - EXPECT_TRUE(omm::find_coherent_ranges(std::vector{}, [](auto&&) { return true; }).empty()); - - static constexpr auto is_even = [](int i) { return i % 2 == 0; }; - const auto even_ranges = omm::find_coherent_ranges(values, is_even); - ASSERT_EQ(even_ranges.size(), 5); - for (std::size_t i = 0; i < even_ranges.size(); ++i) { - EXPECT_EQ(even_ranges[i].start, 2 * i); - EXPECT_EQ(even_ranges[i].size, 1); - } - - static constexpr auto is_small = [](int i) { return i < 5; }; - const auto small_ranges = omm::find_coherent_ranges(values, is_small); - ASSERT_EQ(small_ranges.size(), 1); - EXPECT_EQ(small_ranges[0].start, 0); - EXPECT_EQ(small_ranges[0].size, 5); - - static constexpr auto is_big = [](int i) { return i > 5; }; - const auto big_ranges = omm::find_coherent_ranges(values, is_big); - ASSERT_EQ(big_ranges.size(), 1); - EXPECT_EQ(big_ranges[0].start, 6); - EXPECT_EQ(big_ranges[0].size, 3); - - static constexpr auto is_prime_power = [](int i) { return std::set{2, 3, 4, 5, 7, 8}.contains(i); }; - const auto prime_power_ranges = omm::find_coherent_ranges(values, is_prime_power); - ASSERT_EQ(prime_power_ranges.size(), 2); - EXPECT_EQ(prime_power_ranges[0].start, 2); - EXPECT_EQ(prime_power_ranges[0].size, 4); - EXPECT_EQ(prime_power_ranges[1].start, 7); - EXPECT_EQ(prime_power_ranges[1].size, 2); -} - -TEST(common, disjoint_set_forest) -{ - EXPECT_TRUE(omm::DisjointSetForest::sets_disjoint({1, 2, 3}, {4, 5, 6})); - EXPECT_FALSE(omm::DisjointSetForest::sets_disjoint({1, 2, 3}, {3, 5, 6})); - EXPECT_TRUE(omm::DisjointSetForest::sets_disjoint({}, {3, 5, 6})); - EXPECT_TRUE(omm::DisjointSetForest::sets_disjoint({3, 5, 6}, {})); - - omm::DisjointSetForest cluster; - EXPECT_EQ(cluster.get(42), ::transparent_set{}); - cluster.insert({1, 2 ,3}); - EXPECT_EQ(cluster.get(1), (::transparent_set{1, 2, 3})); - EXPECT_EQ(cluster.get(2), (::transparent_set{1, 2, 3})); - EXPECT_EQ(cluster.get(0), ::transparent_set{}); - - cluster.insert({4, 5}); - EXPECT_EQ(cluster.get(1), (::transparent_set{1, 2, 3})); - EXPECT_EQ(cluster.get(0), ::transparent_set{}); - EXPECT_EQ(cluster.get(4), (::transparent_set{4, 5})); - - cluster.remove({4}); - EXPECT_EQ(cluster.get(1), (::transparent_set{1, 2, 3})); - EXPECT_EQ(cluster.get(4), ::transparent_set{}); - EXPECT_EQ(cluster.get(5), ::transparent_set{}); - - cluster.insert({4, 5}); - cluster.insert({1, 6, 7}); - EXPECT_EQ(cluster.get(1), (::transparent_set{1, 2, 3, 6, 7})); - EXPECT_EQ(cluster.get(6), (::transparent_set{1, 2, 3, 6, 7})); - - cluster.insert({1, 4}); - EXPECT_EQ(cluster.get(1), (::transparent_set{1, 2, 3, 4, 5, 6, 7})); - EXPECT_EQ(cluster.get(3), (::transparent_set{1, 2, 3, 4, 5, 6, 7})); - - cluster.remove({4}); - EXPECT_EQ(cluster.get(1), ::transparent_set{}); - EXPECT_EQ(cluster.get(4), ::transparent_set{}); - EXPECT_EQ(cluster.get(5), ::transparent_set{}); -} diff --git a/test/unit/converttest.cpp b/test/unit/converttest.cpp index 244fe9786..3508af497 100644 --- a/test/unit/converttest.cpp +++ b/test/unit/converttest.cpp @@ -3,7 +3,6 @@ #include "external/json.hpp" #include "gtest/gtest.h" #include "main/application.h" -#include "main/options.h" #include "mainwindow/pathactions.h" #include "objects/ellipse.h" #include "objects/pathobject.h" @@ -11,43 +10,10 @@ #include "path/pathvector.h" #include "path/path.h" #include "scene/scene.h" -#include "scene/disjointpathpointsetforest.h" #include "testutil.h" -namespace -{ - -std::unique_ptr options() -{ - return std::make_unique(false, // is_cli - false // have_opengl - ); -} - -} // namespace TEST(convert, ellipse) { - ommtest::Application test_app(::options()); - auto& app = test_app.omm_app(); - auto& e = app.insert_object(omm::Ellipse::TYPE, omm::Application::InsertionMode::Default); - static constexpr auto corner_count = 12; - e.property(omm::Ellipse::CORNER_COUNT_PROPERTY_KEY)->set(corner_count); - app.scene->set_selection({&e}); - ASSERT_EQ(omm::path_actions::convert_objects(app).size(), 1); - app.scene->history().undo(); - const auto cs = omm::path_actions::convert_objects(app); - ASSERT_EQ(cs.size(), 1); - auto* const po = ::type_cast(*cs.begin()); - ASSERT_NE(po, nullptr); - auto& path_vector = po->geometry(); - ASSERT_EQ(path_vector.paths().size(), 1); - const auto& path = *path_vector.paths().front(); - ASSERT_EQ(path.points().size(), corner_count + 1); - EXPECT_TRUE(path_vector.joined_points_shared()); - const auto& joined_points = path_vector.joined_points(); - ASSERT_EQ(joined_points.sets().size(), 1); - const auto set = joined_points.get(path.points().front()); - EXPECT_EQ(set, (std::set>{path.points().front(), path.points().back()})); } diff --git a/test/unit/faceareatest.cpp b/test/unit/faceareatest.cpp new file mode 100644 index 000000000..28fbde07f --- /dev/null +++ b/test/unit/faceareatest.cpp @@ -0,0 +1,93 @@ +#include "path/face.h" +#include "path/path.h" +#include "path/pathvector.h" +#include "path/pathvectorview.h" +#include "testutil.h" +#include "gtest/gtest.h" + +#include "path/pathpoint.h" + +class TestCase : ommtest::PathVectorHeap +{ +public: + TestCase(std::unique_ptr pv, omm::Face face, const double area, const double eps) + : path_vector(annex(std::move(pv))), face(std::move(face)), area(area), eps(eps) + { + } + + omm::PathVector* path_vector; + omm::Face face; + double area; + double eps; + + friend std::ostream& operator<<(std::ostream& os, const TestCase& tc) + { + return os << tc.face; + } +}; + +class FaceArea : public ::testing::TestWithParam +{ +}; + +TEST_P(FaceArea, Area) +{ + const auto& tc = GetParam(); + ASSERT_TRUE(true); + // ASSERT_NEAR(std::abs(tc.face.area()), tc.area, tc.eps); +} + +TestCase rectangle(const omm::Vec2f& origin, const omm::Vec2f& size) +{ + auto pv = std::make_unique(); + auto& path = pv->add_path(); + const auto point = [pv = pv.get()](const auto& pos) { return std::make_shared(omm::Point(pos), pv); }; + auto points = std::vector{ + point(origin), + point(origin + omm::Vec2f{size.x, 0}), + point(origin + size), + point(origin + omm::Vec2f{0, size.y}), + }; + std::deque edges; + for (std::size_t i = 0; i < points.size(); ++i) { + auto& edge = path.add_edge(points.at(i), points.at((i + 1) % points.size())); + edges.emplace_back(&edge, omm::Direction::Forward); + } + static constexpr auto eps = 0.0001; + const auto area = size.x * size.y; + return {std::move(pv), omm::Face(omm::PathVectorView(std::move(edges))), area, eps}; +} + +TestCase ellipse(const std::size_t n, const omm::Vec2f& origin, const omm::Vec2f& radius) +{ + ommtest::EllipseMaker em{origin, radius, n, true, true}; + auto pv = std::make_unique(); + em.make_path(*pv); + const auto rel_eps = 2.0 / static_cast(n); + return {std::move(pv), omm::Face(omm::PathVectorView(*em.faces().begin())), em.area(), radius.x * radius.y * rel_eps}; +} + +// clang-format off +#define EXPAND_ELLIPSE(origin, size) \ + ellipse(11, origin, size), \ + ellipse(1007, origin, size), \ + ellipse(100, origin, size) + +const auto test_cases = ::testing::Values( + EXPAND_ELLIPSE(omm::Vec2f(0.0, 0.0), omm::Vec2f(1.0, 1.0)), + EXPAND_ELLIPSE(omm::Vec2f(3.0, -1.0), omm::Vec2f(0.01, 100.0)), + EXPAND_ELLIPSE(omm::Vec2f(-12.0, -12.0), omm::Vec2f(200.1234, 100.0)), + EXPAND_ELLIPSE(omm::Vec2f(-1000000000, -10000000000), omm::Vec2f(0.1234, 0.776543)), + rectangle({0.0, 0.0}, {1.0, 1.0}), + rectangle({0.0, 0.0}, {1.0, 1.0}), + rectangle({0.0, 0.0}, {1.0, 1.0}), + rectangle({0.0, 0.0}, {1.0, 1.0}), + rectangle({0.0, 0.0}, {1.0, 1.0}), + rectangle({0.0, 0.0}, {1.0, 1.0}), + rectangle({0.0, 0.0}, {1.0, 1.0}), + rectangle({14.0, -12.0}, {3.0, 10.0}) +); + +// clang-format on + +INSTANTIATE_TEST_SUITE_P(P, FaceArea, test_cases); diff --git a/test/unit/graphtest.cpp b/test/unit/graphtest.cpp new file mode 100644 index 000000000..2d1e637f2 --- /dev/null +++ b/test/unit/graphtest.cpp @@ -0,0 +1,540 @@ +#include "path/graph.h" +#include "fmt/format.h" +#include "geometry/point.h" +#include "path/dedge.h" +#include "path/edge.h" +#include "path/face.h" +#include "path/path.h" +#include "path/pathpoint.h" +#include "path/pathvector.h" +#include "path/pathvectorview.h" +#include "testutil.h" +#include "transform.h" +#include "gtest/gtest.h" +#include + +class TestCase : private ommtest::PathVectorHeap +{ +public: + TestCase(std::unique_ptr&& path_vector, std::set&& pvvs, + const std::size_t n_expected_components, std::string name, const bool is_planar = true) + : m_path_vector(annex(std::move(path_vector))) + , m_expected_faces(util::transform(std::move(pvvs))) + , m_n_expected_components(n_expected_components) + , m_name(std::move(name)) + , m_is_planar(is_planar) + { + } + + template TestCase(std::unique_ptr&& path_vector, std::set&& edgess, + const std::size_t n_expected_components, std::string name) + : TestCase(std::move(path_vector), util::transform(std::move(edgess)), n_expected_components, + std::move(name), true) + { + } + + TestCase(std::unique_ptr&& path_vector, const std::size_t n_expected_components, std::string name) + : TestCase(std::move(path_vector), {}, n_expected_components, std::move(name), false) + { + } + + [[nodiscard]] const omm::PathVector& path_vector() const + { + return *m_path_vector; + } + + [[nodiscard]] const std::set& expected_faces() const + { + return m_expected_faces; + } + + [[nodiscard]] bool is_planar() const + { + return m_is_planar; + } + + [[nodiscard]] TestCase add_arm(const std::size_t path_index, const std::size_t point_index, + std::vector geometries) && + { + const auto* const hinge = m_path_vector->paths().at(path_index)->points().at(point_index); + auto& arm = m_path_vector->add_path(); + auto last_point = m_path_vector->share(*hinge); + for (auto g : geometries) { + g.set_position(g.position() + hinge->geometry().position()); + auto next_point = std::make_shared(g, m_path_vector); + arm.add_edge(last_point, next_point); + last_point = next_point; + } + m_name += fmt::format("-arm[{}.{}-{}]", path_index, point_index, geometries.size()); + return std::move(*this); + } + + [[nodiscard]] TestCase add_loop(const std::size_t path_index, const std::size_t point_index, const double arg0, + const double arg1) && + { + const auto& src_path = *m_path_vector->paths().at(path_index); + auto& loop = m_path_vector->add_path(); + auto* const hinge = src_path.points().at(point_index); + hinge->geometry().set_tangent({&loop, omm::Direction::Forward}, omm::PolarCoordinates(arg0, 1.0)); + hinge->geometry().set_tangent({&loop, omm::Direction::Backward}, omm::PolarCoordinates(arg1, 1.0)); + auto shared_hinge = src_path.share(*hinge); + auto& loop_edge = loop.add_edge(shared_hinge, shared_hinge); + m_expected_faces.emplace(omm::PathVectorView({omm::DEdge::fwd(&loop_edge)})); + + static constexpr auto deg = M_1_PI * 180.0; + m_name += fmt::format("-loop[{}.{}-{}]", path_index, point_index, arg0 * deg, arg1 * deg); + return std::move(*this); + } + + [[nodiscard]] TestCase add_loops(const std::size_t path_index, const std::size_t point_index, const double arg0, + const double arg1, const std::size_t count) && + { + auto tc = std::move(*this); + const double advance = (arg1 - arg0) / static_cast(2 * count); + for (std::size_t i = 0; i < count; ++i) { + const double start = arg0 + 2 * i * advance; + const double end = arg0 + 2 * (i + 1) * advance; + tc = std::move(tc).add_loop(path_index, point_index, start, end); + } + return tc; + } + + [[nodiscard]] auto n_expected_components() const + { + return m_n_expected_components; + } + + friend std::ostream& operator<<(std::ostream& os, const TestCase& tc) + { + return os << tc.m_name; + } + +private: + omm::PathVector* m_path_vector; + std::set m_expected_faces; + std::size_t m_n_expected_components; + std::string m_name; + bool m_is_planar; +}; + +[[nodiscard]] TestCase empty_paths(const std::size_t path_count) +{ + auto pv = std::make_unique(); + for (std::size_t i = 0; i < path_count; ++i) { + pv->add_path(); + } + return {std::move(pv), {}, 0, fmt::format("{}-empty paths", path_count)}; +} + +[[nodiscard]] TestCase ellipse(ommtest::EllipseMaker ellipse_maker) +{ + auto pv = std::make_unique(); + ellipse_maker.make_path(*pv); + return {std::move(pv), ellipse_maker.faces(), 1, ellipse_maker.to_string()}; +} + +[[nodiscard]] TestCase rectangles(const std::size_t count) +{ + auto pv = std::make_unique(); + std::set> expected_pvvs; + + for (std::size_t i = 0; i < count; ++i) { + auto& path = pv->add_path(); + const auto p = [pv=pv.get()](const double x, const double y) { + return std::make_shared(omm::Point({x, y}), pv); + }; + + const double x = i * 3.0; + + std::deque edges; + path.set_single_point(p(x - 1.0, -1.0)); + edges.emplace_back(&path.add_edge(path.last_point(), p(x + 1.0, -1.0)), omm::Direction::Forward); + edges.emplace_back(&path.add_edge(path.last_point(), p(x + 1.0, 1.0)), omm::Direction::Forward); + edges.emplace_back(&path.add_edge(path.last_point(), p(x - 1.0, 1.0)), omm::Direction::Forward); + edges.emplace_back(&path.add_edge(path.last_point(), path.first_point()), omm::Direction::Forward); + expected_pvvs.emplace(std::move(edges)); + } + return {std::move(pv), std::move(expected_pvvs), count, fmt::format("{} Rectangles", count)}; +} + +[[nodiscard]] TestCase grid(const QSize& size, const QMargins& margins) +{ + auto pv = std::make_unique(); + std::vector>> points(size.height()); + for (int y = 0; y < size.height(); ++y) { + auto& row = points.at(y); + row.reserve(size.width()); + for (int x = 0; x < size.width(); ++x) { + const omm::Point geom({static_cast(x), static_cast(y)}); + row.emplace_back(std::make_shared(geom, pv.get())); + } + } + + std::deque h_paths; + for (int y = margins.top(); y < size.height() - margins.bottom(); ++y) { + auto& path = pv->add_path(); + for (int x = 1; x < size.width(); ++x) { + path.add_edge(points.at(y).at(x - 1), points.at(y).at(x)); + } + h_paths.emplace_back(&path); + } + + std::deque v_paths; + for (int x = margins.left(); x < size.width() - margins.right(); ++x) { + auto& path = pv->add_path(); + for (int y = 1; y < size.height(); ++y) { + path.add_edge(points.at(y - 1).at(x), points.at(y).at(x)); + } + v_paths.emplace_back(&path); + } + + std::set> expected_pvvs; + if (!h_paths.empty() && !v_paths.empty()) { + for (std::size_t x = 0; x < v_paths.size() - 1; ++x) { + for (std::size_t y = 0; y < h_paths.size() - 1; ++y) { + expected_pvvs.emplace(std::deque{omm::DEdge::fwd(h_paths.at(y + 0)->edges().at(x + margins.left())), + omm::DEdge::fwd(v_paths.at(x + 1)->edges().at(y + margins.top())), + omm::DEdge::fwd(h_paths.at(y + 1)->edges().at(x + margins.left())), + omm::DEdge::fwd(v_paths.at(x + 0)->edges().at(y + margins.top()))}); + } + } + } + + const auto name = [m = margins, size]() { + static constexpr auto fmt_m = [](const int value, const auto& name) { + return value == 0 ? "" : fmt::format("-{}={}", name, value); + }; + const auto s_ms = fmt_m(m.left(), "l") + fmt_m(m.right(), "r") + fmt_m(m.top(), "t") + fmt_m(m.bottom(), "b"); + return fmt::format("{}x{}-Grid{}", size.width(), size.height(), s_ms); + }; + + return {std::move(pv), std::move(expected_pvvs), 1, name()}; +} + +[[nodiscard]] TestCase leaf(std::vector counts) +{ + assert(counts.size() >= 2); + auto pv = std::make_unique(); + auto start = std::make_shared(omm::Point({1.0, 0.0}), pv.get()); + auto end = std::make_shared(omm::Point({0.0, 1.0}), pv.get()); + for (std::size_t i = 0; i < counts.size(); ++i) { + const double s = static_cast(i) / (static_cast(counts.size() - 1)); + auto& path = pv->add_path(); + auto last_point = start; + for (int j = 0; j < counts.at(i); ++j) { + const double arg = static_cast(j + 1) / static_cast(counts.at(i) + 1) * M_PI / 2.0; + const omm::Vec2f p(std::cos(arg), std::sin(arg)); + const omm::Vec2f q(1.0 - std::sin(arg), 1.0 - std::cos(arg)); + const auto x = std::lerp(p.x, q.x, s); + const auto y = std::lerp(p.y, q.y, s); + auto current = std::make_shared(omm::Point({x, y}), pv.get()); + path.add_edge(last_point, current); + last_point = current; + } + path.add_edge(last_point, end); + } + + std::set expected_pvvs; + for (std::size_t i = 1; i < counts.size(); ++i) { + const auto* const current = pv->paths()[i]; + const auto* const previous = pv->paths()[i - 1]; + std::deque edges; + for (auto* const e : current->edges()) { + edges.emplace_back(omm::DEdge::fwd(e)); + } + const auto previous_edges = previous->edges(); + for (auto it = previous_edges.rbegin(); it != previous_edges.rend(); ++it) { + edges.emplace_back(omm::DEdge::bwd(*it)); + } + expected_pvvs.emplace(edges); + } + + static constexpr auto format_count = [](const auto accu, const auto c) { return accu + fmt::format("-{}", c); }; + const auto s_counts = std::accumulate(counts.begin(), counts.end(), std::string{}, format_count); + + return {std::move(pv), std::move(expected_pvvs), 1, "leaf" + s_counts}; +} + +[[nodiscard]] TestCase blossom(const std::vector& segments, const double spacing) +{ + auto pv = std::make_unique(); + auto center = std::make_shared(omm::Point(), pv.get()); + std::set> expected_pvvs; + + const auto add_petal = + [¢er, &pv = *pv](const double start_angle, const double end_angle, const int subdivisions) -> auto& + { + auto& path = pv.add_path(); + const auto tangent_length = 2.0; + std::vector> points; + center->geometry().set_tangent({&path, omm::Direction::Forward}, + omm::PolarCoordinates(start_angle, tangent_length)); + center->geometry().set_tangent({&path, omm::Direction::Backward}, omm::PolarCoordinates(end_angle, tangent_length)); + if (subdivisions == 0) { + path.add_edge(center, center); + } else if (subdivisions == 1) { + const auto phi = (start_angle + end_angle) / 2.0; + const omm::PolarCoordinates intermediate_position(phi, tangent_length * 2.0); + omm::Point intermediate_geometry(intermediate_position.to_cartesian()); + const omm::PolarCoordinates tangent(phi + M_PI_2, tangent_length * 0.7); + intermediate_geometry.set_tangent({&path, omm::Direction::Forward}, tangent); + intermediate_geometry.set_tangent({&path, omm::Direction::Backward}, -tangent); + auto intermediate = std::make_shared(intermediate_geometry, &pv); + path.add_edge(center, intermediate); + path.add_edge(intermediate, center); + } else { + // More than one subdivision is not yet implemented. + exit(1); + } + return path; + }; + + for (std::size_t i = 0; i < segments.size(); ++i) { + const auto n = static_cast(segments.size()); + const auto start_angle = static_cast(i + spacing / 2.0) / n * 2.0 * M_PI; + const auto end_angle = static_cast(i + 1 - spacing / 2.0) / n * 2.0 * M_PI; + auto& path = add_petal(start_angle, end_angle, segments.at(i)); + expected_pvvs.insert( + util::transform(path.edges(), [](auto* const edge) { return omm::DEdge::fwd(edge); })); + } + + const auto name = + "blossom-" + + static_cast(util::transform(segments, [](const int n) { return QString("%1").arg(n); })) + .join("-") + + QString("_%1").arg(spacing); + return {std::move(pv), std::move(expected_pvvs), 1, name.toStdString()}; +} + +class GraphTest : public ::testing::TestWithParam +{ +}; + +TEST_P(GraphTest, FaceEqualityIsReflexive) +{ + for (const auto& face : GetParam().expected_faces()) { + ASSERT_EQ(face, face); + } +} + +TEST_P(GraphTest, FacesAreDistinct) +{ + const auto faces = GetParam().expected_faces(); + for (auto it1 = faces.begin(); it1 != faces.end(); ++it1) { + for (auto it2 = faces.begin(); it2 != faces.end(); ++it2) { + if (it1 != it2) { + ASSERT_NE(*it1, *it2); + ASSERT_NE(*it2, *it1); + } + } + } +} + +TEST_P(GraphTest, RotationReverseInvariance) +{ + for (bool reverse : {true, false}) { + for (const auto& face : GetParam().expected_faces()) { + const auto edges = face.path_vector_view().edges(); + for (std::size_t i = 0; i < edges.size(); ++i) { + auto rotated_edges = edges; + std::rotate(rotated_edges.begin(), std::next(rotated_edges.begin(), i), rotated_edges.end()); + if (reverse) { + std::reverse(rotated_edges.begin(), rotated_edges.end()); + } + const omm::Face rotated_face{omm::PathVectorView(rotated_edges)}; + ASSERT_EQ(face, rotated_face); + ASSERT_EQ(rotated_face, face); + } + } + } +} + +TEST_P(GraphTest, Normalization) +{ + const auto test_case = GetParam(); + for (auto face : GetParam().expected_faces()) { + const auto edges = face.path_vector_view().normalized(); + for (std::size_t i = 1; i < edges.size(); ++i) { + ASSERT_LT(edges.front(), edges.at(i)); + } + if (edges.size() > 2) { + ASSERT_LT(edges.at(1), edges.back()); + } + } +} + +TEST_P(GraphTest, ComputeFaces) +{ + static constexpr auto print_graph_into_svg = true; + + const auto& test_case = GetParam(); + + if constexpr (print_graph_into_svg) { + ommtest::Application app; + std::ostringstream oss; + oss << "/tmp/foo_" << ommtest::Application::test_id_for_filename().toStdString() << "_" << test_case << ".svg"; + const auto fname = QString::fromStdString(oss.str()); + test_case.path_vector().to_svg(fname); + LDEBUG << "save svg file " << fname; + } + + std::set actual_faces; + ASSERT_NO_THROW(actual_faces = test_case.path_vector().faces()); + if (test_case.is_planar()) { + // computing faces only makes sense if the graph is planar. + ASSERT_EQ(test_case.expected_faces(), actual_faces); + } +} + +TEST_P(GraphTest, ConnectedComponents) +{ + const auto& test_case = GetParam(); + EXPECT_EQ(omm::Graph(test_case.path_vector()).connected_components().size(), test_case.n_expected_components()); +} + +[[nodiscard]] std::vector linear_arm_geometry(const std::size_t length, const omm::Vec2f& direction) +{ + std::vector ps; + ps.reserve(length); + for (std::size_t i = 0; i < length; ++i) { + ps.emplace_back(omm::Point(static_cast(i + 1) * direction)); + } + return ps; +} + +[[nodiscard]] TestCase special_test(const std::size_t variant) +{ + using PC = omm::PolarCoordinates; + using D = omm::Direction; + auto pv = std::make_unique(); + auto& path_0 = pv->add_path(); + const auto make_point = [&path_0, &pv = *pv](const omm::Vec2f& pos) { + omm::Point geometry{pos}; + geometry.set_tangent({&path_0, omm::Direction::Backward}, PC{}); + geometry.set_tangent({&path_0, omm::Direction::Forward}, PC{}); + return std::make_unique(geometry, &pv); + }; + std::deque> points; + points.emplace_back(make_point({-75.5, -155.0})); + points.emplace_back(make_point({198.5, -183.0})); + points.emplace_back(make_point({281.5, -10.0})); + points.emplace_back(make_point({237.5, 129.0})); + points.emplace_back(make_point({-39.5, 83.0})); + + for (std::size_t i = 0; i < points.size(); ++i) { + path_0.add_edge(points.at(i), points.at((i + 1) % points.size())); + } + + auto& path_1 = pv->add_path(); + path_1.add_edge(points.at(1), points.at(4)); + + const std::deque face_0 = { + omm::DEdge::fwd(path_0.edges().at(1)), + omm::DEdge::fwd(path_0.edges().at(2)), + omm::DEdge::fwd(path_0.edges().at(3)), + omm::DEdge::bwd(path_1.edges().at(0)), + }; + const std::deque face_1 = { + omm::DEdge::fwd(path_0.edges().at(0)), + omm::DEdge::fwd(path_1.edges().at(0)), + omm::DEdge::fwd(path_0.edges().at(4)), + }; + const std::deque face_2 = { + omm::DEdge::bwd(path_0.edges().at(0)), omm::DEdge::bwd(path_0.edges().at(1)), + omm::DEdge::bwd(path_0.edges().at(2)), omm::DEdge::bwd(path_0.edges().at(3)), + omm::DEdge::bwd(path_0.edges().at(4)), + }; + const std::deque face_3 = { + omm::DEdge::fwd(path_0.edges().at(0)), + omm::DEdge::fwd(path_1.edges().at(0)), + omm::DEdge::fwd(path_0.edges().at(4)), + }; + + std::set> expected_faces; + switch (variant) { + case 0: + points.at(4)->geometry().set_tangent({&path_1, D::Backward}, PC{}); + points.at(4)->geometry().set_tangent({&path_0, D::Backward}, PC{1.0303768265243127, 99.12618221237013}); + points.at(4)->geometry().set_tangent({&path_0, D::Forward}, PC{-2.1112158270654806, 99.12618221237013}); + expected_faces = {face_0, face_1}; + break; + case 1: + points.at(4)->geometry().set_tangent({&path_1, D::Backward}, PC{-0.6675554919511357, 250.7695451381673}); + points.at(4)->geometry().set_tangent({&path_0, D::Backward}, PC{1.0303768265243127, 99.12618221237013}); + points.at(4)->geometry().set_tangent({&path_0, D::Forward}, PC{-2.1112158270654806, 99.12618221237013}); + expected_faces = {face_0, face_1}; + break; + case 2: + points.at(4)->geometry().set_tangent({&path_1, D::Backward}, PC{-0.6675554919511357, 350.7695451381673}); + points.at(4)->geometry().set_tangent({&path_0, D::Backward}, PC{1.0303768265243127, 99.12618221237013}); + points.at(4)->geometry().set_tangent({&path_0, D::Forward}, PC{-2.1112158270654806, 99.12618221237013}); + expected_faces.clear(); // the graph is not planar + break; + case 3: + points.at(1)->geometry().set_position({120.5, -143.0}); + points.at(1)->geometry().set_tangent({&path_1, D::Backward}, PC{1.2350632478379595, 274.2411547737723}); + points.at(1)->geometry().set_tangent({&path_1, D::Forward}, PC{0.09056247365766779, 0.0}); + points.at(1)->geometry().set_tangent({&path_1, D::Backward}, PC{0.015592141134032511, 355.6724823321008}); + points.at(1)->geometry().set_tangent({&path_1, D::Forward}, PC{-2.921946505938993, 570.3718878667855}); + expected_faces = {face_1, face_2}; + break; + default: + assert(false); + } + + const bool is_planar = variant != 2; + if (is_planar) { + return {std::move(pv), std::move(expected_faces), 1, "special-test-" + std::to_string(variant)}; + } + return {std::move(pv), 1, "special-test-" + std::to_string(variant)}; +} + +// clang-format off +#define EXPAND_ELLIPSE(N, ext) \ + ellipse(ommtest::EllipseMaker{{0.0, 0.0}, {1.0, 1.0}, N, true, true}) ext, \ + ellipse(ommtest::EllipseMaker{{0.0, 0.0}, {1.0, 1.0}, N, false, true}) ext, \ + ellipse(ommtest::EllipseMaker{{0.0, 0.0}, {1.0, 1.0}, N, true, false}) ext, \ + ellipse(ommtest::EllipseMaker{{0.0, 0.0}, {1.0, 1.0}, N, false, false}) ext + +const auto test_cases = ::testing::Values( + empty_paths(0), + empty_paths(1), + empty_paths(10), + rectangles(1), + rectangles(3), + rectangles(10), + rectangles(1).add_arm(0, 0, linear_arm_geometry(3, {-1.0, 0.0})), + rectangles(2).add_arm(1, 1, linear_arm_geometry(3, {-1.0, -1.0})), + EXPAND_ELLIPSE(3, ), + EXPAND_ELLIPSE(4, ), + EXPAND_ELLIPSE(2, .add_arm(0, 1, linear_arm_geometry(3, {-1.0, -1.0}))), + EXPAND_ELLIPSE(4, .add_arm(0, 1, linear_arm_geometry(3, {-1.0, -1.0}))), + EXPAND_ELLIPSE(4, .add_loop(0, 2, M_PI - 1.0, M_PI + 1.0)), + EXPAND_ELLIPSE(4, .add_loops(0, 2, M_PI - 1.0, M_PI + 1.0, 2)), + EXPAND_ELLIPSE(4, .add_loops(0, 2, M_PI - 1.0, M_PI + 1.0, 6)), + EXPAND_ELLIPSE(8, ), + EXPAND_ELLIPSE(100, .add_arm(0, 2, linear_arm_geometry(2, {0.0, 1.0}))), + grid({2, 3}, QMargins{}), + grid({4, 4}, QMargins{}), + grid({8, 7}, QMargins{1, 2, 2, 3}), + leaf({2, 1, 5}), + leaf({1, 3}), + leaf({2, 1, 1, 2}), + blossom({0, 0}, 0.5), + blossom({0, 1}, 0.5), + blossom({1, 1}, 0.5), + blossom({0, 0, 0}, 0.0), + blossom({0, 1, 0}, 0.0), + blossom({1, 1, 1}, 0.0), + blossom({0, 0, 0}, 0.1), + blossom({0, 0, 0}, 0.9), + blossom({0, 0, 0, 0, 0, 0, 0}, 0.0), + blossom({0, 1, 1, 0, 1, 0, 0}, 0.0), + special_test(0), + special_test(1), + special_test(2), + special_test(3) +); +// clang-format on + +INSTANTIATE_TEST_SUITE_P(P, GraphTest, test_cases); diff --git a/test/unit/icon.cpp b/test/unit/icon.cpp index 61bcdc93f..b075f4a5e 100644 --- a/test/unit/icon.cpp +++ b/test/unit/icon.cpp @@ -1,7 +1,6 @@ #include "config.h" #include "gtest/gtest.h" #include "main/application.h" -#include "main/options.h" #include "mainwindow/exporter.h" #include "objects/view.h" #include "scene/scene.h" @@ -20,11 +19,6 @@ class NonUniqueException : public std::runtime_error using std::runtime_error::runtime_error; }; -auto options() -{ - return std::make_unique(false, false); -} - auto& find_unique_item(omm::Scene& scene, const QString& name) { const auto items = scene.find_items(name); @@ -76,8 +70,7 @@ class Icon : public ::testing::TestWithParam { protected: Icon() - : m_app(ommtest::Application(options())) - , m_scene(*m_app.omm_app().scene) + : m_scene(*m_app.omm_app().scene) { } diff --git a/test/unit/knifetest.cpp b/test/unit/knifetest.cpp new file mode 100644 index 000000000..4f88b0238 --- /dev/null +++ b/test/unit/knifetest.cpp @@ -0,0 +1,85 @@ +#include "commands/cutpathcommand.h" +#include "objects/pathobject.h" +#include "path/path.h" +#include "path/pathpoint.h" +#include "path/pathvector.h" +#include "gtest/gtest.h" +#include + +struct KnifeTestCase +{ + omm::PathVector geometry_before; + omm::PathVector geometry_after; + std::vector cuts; + std::string name; +}; + +std::ostream& operator<<(std::ostream& os, const KnifeTestCase& test_case) +{ + return os << test_case.name; +} + +KnifeTestCase cut_straight_line(const std::size_t point_count, const double t) +{ + static constexpr auto geometry = [](const double t) { return omm::Point({0.0, t}); }; + static constexpr auto generate_points = [](const std::size_t point_count, omm::PathVector& path_vector) { + std::vector> points; + points.reserve(point_count); + for (std::size_t i = 0; i < point_count; ++i) { + const auto t = static_cast(i) / (static_cast(point_count) - 1.0); + points.emplace_back(std::make_shared(geometry(t), &path_vector)); + } + return points; + }; + + KnifeTestCase test_case; + const auto points_before = generate_points(point_count, test_case.geometry_before); + const auto points_after = generate_points(point_count, test_case.geometry_after); + + auto& path_before = test_case.geometry_before.add_path(); + auto& path_after = test_case.geometry_after.add_path(); + + for (std::size_t i = 1; i < point_count; ++i) { + path_before.add_edge(points_before.at(i - 1), points_before.at(i)); + + const double lower = static_cast(i - 1) / static_cast(point_count); + const double upper = static_cast(i) / static_cast(point_count); + + if (lower < t && t <= upper) { + auto extra = std::make_shared(geometry(t), &test_case.geometry_after); + path_after.add_edge(points_after.at(i - 1), extra); + path_after.add_edge(extra, points_after.at(i)); + } else { + path_after.add_edge(points_after.at(i - 1), points_after.at(i)); + } + } + + test_case.cuts = {}; // TODO + test_case.name = fmt::format("Straight line with {} points, cut at {}", point_count, t); + return test_case; +} + +class KnifeTest : public ::testing::TestWithParam +{ +}; + +TEST_P(KnifeTest, KnifeCommand) +{ + const auto& test_case = GetParam(); + omm::PathObject path_object(nullptr, test_case.geometry_before); + const auto path_object_geometry = std::make_unique(*path_object.compute_geometry()); + omm::CutPathCommand cut_command(path_object, test_case.cuts); + + cut_command.redo(); + + ASSERT_EQ(path_object.geometry(), test_case.geometry_after); + + cut_command.undo(); + + EXPECT_EQ(path_object.geometry(), *path_object_geometry); +} + +INSTANTIATE_TEST_SUITE_P(KnifeCommandTests, KnifeTest, + ::testing::ValuesIn(std::vector{ + cut_straight_line(10, 0.5), + })); diff --git a/test/unit/linetest.cpp b/test/unit/linetest.cpp new file mode 100644 index 000000000..2cf0847eb --- /dev/null +++ b/test/unit/linetest.cpp @@ -0,0 +1,130 @@ +#include "geometry/line.h" +#include "fmt/format.h" +#include "geometry/vec2.h" +#include "gtest/gtest.h" + +class LineIntersectionTestCase +{ +public: + omm::Line a; + omm::Line b; + double expected_t; + double expected_u; + friend std::ostream& operator<<(std::ostream& os, const LineIntersectionTestCase& tc) + { + return os << tc.a << "-" << tc.b; + } +}; + +class LineIntersectionTest : public ::testing::TestWithParam +{ +}; + +TEST_P(LineIntersectionTest, LineIntersection) +{ + static constexpr auto eps = 0.001; + + const auto tcase = GetParam(); + const auto t = tcase.a.intersect(tcase.b); + const auto u = tcase.b.intersect(tcase.a); + + EXPECT_NEAR(tcase.a.lerp(t).x, tcase.b.lerp(u).x, eps); + EXPECT_NEAR(tcase.a.lerp(t).y, tcase.b.lerp(u).y, eps); + + EXPECT_NEAR(t, tcase.expected_t, eps); + EXPECT_NEAR(u, tcase.expected_u, eps); +} + +// clang-format off + +using LITC = LineIntersectionTestCase; +const auto litc_test_cases = ::testing::Values( + LITC{{{0.0, 0.0}, {1.0, 1.0}}, {{0.0, 1.0}, {1.0, 0.0}}, 0.5, 0.5}, + LITC{{{0.0, 0.0}, {2.0, 2.0}}, {{0.0, 1.0}, {1.0, 0.0}}, 0.25, 0.5}, + LITC{{{0.0, 0.0}, {1.0, 0.0}}, {{0.5, 0.0}, {0.5, 1.0}}, 0.5, 0.0}, + LITC{{{0.0, 0.0}, {0.0, 1.0}}, {{-2.0, 0.3}, {6.0, 0.3}}, 0.3, 0.25} +); + +// clang-format on + +INSTANTIATE_TEST_SUITE_P(P, LineIntersectionTest, litc_test_cases); + +class LineDistanceTestCase +{ +public: + omm::Line line; + omm::Vec2f point; + double expected_distance; + friend std::ostream& operator<<(std::ostream& os, const LineDistanceTestCase& tc) + { + return os << tc.line << "-" << fmt::format("Point[{}, {}]", tc.point.x, tc.point.y); + } +}; + +class LineDistanceTest : public ::testing::TestWithParam +{ +}; + +TEST_P(LineDistanceTest, LineDistance) +{ + static constexpr auto eps = 0.001; + + const auto tcase = GetParam(); + EXPECT_NEAR(tcase.line.distance(tcase.point), tcase.expected_distance, eps); + EXPECT_NEAR(tcase.line.distance(tcase.line.a), 0.0, eps); + EXPECT_NEAR(tcase.line.distance(tcase.line.b), 0.0, eps); +} + +// clang-format off + +using LDTC = LineDistanceTestCase; +const auto ldtc_test_cases = ::testing::Values( + LDTC{{{0.0, 0.0}, {1.0, 1.0}}, {0.0, 0.0}, 0.0}, + LDTC{{{0.0, 0.0}, {0.0, 2.0}}, {1.0, 10.0}, 1.0}, + LDTC{{{0.0, 0.0}, {2.0, 0.0}}, {-99.1234, -3}, 3.0}, + LDTC{{{0.0, 0.0}, {1.0, 1.0}}, {0.0, 1.0}, -std::sqrt(0.5)} +); + +// clang-format on + +INSTANTIATE_TEST_SUITE_P(P, LineDistanceTest, ldtc_test_cases); + +class LineProjectionTestCase +{ +public: + omm::Line line; + omm::Vec2f point; + double expected_t; + friend std::ostream& operator<<(std::ostream& os, const LineProjectionTestCase& tc) + { + return os << tc.line << "-" << fmt::format("Point[{}, {}]", tc.point.x, tc.point.y); + } +}; + +class LineProjectionTest : public ::testing::TestWithParam +{ +}; + +TEST_P(LineProjectionTest, LineProjection) +{ + static constexpr auto eps = 0.001; + + const auto tcase = GetParam(); + EXPECT_NEAR(tcase.line.project(tcase.point), tcase.expected_t, eps); + EXPECT_NEAR(tcase.line.project(tcase.line.a), 0.0, eps); + EXPECT_NEAR(tcase.line.project(tcase.line.b), 1.0, eps); +} + +// clang-format off + +using LPTC = LineProjectionTestCase; +const auto lptc_test_cases = ::testing::Values( + LPTC{{{0.0, 0.0}, {1.0, 1.0}}, {1.0, 0.0}, 0.5}, + LPTC{{{0.0, 0.0}, {1.0, 1.0}}, {0.0, 0.3}, 0.15}, + LPTC{{{0.0, 0.0}, {0.0, 2.0}}, {3.1415, 0.468}, 0.234}, + LPTC{{{1.0, 2.0}, {3.0, 4.0}}, {5.0, 6.0}, 2.0} +); + +// clang-format on + +INSTANTIATE_TEST_SUITE_P(P, LineProjectionTest, lptc_test_cases); diff --git a/test/unit/nodetest.cpp b/test/unit/nodetest.cpp index 8b33e1b0b..d1ae52623 100644 --- a/test/unit/nodetest.cpp +++ b/test/unit/nodetest.cpp @@ -1,6 +1,5 @@ #include "gtest/gtest.h" #include "main/application.h" -#include "main/options.h" #include "nodesystem/nodecompilerglsl.h" #include "nodesystem/nodecompilerpython.h" #include "nodesystem/nodemodel.h" @@ -21,20 +20,12 @@ namespace { -std::unique_ptr options() -{ - return std::make_unique(false, // is_cli - false // have_opengl - ); -} - template class NodeTestFixture { public: NodeTestFixture() - : m_q_app(options()) - , m_model(omm::nodes::NodeModel(Compiler::LANGUAGE, m_q_app.omm_app().scene.get())) + : m_model(omm::nodes::NodeModel(Compiler::LANGUAGE, m_q_app.omm_app().scene.get())) , m_compiler(m_model) { } diff --git a/test/unit/pathtest.cpp b/test/unit/pathtest.cpp index 220ac9583..45601ef7e 100644 --- a/test/unit/pathtest.cpp +++ b/test/unit/pathtest.cpp @@ -1,171 +1,676 @@ -#include "gtest/gtest.h" -#include "path/pathvector.h" #include "path/path.h" -#include "path/graph.h" +#include "commands/addpointscommand.h" +#include "commands/ownedlocatedpath.h" +#include "commands/removepointscommand.h" +#include "geometry/point.h" #include "path/edge.h" -#include "path/face.h" #include "path/pathpoint.h" -#include "scene/disjointpathpointsetforest.h" +#include "path/pathvector.h" +#include "path/pathview.h" +#include "gtest/gtest.h" + +#include + +#include namespace { -class EdgeLoop +bool check_correspondence(const auto& a, const std::vector& a_indices, + const auto& b, const std::vector& b_indices) +{ + assert(a_indices.size() == b_indices.size()); + for (std::size_t i = 0; i < a_indices.size(); ++i) { + if (a.at(a_indices.at(i)) != b.at(b_indices.at(i))) { + return false; + } + } + return true; +} + +template +class UndoRedoStackMock { public: - explicit EdgeLoop(const std::size_t n) - : m_path(m_path_vector.add_path(std::make_unique())) - , m_edges(create_edge_loop(n)) + using Status = Status_; + struct StackItem + { + explicit StackItem(std::unique_ptr&& command, Status&& before) + : command(std::move(command)) + , before(std::move(before)) + { + } + + std::unique_ptr command; + Status before; + std::optional after; + }; + + const StackItem& submit(std::unique_ptr command) + { + m_stack.erase(m_stack.begin() + m_index, m_stack.end()); + auto& current = m_stack.emplace_back(std::move(command), status()); + redo(); + current.after = status(); + return current; + } + + virtual Status status() const = 0; + + void redo() + { + const auto& current = m_stack.at(m_index); + ASSERT_EQ(current.before, status()); + current.command->redo(); + if (current.after.has_value()) { + // this status does not yet exist if redo is called from submit. + ASSERT_EQ(*current.after, status()); + } + m_index += 1; + } + + void undo() { + m_index -= 1; + const auto& current = m_stack.at(m_index); + ASSERT_EQ(current.after, status()); + current.command->undo(); + ASSERT_EQ(current.before, status()); } - const auto& edges() const { return m_edges; } + void undo_all() + { + while (m_index > 0) { + ASSERT_NO_FATAL_FAILURE(undo()); + } + } + + void redo_all() + { + while (m_index < m_stack.size()) { + ASSERT_NO_FATAL_FAILURE(redo()); + } + } private: - omm::PathVector m_path_vector; - omm::Path& m_path; - const std::deque m_edges; + std::size_t m_index = 0; + std::deque m_stack; +}; + +class PathCommandUndoRedoStackMock : public UndoRedoStackMock> +{ +public: + explicit PathCommandUndoRedoStackMock(const omm::Path& path) + : m_path(path) + { + } - std::deque create_edge_loop(const std::size_t n) const + Status status() const override { - assert(n >= 2); - std::deque edges(n); + return m_path.points(); + } + +private: + const omm::Path& m_path; +}; + +class PathCommandTest : public ::testing::Test +{ +protected: + explicit PathCommandTest() + : m_path(m_path_vector.add_path()) + , m_stack(m_path) + { + } + + void SetUp() + { + ASSERT_EQ(m_path_vector.paths().size(), 1); + EXPECT_EQ(m_path_vector.paths().front(), &m_path); + EXPECT_EQ(m_path.points().size(), 0); + } + + const auto& submit_add_n_points_command(const std::size_t offset = 0, const std::size_t n = 1) + { + std::deque> points; for (std::size_t i = 0; i < n; ++i) { - edges.at(i).a = &m_path.add_point({}); - edges.at((i + 1) % n).b = edges.at(i).a; + points.emplace_back(std::make_unique(omm::Point{}, m_path.path_vector())); } - return edges; + return m_stack.submit(std::make_unique(omm::OwnedLocatedPath{&m_path, offset, points})); + } + + const auto& submit_remove_point_command(const std::size_t offset = 0, const std::size_t n = 1) + { + return m_stack.submit(std::make_unique(omm::PathView(m_path, offset, n))); } + + const auto& submit_add_edge_command() + { + std::deque> points; + points.emplace_back(std::make_unique(omm::Point{}, m_path.path_vector())); + points.emplace_back(std::make_unique(omm::Point{}, m_path.path_vector())); + return m_stack.submit(std::make_unique(omm::OwnedLocatedPath(&m_path, 0, points))); + } + + omm::Path& path() const + { + return m_path; + } + + bool has_distinct_points() const + { + const auto points = m_path.points(); + return std::set(points.begin(), points.end()).size() == points.size(); + } + + PathCommandUndoRedoStackMock& stack() + { + return m_stack; + } + +private: + omm::PathVector m_path_vector; + omm::Path& m_path; + PathCommandUndoRedoStackMock m_stack; }; -auto make_face(const omm::PathVector& pv, const std::vector>& indices) +struct RemoveAddPointsCommandTestParameter { - omm::Face face; - for (const auto& [ai, bi] : indices) { - omm::Edge edge; - edge.a = &pv.point_at_index(ai); - edge.b = &pv.point_at_index(bi); - face.add_edge(edge); + using Info = ::testing::TestParamInfo; + const std::size_t initial_point_count; + const std::size_t offset; + const std::size_t count; +protected: + static std::string name_generator(const Info& info, std::string_view what); +}; + +struct RemovePointsCommandTestParameter : RemoveAddPointsCommandTestParameter +{ + static std::string name_generator(const Info& info) + { + return RemoveAddPointsCommandTestParameter::name_generator(info, "remove"); } - return face; +}; + +struct AddPointsCommandTestParameter : RemoveAddPointsCommandTestParameter +{ + static std::string name_generator(const Info& info) + { + return RemoveAddPointsCommandTestParameter::name_generator(info, "add"); + } +}; + +class RemovePointsCommandTest + : public PathCommandTest + , public ::testing::WithParamInterface +{ +}; + +std::string RemoveAddPointsCommandTestParameter::name_generator(const Info& info, const std::string_view what) +{ + const auto begin = info.param.offset; + const auto end = begin + info.param.count - 1; + const auto n = info.param.initial_point_count; + return fmt::format("{}_points_{}_to_{}_from_{}_point_path", what, begin, end, n); } -omm::Face create_face(const std::deque& edges, const int offset, const bool reverse) +class AddPointsCommandTest + : public PathCommandTest + , public ::testing::WithParamInterface +{ +}; + +class Range { - std::deque es; - std::rotate_copy(edges.begin(), edges.begin() + offset, edges.end(), std::back_insert_iterator(es)); - if (reverse) { - std::reverse(es.begin(), es.end()); +public: + Range(std::size_t begin, std::size_t end, const std::vector& ranges = {}) + : begin(begin), end(end), ranges(ranges) + { } - omm::Face face; - for (const auto& edge : es) { - static constexpr auto r = [](const omm::Edge& e) { - omm::Edge r; - r.a = e.b; - r.b = e.a; - return r; - }; - face.add_edge(reverse ? r(edge) : edge); + + operator std::vector() const + { + std::list items; + for (std::size_t i = begin; i < end; ++i) { + items.push_back(i); + } + for (const auto& r : ranges) { + const auto vs = static_cast>(r); + items.insert(items.end(), vs.begin(), vs.end()); + } + return std::vector(items.begin(), items.end()); + } + + friend Range operator+(const Range& a, const Range& b) + { + return Range{0, 0, {a, b}}; } - return face; -} + + const std::size_t begin; + const std::size_t end; + std::vector ranges; +}; } // namespace -TEST(Path, FaceAddEdge) +TEST_F(PathCommandTest, AddSinglePoint) { - const EdgeLoop loop(4); + ASSERT_NO_FATAL_FAILURE(submit_add_n_points_command()); + ASSERT_EQ(path().points().size(), 1); + ASSERT_NO_FATAL_FAILURE(stack().undo()); + ASSERT_TRUE(path().points().empty()); + ASSERT_NO_FATAL_FAILURE(stack().redo()); +} - static constexpr auto expect_true_perms = { - std::array{0, 1, 2, 3}, - std::array{1, 2, 3, 0}, - std::array{3, 2, 1, 0}, - std::array{2, 1, 0, 3}, - }; +TEST_F(PathCommandTest, AddTwoPoints) +{ + ASSERT_NO_FATAL_FAILURE(submit_add_edge_command()); + ASSERT_TRUE(has_distinct_points()); + ASSERT_EQ(path().points().size(), 2); + ASSERT_NO_FATAL_FAILURE(stack().undo()); + ASSERT_NO_FATAL_FAILURE(stack().redo()); +} + +TEST_F(PathCommandTest, AddThreePointsFrontOneByOne) +{ + ASSERT_NO_FATAL_FAILURE(submit_add_n_points_command()); + ASSERT_EQ(path().points().size(), 1); + + ASSERT_NO_FATAL_FAILURE(submit_add_n_points_command()); + ASSERT_EQ(path().points().size(), 2); + ASSERT_TRUE(has_distinct_points()); - for (const auto permutation : expect_true_perms) { - omm::Face face; - for (const std::size_t i : permutation) { - EXPECT_TRUE(face.add_edge(loop.edges().at(i))); + ASSERT_NO_FATAL_FAILURE(submit_add_n_points_command()); + ASSERT_EQ(path().points().size(), 3); + ASSERT_TRUE(has_distinct_points()); + + ASSERT_NO_FATAL_FAILURE(stack().undo_all()); + ASSERT_TRUE(path().points().empty()); + ASSERT_NO_FATAL_FAILURE(stack().redo_all()); + ASSERT_EQ(path().points().size(), 3); +} + +TEST_F(PathCommandTest, AddPointsMiddle_A) +{ + ASSERT_NO_FATAL_FAILURE(submit_add_n_points_command(0, 4)); + ASSERT_TRUE(has_distinct_points()); + const auto four_points = path().points(); + ASSERT_EQ(four_points.size(), 4); + + ASSERT_NO_FATAL_FAILURE(submit_add_n_points_command(0, 1)); + ASSERT_TRUE(has_distinct_points()); + const auto five_points = path().points(); + ASSERT_EQ(five_points.size(), 5); + ASSERT_TRUE(check_correspondence(five_points, {1, 2, 3, 4}, + four_points, {0, 1, 2, 3})); + + ASSERT_NO_FATAL_FAILURE(submit_add_n_points_command(1, 1);); + ASSERT_TRUE(has_distinct_points()); + const auto six_points = path().points(); + ASSERT_EQ(six_points.size(), 6); + ASSERT_TRUE(check_correspondence(six_points, {0, 2, 3, 4, 5}, + five_points, {0, 1, 2, 3, 4})); + + ASSERT_NO_FATAL_FAILURE(submit_add_n_points_command(2, 3);); + ASSERT_TRUE(has_distinct_points()); + const auto nine_points = path().points(); + EXPECT_EQ(nine_points.size(), 9); + ASSERT_TRUE(check_correspondence(nine_points, {0, 1, 5, 6, 7, 8}, + six_points, {0, 1, 2, 3, 4, 5})); + + ASSERT_NO_FATAL_FAILURE(stack().undo_all()); + ASSERT_TRUE(path().points().empty()); + ASSERT_NO_FATAL_FAILURE(stack().redo_all()); + ASSERT_EQ(path().points().size(), 9); +} + +TEST_F(PathCommandTest, AddPointsMiddle_B) +{ + ASSERT_NO_FATAL_FAILURE(submit_add_n_points_command(0, 4)); + const auto four_points = path().points(); + ASSERT_EQ(four_points.size(), 4); + + const auto& b = submit_add_n_points_command(2, 3); + const auto seven_points = path().points(); + ASSERT_EQ(seven_points.size(), 7); + ASSERT_TRUE(check_correspondence(four_points, {0, 1, 2, 3}, seven_points, {0, 1, 5, 6})); + ASSERT_EQ(dynamic_cast(*b.command).owned_edges().size(), 1); + + ASSERT_NO_FATAL_FAILURE(stack().undo()); + ASSERT_NO_FATAL_FAILURE(stack().redo()); +} + +TEST_P(RemovePointsCommandTest, RemovePoints) +{ + const auto p = GetParam(); + ASSERT_NO_FATAL_FAILURE(submit_add_n_points_command(0, p.initial_point_count)); + const auto initial_points = path().points(); + ASSERT_EQ(initial_points.size(), p.initial_point_count); + ASSERT_NO_FATAL_FAILURE(submit_remove_point_command(p.offset, p.count)); + const auto final_points = path().points(); + ASSERT_EQ(final_points.size(), p.initial_point_count - p.count); + ASSERT_TRUE(check_correspondence(initial_points, + Range(0, p.offset) + Range(p.offset + p.count, p.initial_point_count), + final_points, + Range(0, final_points.size()))); + ASSERT_NO_FATAL_FAILURE(stack().undo()); + ASSERT_NO_FATAL_FAILURE(stack().redo()); +} + +INSTANTIATE_TEST_SUITE_P(X, RemovePointsCommandTest, + ::testing::ValuesIn(std::vector{ + {.initial_point_count = 1, .offset = 0, .count = 1}, + {.initial_point_count = 2, .offset = 0, .count = 1}, + {.initial_point_count = 2, .offset = 1, .count = 1}, + {.initial_point_count = 3, .offset = 0, .count = 1}, + {.initial_point_count = 3, .offset = 1, .count = 1}, + {.initial_point_count = 3, .offset = 2, .count = 1}, + {.initial_point_count = 2, .offset = 0, .count = 2}, + {.initial_point_count = 3, .offset = 0, .count = 2}, + {.initial_point_count = 3, .offset = 1, .count = 2}, + {.initial_point_count = 5, .offset = 0, .count = 5}, + {.initial_point_count = 5, .offset = 0, .count = 2}, + {.initial_point_count = 5, .offset = 1, .count = 2}, + {.initial_point_count = 5, .offset = 3, .count = 2} + }), + &RemovePointsCommandTestParameter::name_generator); + +TEST_P(AddPointsCommandTest, AddPoints) +{ + const auto p = GetParam(); + if (p.initial_point_count > 0) { + ASSERT_NO_FATAL_FAILURE(submit_add_n_points_command(0, p.initial_point_count)); + } + const auto initial_points = path().points(); + ASSERT_EQ(initial_points.size(), p.initial_point_count); + LINFO << "before: " << path().print_edge_info(); + ASSERT_NO_FATAL_FAILURE(submit_add_n_points_command(p.offset, p.count)); + const auto final_points = path().points(); + ASSERT_EQ(final_points.size(), p.initial_point_count + p.count); + LINFO << "after: " << path().print_edge_info(); + ASSERT_TRUE(check_correspondence(initial_points, Range(0, p.initial_point_count), + final_points, Range(0, p.offset) + Range(p.offset + p.count, final_points.size()))); + ASSERT_NO_FATAL_FAILURE(stack().undo()); + ASSERT_NO_FATAL_FAILURE(stack().redo()); +} + +INSTANTIATE_TEST_SUITE_P(X, AddPointsCommandTest, + ::testing::ValuesIn(std::vector{ + {.initial_point_count = 0, .offset = 0, .count = 1}, + {.initial_point_count = 0, .offset = 0, .count = 2}, + {.initial_point_count = 0, .offset = 0, .count = 3}, + // TODO one initial point? + {.initial_point_count = 2, .offset = 0, .count = 1}, + {.initial_point_count = 2, .offset = 1, .count = 1}, + {.initial_point_count = 2, .offset = 2, .count = 1}, + {.initial_point_count = 2, .offset = 0, .count = 2}, + {.initial_point_count = 2, .offset = 1, .count = 2}, + {.initial_point_count = 2, .offset = 2, .count = 2}, + {.initial_point_count = 2, .offset = 0, .count = 3}, + {.initial_point_count = 2, .offset = 1, .count = 3}, + {.initial_point_count = 2, .offset = 2, .count = 3}, + {.initial_point_count = 3, .offset = 0, .count = 2}, + {.initial_point_count = 3, .offset = 1, .count = 2}, + {.initial_point_count = 3, .offset = 2, .count = 2}, + {.initial_point_count = 3, .offset = 3, .count = 2}, + }), + &AddPointsCommandTestParameter::name_generator); + +class PathVectorCopy : public ::testing::Test +{ +public: + explicit PathVectorCopy() + { + } + + omm::PathVector& make_copy() + { + pv_copy = std::make_unique(pv_original); + return *pv_copy; + } + + void assert_valid() const + { + ASSERT_NO_FATAL_FAILURE(assert_valid_tangents(*pv_copy)); + ASSERT_NO_FATAL_FAILURE(assert_valid_tangents(pv_original)); + ASSERT_NO_FATAL_FAILURE(assert_equiv_but_distinct()); + } + +private: + static void assert_valid_tangents(const omm::PathVector& path_vector) + { + for (const auto* path : path_vector.paths()) { + for (const auto* const p : path->points()) { + for (const auto& [key, tangent] : p->geometry().tangents()) { + ASSERT_TRUE(key.path == path || key.path == nullptr) << "key.path: " << key.path << ", path: " << path; + } + } } } - // It adding the third edge is expected to fail because there's no way to orient it such that it - // has a common point with the previous edge. - // Hence, adding a fourth edge is not required. - static constexpr auto expect_false_perms = { - std::array{0, 1, 3}, - std::array{2, 1, 3}, - std::array{1, 2, 0}, - std::array{1, 0, 2}, - }; + static void assert_equiv_but_distinct(const omm::Path& a, const omm::Path& b) + { + ASSERT_NE(&a, &b) << "Identical paths expected to be distinguishable."; + const auto ps_a = a.points(); + const auto ps_b = b.points(); + ASSERT_EQ(ps_a.size(), ps_b.size()); + for (std::size_t i = 0; i < ps_a.size(); ++i) { + const auto& a = *ps_a.at(i); + const auto& b = *ps_b.at(i); + ASSERT_NE(&a, &b) << "Identical path points expected to be distinguishable."; + ASSERT_NE(&a.geometry(), &b.geometry()) << "Geometry shall not be shared among path points."; + ASSERT_EQ(a.geometry().position(), b.geometry().position()); + ASSERT_EQ(a.geometry().tangents().size(), b.geometry().tangents().size()); + } + } - for (const auto permutation : expect_false_perms) { - omm::Face face; - for (std::size_t k = 0; k < permutation.size() - 1; ++k) { - EXPECT_TRUE(face.add_edge(loop.edges().at(permutation.at(k)))); + void assert_equiv_but_distinct() const + { + const auto a_paths = pv_original.paths(); + const auto b_paths = pv_copy->paths(); + ASSERT_EQ(a_paths.size(), b_paths.size()); + for (std::size_t i = 0; i < a_paths.size(); ++i) { + ASSERT_NO_FATAL_FAILURE(assert_equiv_but_distinct(*a_paths.at(i), *b_paths.at(i))); } - EXPECT_FALSE(face.add_edge(loop.edges().at(permutation.back()))); } + +protected: + omm::PathVector pv_original; + std::unique_ptr pv_copy; +}; + +TEST_F(PathVectorCopy, one_edge) +{ + auto pv1_p0 = std::make_shared(omm::Point({0, 0}), &pv_original); + auto pv1_p1 = std::make_shared(omm::Point({0, 1}), &pv_original); + auto& pv1_path = pv_original.add_path(); + pv1_path.add_edge(std::make_unique(pv1_p0, pv1_p1, &pv1_path)); + pv1_p0->geometry().set_tangent({&pv1_path, omm::Direction::Backward}, omm::PolarCoordinates()); + pv1_p1->geometry().set_tangent({&pv1_path, omm::Direction::Forward}, omm::PolarCoordinates()); + + ASSERT_NO_THROW(make_copy()); + + ASSERT_NO_FATAL_FAILURE(assert_valid()); } -TEST(Path, FaceEquality) +TEST_F(PathVectorCopy, one_point) { - EdgeLoop loop(4); - for (std::size_t i = 0; i < loop.edges().size(); ++i) { - EXPECT_EQ(create_face(loop.edges(), 0, false), create_face(loop.edges(), i, false)); - EXPECT_EQ(create_face(loop.edges(), 0, true), create_face(loop.edges(), i, false)); - EXPECT_EQ(create_face(loop.edges(), i, true), create_face(loop.edges(), 0, true)); - } + auto pv1_p0 = std::make_shared(omm::Point({0, 0}), &pv_original); + auto& pv1_path = pv_original.add_path(); + pv1_path.set_single_point(pv1_p0); + pv1_p0->geometry().set_tangent({&pv1_path, omm::Direction::Backward}, omm::PolarCoordinates()); - auto scrambled_edges = loop.edges(); - std::swap(scrambled_edges.at(0), scrambled_edges.at(1)); - for (std::size_t i = 0; i < loop.edges().size(); ++i) { - EXPECT_NE(create_face(scrambled_edges, 0, false), create_face(loop.edges(), i, false)); - EXPECT_NE(create_face(scrambled_edges, 0, true), create_face(loop.edges(), i, false)); - EXPECT_NE(create_face(scrambled_edges, i, true), create_face(loop.edges(), 0, true)); - } + ASSERT_NO_THROW(make_copy()); + + ASSERT_NO_FATAL_FAILURE(assert_valid()); +} + +auto default_point(omm::PathVector& pv) +{ + return std::make_shared(omm::Point(), &pv); +} + +void add_empty_path(omm::PathVector& path_vector) +{ + path_vector.add_path(); +} + +void add_path_with_single_point(omm::PathVector& path_vector) +{ + auto& path = path_vector.add_path(); + path.set_single_point(default_point(path_vector)); +} +void add_path_with_single_edge(omm::PathVector& path_vector) +{ + auto& path = path_vector.add_path(); + path.add_edge(default_point(path_vector), default_point(path_vector)); +} + +void modify_position_of_first_point(omm::PathVector& path_vector) +{ + // assuming path_vector with at least one point + auto& geometry = (*path_vector.points().begin())->geometry(); + geometry.set_position(geometry.position() + omm::Vec2f{1.0, 2.0}); +} + +void modify_tangent_of_first_point(omm::PathVector& path_vector) +{ + // assuming path_vector with at least one point + auto& geometry = (*path_vector.points().begin())->geometry(); + auto& [key, value] = *geometry.tangents().begin(); + value.magnitude += 1.0; + geometry.set_tangent(key, value); } -TEST(Path, face_detection) +void modify_topology(omm::PathVector& path_vector) { + // assuming path vector with horizontal and vertical path with three points each, common center point. + const auto& vertical_path = *path_vector.paths().at(1); + auto& independent_vertical_path = path_vector.add_path(); + + auto top = vertical_path.edges().at(0)->a(); + auto bottom = vertical_path.edges().at(1)->b(); + auto center = std::make_shared(vertical_path.edges().at(0)->b()->geometry(), &path_vector); + path_vector.remove_path(vertical_path); + independent_vertical_path.add_edge(top, center); + independent_vertical_path.add_edge(center, bottom); + // Now the path vector is geometrically the same as before but the center point is independent +} + +class PathVectorEqualityTestCase +{ +public: + using PathVectorModifier = std::function; + + PathVectorEqualityTestCase(std::string label, std::list modifiers) + : m_label(std::move(label)), m_modifiers(std::move(modifiers)) + { + } + + static auto empty_path_vector() + { + return PathVectorEqualityTestCase( + "Empty Path Vector", + std::list{add_empty_path, add_path_with_single_point, add_path_with_single_edge}); + } + + static auto small_path_vector() + { + auto test_case = PathVectorEqualityTestCase( + "Small Path Vector", + std::list{add_empty_path, add_path_with_single_point, add_path_with_single_edge, + modify_position_of_first_point, modify_tangent_of_first_point}); - omm::PathVector path_vector; + using PC = omm::PolarCoordinates; + auto& path = test_case.path_vector().add_path(); + const auto tk = [&path](PC fwd, PC bwd) { + return std::map{ + {{&path, omm::Direction::Forward}, std::move(fwd)}, + {{&path, omm::Direction::Backward}, std::move(bwd)}, + }; + }; + const std::vector geometries{ + omm::Point{{0.0, 0.2}, tk(PC{0.0, 1.0}, PC{M_PI, 1.0})}, + omm::Point{{1.0, 0.3}, tk(PC{-M_PI / 2.0, 1.2}, PC{M_PI / 2.0, 1.4})}, + omm::Point{{2.0, 0.1}, tk(PC{0.0, 0.1}, PC{M_PI / 4, 1.3})}, + }; + + const auto points = util::transform(geometries, [&test_case](const omm::Point& geometry) { + return std::make_shared(geometry, &test_case.path_vector()); + }); + + path.add_edge(points.at(0), points.at(1)); + path.add_edge(points.at(1), points.at(2)); + return test_case; + } + + static auto cross_path_vector() + { + auto test_case = PathVectorEqualityTestCase( + "Crossing Path Vector", + std::list{add_empty_path, add_path_with_single_point, add_path_with_single_edge, + modify_position_of_first_point, modify_tangent_of_first_point, modify_topology}); - // define following path vector: - // - // (3) --- (2,8) --- (7) - // | | | - // | | | - // (0,4) --- (1,5) --- (6) + auto& horizontal_path = test_case.path_vector().add_path(); + auto& vertical_path = test_case.path_vector().add_path(); - using omm::Path; - using omm::Point; - using omm::Graph; + auto center = std::make_shared(omm::Point{{0.0, 0.0}}, &test_case.path_vector()); + auto right = std::make_shared(omm::Point{{1.0, 0.0}}, &test_case.path_vector()); + auto left = std::make_shared(omm::Point{{-1.0, 0.0}}, &test_case.path_vector()); + auto top = std::make_shared(omm::Point{{0.0, -1.0}}, &test_case.path_vector()); + auto bottom = std::make_shared(omm::Point{{0.0, 1.0}}, &test_case.path_vector()); - const auto as = path_vector.add_path(std::make_unique(std::deque{ - Point{{0.0, 0.0}}, // 0 - Point{{1.0, 0.0}}, // 1 - Point{{1.0, 1.0}}, // 2 - Point{{0.0, 1.0}}, // 3 - Point{{0.0, 0.0}}, // 4 - })).points(); + horizontal_path.add_edge(left, center); + horizontal_path.add_edge(center, right); + vertical_path.add_edge(top, center); + vertical_path.add_edge(center, bottom); - const auto bs = path_vector.add_path(std::make_unique(std::deque{ - Point{{1.0, 0.0}}, // 5 - Point{{2.0, 0.0}}, // 6 - Point{{2.0, 1.0}}, // 7 - Point{{1.0, 1.0}}, // 8 - })).points(); + return test_case; + } - path_vector.joined_points().insert({as[0], as[4]}); - path_vector.joined_points().insert({as[1], bs[0]}); - path_vector.joined_points().insert({as[2], bs[3]}); + [[nodiscard]] const auto& modifiers() const noexcept + { + return m_modifiers; + } - const Graph graph{path_vector}; - const auto faces = graph.compute_faces(); - ASSERT_EQ(faces.size(), 2); - ASSERT_EQ(faces[0], make_face(path_vector, {{0, 1}, {1, 2}, {2, 3}, {3, 4}})); - ASSERT_EQ(faces[1], make_face(path_vector, {{5, 6}, {6, 7}, {7, 8}, {1, 2}})); + friend std::ostream& operator<<(std::ostream& os, const PathVectorEqualityTestCase& test_case) + { + return os << test_case.m_label; + } + + [[nodiscard]] omm::PathVector& path_vector() const noexcept + { + return *m_path_vector; + } + +private: + std::string m_label; + std::list m_modifiers; + std::shared_ptr m_path_vector = std::make_shared(); +}; + +class PathVectorEquality : public ::testing::TestWithParam +{ +}; + +TEST_P(PathVectorEquality, PathVectorEquality) +{ + const auto& test_case = GetParam(); + + const auto copy = test_case.path_vector(); + EXPECT_EQ(test_case.path_vector(), copy) << "Expected copy of PathVector to equal original."; + + for (const auto& modifier : test_case.modifiers()) { + auto copy = test_case.path_vector(); + modifier(copy); + EXPECT_NE(test_case.path_vector(), copy) << "Expected PathVector not to equal the original after modifying it."; + } } + +const auto values = ::testing::ValuesIn(std::vector{ + PathVectorEqualityTestCase::empty_path_vector(), + PathVectorEqualityTestCase::small_path_vector(), + PathVectorEqualityTestCase::cross_path_vector(), +}); +INSTANTIATE_TEST_SUITE_P(P, PathVectorEquality, values); diff --git a/test/unit/polygontest.cpp b/test/unit/polygontest.cpp new file mode 100644 index 000000000..062e14607 --- /dev/null +++ b/test/unit/polygontest.cpp @@ -0,0 +1,56 @@ +#include "fmt/format.h" +#include "geometry/line.h" +#include "geometry/vec2.h" +#include "logging.h" +#include "path/face.h" +#include "gtest/gtest.h" + +struct TestCase +{ + std::vector polygon; + std::vector expected_points_inside; + std::vector expected_points_outside; + + friend std::ostream& operator<<(std::ostream& os, const TestCase& tc) + { + const auto ps = util::transform(tc.polygon, [](const auto& v) { return fmt::format("({}, {})", v.x, v.y); }); + return os << fmt::format("{}", fmt::join(ps.begin(), ps.end(), "-")); + } +}; + +class PolygonContainsTest : public ::testing::TestWithParam +{ +}; + +TEST_P(PolygonContainsTest, Contains) +{ + auto test_case = GetParam(); + + // the corner points are expected to be in the polygon. + test_case.expected_points_inside.insert(test_case.expected_points_inside.end(), test_case.polygon.begin(), + test_case.polygon.end()); + + for (const auto& p : test_case.expected_points_inside) { + ASSERT_NE(omm::polygon_contains(test_case.polygon, p), omm::PolygonLocation::Outside); + } + + for (const auto& p : test_case.expected_points_outside) { + ASSERT_EQ(omm::polygon_contains(test_case.polygon, p), omm::PolygonLocation::Outside); + } +} + +// clang-format off +const auto polygon_test_cases = ::testing::Values( +TestCase{{{0.0, 0.0}, {1.0, 1.0}, {2.0, 0.0}, {1.0, -1.0}}, + {{0.5, 0.5}, {1.0, 0.5}, {0.5, 0.0}, {1.0, -0.5}}, + {{2.0, 2.0}, {1.0, 2.0}, {0.5, 2.0}, {0.5, -2.0}, {-2.0, 0.5}, {0.0, 0.5}} +}, +TestCase{{{0.0, 0.0}, {1.0, 0.0}, {1.0, 1.0}, {0.0, 1.0}}, + {{0.5, 0.5}, {1.0, 0.5}, {0.5, 1.0}, {0.5, 0.0}, {0.0, 0.5}}, + {{2.0, 2.0}, {1.0, 2.0}, {0.5, 2.0}, {0.5, -2.0}, {-2.0, 0.5}, {2.0, 0.0}} +} +); + +// clang-format on + +INSTANTIATE_TEST_SUITE_P(P, PolygonContainsTest, polygon_test_cases); diff --git a/test/unit/serialization.cpp b/test/unit/serialization.cpp index 1b1d2c9e1..fcf4bca7b 100644 --- a/test/unit/serialization.cpp +++ b/test/unit/serialization.cpp @@ -19,13 +19,6 @@ namespace { -std::unique_ptr options() -{ - return std::make_unique(false, // is_cli - false // have_opengl - ); -} - bool scene_eq(const nlohmann::json& a, const nlohmann::json& b) { static constexpr auto object_t = nlohmann::detail::value_t::object; @@ -138,7 +131,7 @@ TEST(serialization, SceneEq) TEST(serialization, JSONInvalidScene) { - ommtest::Application qt_app{options()}; + ommtest::Application qt_app; nlohmann::json json_file; omm::serialization::JSONDeserializer deserializer(json_file); EXPECT_FALSE(omm::SceneSerialization{*qt_app.omm_app().scene}.load(deserializer)); @@ -146,7 +139,7 @@ TEST(serialization, JSONInvalidScene) TEST(serialization, BinaryInvalidScene) { - ommtest::Application qt_app{options()}; + ommtest::Application qt_app; nlohmann::json json_file; omm::serialization::JSONDeserializer deserializer(json_file); EXPECT_FALSE(omm::SceneSerialization{*qt_app.omm_app().scene}.load(deserializer)); @@ -221,8 +214,7 @@ class SceneFromFileInvariance : public testing::TestWithParam { protected: SceneFromFileInvariance() - : m_app(ommtest::Application(options())) - , m_scene(*m_app.omm_app().scene) + : m_scene(*m_app.omm_app().scene) { } diff --git a/test/unit/testutil.cpp b/test/unit/testutil.cpp index 2d1f97dad..219c0cf66 100644 --- a/test/unit/testutil.cpp +++ b/test/unit/testutil.cpp @@ -1,17 +1,35 @@ #include "testutil.h" -#include "registers.h" #include "main/application.h" #include "main/options.h" +#include "path/dedge.h" +#include "path/path.h" +#include "path/pathpoint.h" +#include "path/pathvector.h" +#include "registers.h" +#include "gtest/gtest.h" #include #include #include +#include + +namespace +{ + +std::unique_ptr options() +{ + return std::make_unique(false, // is_cli + false // have_opengl + ); +} + +} // namespace namespace ommtest { -Application::Application(std::unique_ptr options) +Application::Application() : m_q_application(argc, argv.data()) - , m_omm_application(std::make_unique(m_q_application, std::move(options))) + , m_omm_application(std::make_unique(m_q_application, options())) { } @@ -22,10 +40,71 @@ omm::Application& Application::omm_app() const return *m_omm_application; } +QString Application::test_id_for_filename() +{ + QString name(::testing::UnitTest::GetInstance()->current_test_case()->name()); + return name.replace("/", "_"); +} + bool have_opengl() { const auto value = QProcessEnvironment::systemEnvironment().value("HAVE_OPENGL", "0"); return value != "0"; } +omm::Point EllipseMaker::ith_point(const std::size_t i) const +{ + const double theta = M_PI * 2.0 * static_cast(i) / static_cast(point_count); + const auto pos = omm::Vec2f(radii.x * std::cos(theta), radii.y * std::sin(theta)) + origin; + if (no_tangents) { + return omm::Point(pos); + } else { + const auto t_scale = 4.0 / 3.0 * std::tan(M_PI / (2.0 * static_cast(point_count))); + const auto t = omm::Vec2f(-std::sin(theta), std::cos(theta)) * static_cast(t_scale); + return omm::Point(pos, omm::PolarCoordinates(-t), omm::PolarCoordinates(t)); + } +} + +omm::Path& EllipseMaker::make_path(omm::PathVector& pv) +{ + assert(edges.empty()); + auto& path = pv.add_path(); + path.set_single_point(std::make_shared(ith_point(0), &pv)); + for (std::size_t i = 1; i < point_count; ++i) { + auto new_point = std::make_shared(ith_point(i), &pv); + edges.emplace_back(&path.add_edge(path.last_point(), std::move(new_point)), omm::Direction::Forward); + } + if (closed && point_count > 1) { + edges.emplace_back(&path.add_edge(path.last_point(), path.first_point()), omm::Direction::Forward); + } + return path; +} + +std::string EllipseMaker::to_string() const +{ + const auto s_open_closed = closed ? "closed" : "open"; + const auto s_interp = no_tangents ? "linear" : "smooth"; + return fmt::format("{}-Ellipse-{}-{}", point_count, s_open_closed, s_interp); +} + +std::set> EllipseMaker::faces() const +{ + if (closed) { + return {edges}; + } + return {}; +} + +double EllipseMaker::area() const +{ + return M_PI * radii.x * radii.y; +} + +std::list> PathVectorHeap::m_path_vectors; + +omm::PathVector* PathVectorHeap::annex(std::unique_ptr pv) +{ + return m_path_vectors.emplace_back(std::move(pv)).get(); +} + } // namespace ommtest diff --git a/test/unit/testutil.h b/test/unit/testutil.h index 2b78d5cf0..085b4cad2 100644 --- a/test/unit/testutil.h +++ b/test/unit/testutil.h @@ -1,15 +1,21 @@ #pragma once +#include "geometry/point.h" +#include "geometry/vec2.h" +#include "path/dedge.h" #include -#include #include +#include #include +#include namespace omm { + class Application; class PythonEngine; class Options; +class PathVector; } // namespace omm namespace ommtest @@ -29,9 +35,10 @@ std::vector string_array_to_charpp(std::array& string_arr class Application { public: - explicit Application(std::unique_ptr options); + explicit Application(); ~Application(); omm::Application& omm_app() const; + [[nodiscard]] static QString test_id_for_filename(); private: std::array argv_{"test", "-platform", "offscreen"}; @@ -43,6 +50,39 @@ class Application bool have_opengl(); -#define SKIP_IF_NO_OPENGL do { if (!ommtest::have_opengl()) { GTEST_SKIP(); } } while (false) +#define SKIP_IF_NO_OPENGL \ + do { \ + if (!ommtest::have_opengl()) { \ + GTEST_SKIP(); \ + } \ + } while (false) + +class PathVectorHeap +{ +protected: + omm::PathVector* annex(std::unique_ptr pv); + +private: + // The TestCase objects are not persistent, hence the path vectors need to be stored beyond the + // lifetime of the test case. + static std::list> m_path_vectors; +}; + +struct EllipseMaker +{ +public: + omm::Vec2f origin; + omm::Vec2f radii; + std::size_t point_count; + bool closed; + bool no_tangents; + + std::deque edges = {}; + [[nodiscard]] omm::Point ith_point(const std::size_t i) const; + omm::Path& make_path(omm::PathVector& pv); + [[nodiscard]] std::string to_string() const; + [[nodiscard]] std::set> faces() const; + [[nodiscard]] double area() const; +}; } // namespace ommtest diff --git a/ts/omm_de.ts b/ts/omm_de.ts index 41d218304..1e410b5ba 100644 --- a/ts/omm_de.ts +++ b/ts/omm_de.ts @@ -1783,10 +1783,6 @@ Soll die Selektion trotzdem entfernt werden? Ellipse Ellipse - - show point dialog - Punktdialog anzeigen - Instance Instanz diff --git a/ts/omm_en.ts b/ts/omm_en.ts index e02b7260f..fe6332379 100644 --- a/ts/omm_en.ts +++ b/ts/omm_en.ts @@ -1783,10 +1783,6 @@ Remove the selected items anyway? Ellipse Ellipse - - show point dialog - Show point dialog - Instance Instance diff --git a/ts/omm_es.ts b/ts/omm_es.ts index 4a93a1d0e..7f5be8223 100644 --- a/ts/omm_es.ts +++ b/ts/omm_es.ts @@ -1783,10 +1783,6 @@ Borrar la selección de todos modos? Ellipse Elipsis - - show point dialog - Mostrar diálogo para editar puntos - Instance Instancia diff --git a/uicolors/ui-colors-dark.cfg b/uicolors/ui-colors-dark.cfg index 67a7eaa5f..996265156 100644 --- a/uicolors/ui-colors-dark.cfg +++ b/uicolors/ui-colors-dark.cfg @@ -76,6 +76,7 @@ particle: #ffff80ff/#ffff00ff/#ffff00ff particle fill: #ffff80ff/#ffff00ff/#ffff00ff point: #000000ff/#000000ff/#000000ff point fill: #ffffffff/#ffff00ff/#ffffffff +face: #ffffff40/#ffff0020/#ffffff00 tangent: #000000ff/#000000ff/#000000ff tangent fill: #ffffffff/#ff0000ff/#0000ffff marker: #0000ffff/#0000ffff/#0000ffff diff --git a/uicolors/ui-colors-light.cfg b/uicolors/ui-colors-light.cfg index da3b02f43..313ba9a4f 100644 --- a/uicolors/ui-colors-light.cfg +++ b/uicolors/ui-colors-light.cfg @@ -67,8 +67,8 @@ y-axis-fill: #80ff80ff/#00ff00ff/#ffffffff y-axis-outline: #008000ff/#000000ff/#008000ff rotate-ring-fill: #8080ffff/#0000ffff/#ffffffff rotate-ring-outline: #000080ff/#000000ff/#000080ff -band: #000000ff/#000000ff/#00000000 band fill: #808080ff/#A0A0A0ff/#ffffffff +band: #000000ff/#000000ff/#00000000 bounding-box: #000000ff/#00000080/#ffffffff object: #ffff00ff/#A0A020ff/#ffffffff object fill: #ffff10ff/#B0B030ff/#ffffffff @@ -76,6 +76,7 @@ particle: #ffff80ff/#ffff00ff/#ffffffff particle fill: #ffff80ff/#ffff00ff/#ffffffff point: #000000ff/#000000ff/#000000ff point fill: #ffffffff/#ffff00ff/#ffff00ff +face: #ffffff40/#ffff0020/#ffff0000 tangent: #000000ff/#000000ff/#000000ff tangent fill: #ffffffff/#ff0000ff/#0000ffff marker: #0000ffff/#0000ffff/#0000ffff